Engineering note ยท System

EXIF(Exchangeable Image File Format) ๋ฐ์ดํ„ฐ

2025๋…„ 08์›” 19์ผ์ดํ˜„์ˆ˜
ํƒœ๊ทธandroidmobile

์›น๊ณผ ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ์‚ฌ์ง„์˜ EXIF ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•. ๋ธŒ๋ผ์šฐ์ €์˜ ๋ณด์•ˆ ํŠน์„ฑ ์ƒ ์›น์—์„œ๋Š” EXIF ๋ฅผ ์˜๋„์ ์œผ๋กœ ๋ˆ„๋ฝ์‹œํ‚ด.

**EXIF(Exchangeable Image File Format)**๋Š” ์‚ฌ์ง„ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ž์ฒด ์™ธ์—, ๊ทธ ์‚ฌ์ง„์ด ์–ด๋–ป๊ฒŒ, ์–ธ์ œ, ์–ด๋””์„œ, ๋ฌด์—‡์œผ๋กœ ์ฐํ˜”๋Š”์ง€์— ๋Œ€ํ•œ ๊ตฌ์ฒด์ ์ธ **๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ)**๋ฅผ ๋‹ด๊ณ  ์žˆ๋‹ค.

  • ์นด๋ฉ”๋ผ ์ •๋ณด: ์–ด๋–ค ์žฅ๋น„๋กœ ์‚ฌ์ง„์„ ์ฐ์—ˆ๋Š”์ง€ ์•Œ๋ ค์ค€๋‹ค.
    • ์นด๋ฉ”๋ผ ์ œ์กฐ์‚ฌ ๋ฐ ๋ชจ๋ธ: ์˜ˆ) SAMSUNG, Apple, SM-S928N, iPhone 15 Pro
    • ๋ Œ์ฆˆ ์ •๋ณด: ์‚ฌ์šฉ๋œ ๋ Œ์ฆˆ์˜ ์ข…๋ฅ˜, ์ดˆ์  ๊ฑฐ๋ฆฌ ๋“ฑ
  • ์ดฌ์˜ ์„ค์ •๊ฐ’: ์‚ฌ์ง„์„ ์–ด๋–ค ์„ค์ •์œผ๋กœ ์ฐ์—ˆ๋Š”์ง€์— ๋Œ€ํ•œ ๊ธฐ์ˆ ์ ์ธ ์ •๋ณด.
    • ISO ๊ฐ๋„: ๋น›์— ์–ผ๋งˆ๋‚˜ ๋ฏผ๊ฐํ•˜๊ฒŒ ๋ฐ˜์‘ํ–ˆ๋Š”์ง€
    • ์กฐ๋ฆฌ๊ฐœ ๊ฐ’ (F-stop): ๋ Œ์ฆˆ๋ฅผ ์–ผ๋งˆ๋‚˜ ์—ด์—ˆ๋Š”์ง€ (์‹ฌ๋„ ํ‘œํ˜„๊ณผ ๊ด€๋ จ)
    • ์…”ํ„ฐ ์†๋„: ์–ผ๋งˆ๋‚˜ ๋น ๋ฅธ ์†๋„๋กœ ๋น›์„ ๋ฐ›์•„๋“ค์˜€๋Š”์ง€ (์›€์ง์ž„ ํ‘œํ˜„๊ณผ ๊ด€๋ จ)
    • ๋…ธ์ถœ ๋ณด์ •, ํ™”์ดํŠธ ๋ฐธ๋Ÿฐ์Šค, ํ”Œ๋ž˜์‹œ ์‚ฌ์šฉ ์—ฌ๋ถ€ ๋“ฑ
  • ์‹œ๊ฐ„ ๋ฐ ์œ„์น˜ ์ •๋ณด: ์‚ฌ์ง„ ์ฐ์€ ์‹œ๊ฐ„, ์œ„๊ฒฝ๋„
    • ์ดฌ์˜ ๋‚ ์งœ ๋ฐ ์‹œ๊ฐ„: ์‚ฌ์ง„์„ ์ฐ์€ ์ •ํ™•ํ•œ ๋‚ ์งœ์™€ ์‹œ๊ฐ„.
    • GPS ์ขŒํ‘œ (์œ„๋„, ๊ฒฝ๋„): ์นด๋ฉ”๋ผ์˜ ์œ„์น˜ ์ •๋ณด ๊ธฐ๋ก ๊ธฐ๋Šฅ์ด ์ผœ์ ธ ์žˆ์–ด์•ผ ํ•จ.

๋‚ด๊ฐ€ ํ•„์š”ํ•œ ๊ฑด ์‹œ๊ฐ„ ๋ฐ ์œ„์น˜ ์ •๋ณด๊ฐ€ ๋‚จ๊ธฐ๋ฅผ ๋ฐ”๋ž€๋‹ค.

webview ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด์„œ ์—…๋กœ๋“œ ์‹œ

๋ธŒ๋ผ์šฐ์ €์—์„œ <input type="file"> ๋ฅผ ํ†ตํ•ด ํŒŒ์ผ์„ ์„ ํƒํ•˜๋ฉด, ์•ˆ๋“œ๋กœ์ด๋“œ์˜ ํŒŒ์ผ ์„ ํƒ๊ธฐ๊ฐ€ ์—ด๋ฆฌ๊ณ , ๋ธŒ๋ผ์šฐ์ €๋Š” ํŒŒ์ผ์˜ ๋ณต์‚ฌ๋ณธ์„ ๋งŒ๋“ค์–ด๋‘”๋‹ค. ์ด ๊ณผ์ •์—์„œ ๋ธŒ๋ผ์šฐ์ €๋Š” ๊ฐœ์ธ์ •๋ณด ์œ ์ถœ์„ ๋ง‰๊ธฐ ์œ„ํ•ด์„œ EXIF ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋„์ ์œผ๋กœ ์‚ญ์ œํ•œ๋‹ค.

์›น๋ทฐ์˜ ๊ฒฝ์šฐ onShowFileChooser ์—์„œ ๊ฐœ๋ฐœ์ž๊ฐ€ EXIF ์ •๋ณด๊ฐ€ ๋ณด์กด๋˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค. ACCESS_MEDIA_LOCATION ๊ถŒํ•œ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ—ˆ๊ฐ€๋ฐ›์œผ๋ฉด ํŒŒ์ผ์˜ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋ณด์กดํ•  ์ˆ˜ ์žˆ๋‹ค

xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />

onShowFileChooser ์—์„œ ํŒŒ์ผ ์„ ํƒ ๊ฒฐ๊ณผ๋ฅผ ActivityResultLauncher ๋กœ ๋ฐ›๋Š”๋ฐ, ์ด๋•Œ Uri ๋ฅผ ๊ฐ€๊ณตํ•˜์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ filePathCallback.onReceiveValue() ์— ์ „๋‹ฌํ•˜๋ฉด ๋œ๋‹ค.

kotlin
// EXIF ์ •๋ณด ๋””๋ฒ„๊ทธ ์ถœ๋ ฅ  
try {  
    val uri = data?.data  
    if (uri != null) {  
        activity.contentResolver.openInputStream(uri)?.use { inputStream ->  
            val exif = ExifInterface(inputStream)  
            Log.d(TAG, "Single File EXIF Info:")  
            Log.d(TAG, "  DateTime: ${exif.getAttribute(ExifInterface.TAG_DATETIME)}")  
            Log.d(TAG, "  GPS Latitude: ${exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)}")  
            Log.d(TAG, "  GPS Longitude: ${exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)}")  
            Log.d(TAG, "  Make: ${exif.getAttribute(ExifInterface.TAG_MAKE)}")  
            Log.d(TAG, "  Model: ${exif.getAttribute(ExifInterface.TAG_MODEL)}")  
            Log.d(TAG, "  Orientation: ${exif.getAttribute(ExifInterface.TAG_ORIENTATION)}")  
        }  
    }  
} catch (e: Exception) {  
    Log.e(TAG, "EXIF ์ •๋ณด ์ฝ๊ธฐ ์‹คํŒจ", e)  
}
text
Single File EXIF Info:
   DateTime: 2025:07:18 08:43:28
   GPS Latitude: 37/1,0/1,0/100000
   GPS Longitude: 127/1,0/1,0/00000
   Make: samsung
   Model: SM-N960N
   Orientation: 6

ACCESS_MEDIA_LOCATION ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๋ฉด GPS ์ •๋ณด๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๊ธฐ๋ก๋œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด 0/1๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ๊ธฐ๋ก๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ถŒํ•œ์„ ๋ช…์‹œ์ ์œผ๋กœ ๋ฐ›์•„์•ผ ํ•œ๋‹ค.

์›น์—์„œ exif ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ

์›น์—์„œ ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ํ–ˆ์„ ๋•Œ EXIF ์ •๋ณด๊ฐ€ ๋ˆ„๋ฝ๋˜๋Š” ํ˜„์ƒ์ด ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์œ„ ์‚ฌ์ดํŠธ์—์„œ๋Š” ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค. ๋ฌด์Šจ ์ฐจ์ด๊ฐ€ ์žˆ์„๊นŒ?

์œ„ ํŽ˜์ด์ง€๋Š” ํŒŒ์ผ์„ <form> ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด ๊ทธ๋Œ€๋กœ ์„œ๋ฒ„๋กœ ์ „๋‹ฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— EXIF ๋ฐ์ดํ„ฐ์˜ ์†์‹ค ์—†์ด ์ „๋‹ฌ๋˜์–ด ๋ถ„์„์ด ๊ฐ€๋Šฅํ•œ ๊ฒƒ์ด๋‹ค. ๋ฐ˜๋ฉด ๋ธŒ๋ผ์šฐ์ € JavaScript์—์„œ ํŒŒ์ผ ๋ฐ์ดํ„ฐ์— ์ง์ ‘ ์ ‘๊ทผํ•˜๋ ค๊ณ  ํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ € ๋ณด์•ˆ ์ •์ฑ…์ƒ ์ผ๋ถ€ ์ •๋ณด(EXIF ๋“ฑ)๊ฐ€ ์ฐจ๋‹จ๋  ์ˆ˜ ์žˆ๋‹ค.

์›น๋ทฐ์—์„œ native ๋กœ์ง์„ ํ†ตํ•ด ์ „๋‹ฌ๋œ ๊ฒฝ์šฐ

์•ฑ์ด OS ๊ถŒํ•œ์„ ์–ป์–ด์„œ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ณธ ์ •์ฑ…์ด ์•„๋‹Œ ์•ฑ์˜ ๊ถŒํ•œ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ์ „๋‹ฌํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ์•ฑ์„ ํ†ตํ•ด ํŒŒ์ผ์„ ์„ ํƒํ•œ ๊ฒฝ์šฐ ์›น์—์„œ๋„ EXIF ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ  ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

๋‹ค๋งŒ EXIF ๋„ ๊ฐœ์ธ์ •๋ณด๋กœ ์ทจ๊ธ‰ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ €์žฅ๊ณผ ๋…ธ์ถœ์— ์ฃผ์˜๋ฅผ ํ•ด์•ผํ•œ๋‹ค. ์„œ๋ฒ„์— ์ €์žฅํ•  ๊ฒฝ์šฐ ์ •๋ง ํ•„์š”ํ•œ ๋งŒํผ๋งŒ ํŠน์ •๋˜์ง€ ์•Š์„ ์ •๋„์˜ ์ •๋ณด๋งŒ ์ทจ๊ธ‰ํ•˜๊ฑฐ๋‚˜, ์ €์žฅํ•˜์ง€ ์•Š๋Š” ๊ฒŒ ์ข‹๋‹ค.

์ •๋ถ€ ํŽ˜์ด์ง€์—์„œ EXIF ๋ฐ์ดํ„ฐ์˜ ์œ„ํ—˜์„ฑ๊ณผ ๋Œ€์‘๋ฒ•๋„ ํ™๋ณดํ•˜๊ณ  ์žˆ๋‹ค.

๋ฐ์ดํ„ฐ ํ˜•์‹

ts
fileList.forEach((file) => {
    exifr.parse(file)
        .then((metadata: any) => {
            console.log(`[EXIF] ${file.name}`, metadata);
        })
        .catch((error: any) => {
            console.error(`[EXIF] Failed to parse EXIF for ${file.name}`, error);
        });
});

exifr ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•ด์„œ EXIF ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ–ˆ๋‹ค. parse ๊ฒฐ๊ณผ๊ฐ’์—์„œ ์›ํ•˜๋Š” ์ •๋ณด๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋œ๋‹ค.

exif ๋ฐ์ดํ„ฐ ์‚ฝ์ž…ํ•˜๊ธฐ (piexif ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)

์ด๋ฏธ์ง€๋ฅผ ํ›„์ฒ˜๋ฆฌํ•˜๋‹ค๊ฐ€ EXIF ๋ฐ์ดํ„ฐ๊ฐ€ ์†Œ์‹ค๋˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์บ”๋ฒ„์Šค(Canvas)์— ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋ฆฌ๊ณ  ์ถ”๊ฐ€์ ์ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ ๋’ค ๋‹ค์‹œ ์ด๋ฏธ์ง€๋กœ ์ถ”์ถœํ–ˆ์„ ๋•Œ๋Š” EXIF ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๋Š” ์ƒํƒœ๊ฐ€ ๋œ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ์—๋Š” ๋ฏธ๋ฆฌ ๊ฐ€์ง€๊ณ  ์žˆ๋˜ EXIF ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ด๋‘์—ˆ๋‹ค๊ฐ€ ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€์— ์ฃผ์ž…ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

exifr ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์ฝ๊ธฐ ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋ฏ€๋กœ, EXIF ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์ผ์— ์“ฐ๋Š” ๊ธฐ๋Šฅ์€ ์—†๋‹ค. piexif ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์ž.

ts
// EXIF ์ €์žฅ
let exifStr: string | null = null;
try {
    const originalDataURL = await fileToDataURL(origFile);
    const exifObj = piexif.load(originalDataURL);
    exifStr = piexif.dump(exifObj);
} catch (e) { ... }

// EXIF ์‚ฝ์ž…
try {
    finalDataURL = piexif.insert(exifStr, resizeResult.dataURL);
} catch (e) { ... }

์›๋ณธ์˜ exif ๊ฐ€ ๊ทธ๋Œ€๋กœ ์ฃผ์ž…๋œ๋‹ค.

์ข‹์•„์š”์™€ ๋Œ“๊ธ€

๋Œ“๊ธ€ ๋‚จ๊ธฐ๊ธฐ

๋Œ“๊ธ€ 0๊ฐœ

๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.