From 7295517077ac83bcc46848d0dbad7cc80f3f9e27 Mon Sep 17 00:00:00 2001 From: lodev09 Date: Mon, 23 Feb 2026 00:28:19 +0800 Subject: [PATCH 1/4] fix(android): use file path for ExifInterface read on file:// URIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use file path constructor for file:// URIs instead of InputStream, giving ExifInterface seek capability. Bump exifinterface 1.3.7 → 1.4.2. --- android/build.gradle | 2 +- .../java/com/lodev09/exify/ExifyModule.kt | 68 ++++++++++++++----- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index ac72c92..e3bc546 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -64,5 +64,5 @@ android { dependencies { implementation "com.facebook.react:react-android" - implementation "androidx.exifinterface:exifinterface:1.3.7" + implementation "androidx.exifinterface:exifinterface:1.4.2" } diff --git a/android/src/main/java/com/lodev09/exify/ExifyModule.kt b/android/src/main/java/com/lodev09/exify/ExifyModule.kt index 1171124..391738d 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyModule.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyModule.kt @@ -16,6 +16,7 @@ import com.facebook.react.modules.core.PermissionAwareActivity import com.facebook.react.modules.core.PermissionListener import com.facebook.react.util.RNLog import com.lodev09.exify.ExifyUtils.formatTags +import java.io.File import java.io.IOException private const val ERROR_TAG = "E_EXIFY_ERROR" @@ -72,35 +73,66 @@ class ExifyModule( promise: Promise, ) { try { - val inputStream = - if (scheme == "http" || scheme == "https") { - java.net.URL(uri).openStream() - } else if (scheme == "content" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - try { - context.contentResolver.openInputStream(MediaStore.setRequireOriginal(photoUri)) - } catch (e: SecurityException) { - context.contentResolver.openInputStream(photoUri) - } + val exif = + if (scheme == "file") { + ExifInterface(photoUri.path!!) } else { - context.contentResolver.openInputStream(photoUri) + val inputStream = + if (scheme == "content" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + context.contentResolver.openInputStream(MediaStore.setRequireOriginal(photoUri)) + } catch (_: SecurityException) { + context.contentResolver.openInputStream(photoUri) + } + } else if (scheme == "http" || scheme == "https") { + java.net.URL(uri).openStream() + } else { + context.contentResolver.openInputStream(photoUri) + } + + if (inputStream == null) { + RNLog.w(context, "Exify: Could not open URI: $uri") + promise.reject(ERROR_TAG, "Could not open URI: $uri") + return + } + + inputStream.use { ExifInterface(it) } } - if (inputStream == null) { - RNLog.w(context, "Exify: Could not open URI: $uri") - promise.reject(ERROR_TAG, "Could not open URI: $uri") - return - } + val tags = formatTags(exif) - inputStream.use { - val tags = formatTags(ExifInterface(it)) - promise.resolve(tags) + // ExifInterface ignores IFD0 tags placed in ExifIFD (non-standard but common + // with some image editors). Fall back to raw EXIF parsing for missing tags. + val missingTags = + IFD0_STRING_TAGS.filter { !tags.hasKey(it) }.toSet() + if (missingTags.isNotEmpty()) { + val fallback = + try { + openInputStream(uri, photoUri, scheme)?.use { readFallbackTags(it, missingTags) } + } catch (_: Exception) { + null + } + fallback?.forEach { (tag, value) -> tags.putString(tag, value) } } + + promise.resolve(tags) } catch (e: Exception) { RNLog.w(context, "Exify: ${e.message}") promise.reject(ERROR_TAG, e.message, e) } } + private fun openInputStream( + uri: String, + photoUri: Uri, + scheme: String, + ): java.io.InputStream? = + when (scheme) { + "file" -> File(photoUri.path!!).inputStream() + "content" -> context.contentResolver.openInputStream(photoUri) + else -> java.net.URL(uri).openStream() + } + @Throws(IOException::class) override fun write( uri: String, From dcc5a975d6015093152a8fd410beeddf996f7c2c Mon Sep 17 00:00:00 2001 From: lodev09 Date: Mon, 23 Feb 2026 00:28:25 +0800 Subject: [PATCH 2/4] fix(android): read IFD0 tags misplaced in ExifIFD Some image editors (e.g. ON1 Photo RAW) place IFD0 tags like Make, Model, Artist, Copyright inside ExifIFD. Android's ExifInterface ignores them there. Add a lightweight fallback JPEG parser that scans the ExifIFD for these missing tags. --- .../java/com/lodev09/exify/ExifFallback.kt | 209 ++++++++++++++++++ .../main/java/com/lodev09/exify/ExifyTags.kt | 14 ++ .../main/java/com/lodev09/exify/ExifyUtils.kt | 4 +- 3 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 android/src/main/java/com/lodev09/exify/ExifFallback.kt diff --git a/android/src/main/java/com/lodev09/exify/ExifFallback.kt b/android/src/main/java/com/lodev09/exify/ExifFallback.kt new file mode 100644 index 0000000..7134e47 --- /dev/null +++ b/android/src/main/java/com/lodev09/exify/ExifFallback.kt @@ -0,0 +1,209 @@ +package com.lodev09.exify + +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * IFD0 string tags that ExifInterface may miss when an image editor + * (e.g. ON1 Photo RAW) places them inside the ExifIFD instead of IFD0. + * + * Maps EXIF tag number → ExifInterface tag name. + */ +private val FALLBACK_TAGS = + mapOf( + 0x010F to "Make", + 0x0110 to "Model", + 0x013B to "Artist", + 0x8298 to "Copyright", + 0x010E to "ImageDescription", + 0x0131 to "Software", + ) + +private const val IFD_FORMAT_STRING = 2 +private const val IFD_FORMAT_UNDEFINED = 7 +private const val EXIF_IFD_POINTER_TAG = 0x8769 + +/** + * Scans raw JPEG bytes for IFD0 string tags that may have been placed in + * the ExifIFD. Returns a map of tag name → value for any tags found. + */ +fun readFallbackTags( + inputStream: InputStream, + missingTags: Set, +): Map { + if (missingTags.isEmpty()) return emptyMap() + + val neededTagNumbers = FALLBACK_TAGS.filterValues { it in missingTags }.keys + if (neededTagNumbers.isEmpty()) return emptyMap() + + val bytes = readExifSegment(inputStream) ?: return emptyMap() + val result = mutableMapOf() + + val app1 = findApp1Exif(bytes) ?: return emptyMap() + val tiffOffset = app1.tiffOffset + val buf = ByteBuffer.wrap(bytes) + buf.order(app1.byteOrder) + + // Read IFD0 to find ExifIFD offset (offsets are relative to TIFF start) + val ifd0Offset = buf.getInt(tiffOffset + 4) + val exifIfdOffset = findExifIfdOffset(buf, tiffOffset, ifd0Offset) ?: return emptyMap() + + // Scan ExifIFD entries for our missing tags + scanIfd(buf, tiffOffset, exifIfdOffset, neededTagNumbers, result) + + return result +} + +/** + * Reads only the JPEG header segments up to and including the EXIF APP1, + * avoiding reading the full image file into memory. + */ +private fun readExifSegment(inputStream: InputStream): ByteArray? { + val dis = java.io.DataInputStream(inputStream) + val header = ByteArray(2) + dis.readFully(header) + if (header[0] != 0xFF.toByte() || header[1] != 0xD8.toByte()) return null + + val out = java.io.ByteArrayOutputStream(65536) + out.write(header) + + val segHeader = ByteArray(4) + while (true) { + try { + dis.readFully(segHeader) + } catch (_: java.io.EOFException) { + break + } + val marker = segHeader[1].toInt() and 0xFF + val segLen = ((segHeader[2].toInt() and 0xFF) shl 8) or (segHeader[3].toInt() and 0xFF) + + if (segHeader[0] != 0xFF.toByte() || segLen < 2) break + + out.write(segHeader) + val segData = ByteArray(segLen - 2) + dis.readFully(segData) + out.write(segData) + + // Found EXIF APP1 — we have enough + if (marker == 0xE1 && segData.size >= 6 && + segData[0] == 0x45.toByte() && + segData[1] == 0x78.toByte() && + segData[2] == 0x69.toByte() && + segData[3] == 0x66.toByte() + ) { + break + } + + // Stop if we hit SOS or image data + if (marker == 0xDA) break + } + + return out.toByteArray() +} + +private data class App1Info(val tiffOffset: Int, val byteOrder: ByteOrder) + +private fun findApp1Exif(bytes: ByteArray): App1Info? { + if (bytes.size < 4 || bytes[0] != 0xFF.toByte() || bytes[1] != 0xD8.toByte()) return null + + var pos = 2 + while (pos + 4 < bytes.size) { + if (bytes[pos] != 0xFF.toByte()) return null + val marker = bytes[pos + 1].toInt() and 0xFF + + // Read segment length (big-endian) + val segLen = ((bytes[pos + 2].toInt() and 0xFF) shl 8) or (bytes[pos + 3].toInt() and 0xFF) + + if (marker == 0xE1 && segLen >= 8) { + // Check for "Exif\0\0" + if (pos + 10 < bytes.size && + bytes[pos + 4] == 0x45.toByte() && // E + bytes[pos + 5] == 0x78.toByte() && // x + bytes[pos + 6] == 0x69.toByte() && // i + bytes[pos + 7] == 0x66.toByte() && // f + bytes[pos + 8] == 0x00.toByte() && + bytes[pos + 9] == 0x00.toByte() + ) { + val tiffOffset = pos + 10 + val order = + if (bytes[tiffOffset] == 0x49.toByte() && bytes[tiffOffset + 1] == 0x49.toByte()) { + ByteOrder.LITTLE_ENDIAN + } else { + ByteOrder.BIG_ENDIAN + } + return App1Info(tiffOffset, order) + } + } + + pos += 2 + segLen + } + return null +} + +private fun findExifIfdOffset( + buf: ByteBuffer, + tiffOffset: Int, + ifdOffset: Int, +): Int? { + if (ifdOffset < 0 || tiffOffset + ifdOffset + 2 > buf.limit()) return null + + val count = buf.getShort(tiffOffset + ifdOffset).toInt() and 0xFFFF + for (i in 0 until count) { + val entryOffset = tiffOffset + ifdOffset + 2 + i * 12 + if (entryOffset + 12 > buf.limit()) return null + + val tagNumber = buf.getShort(entryOffset).toInt() and 0xFFFF + if (tagNumber == EXIF_IFD_POINTER_TAG) { + return buf.getInt(entryOffset + 8) + } + } + return null +} + +private fun scanIfd( + buf: ByteBuffer, + tiffOffset: Int, + ifdOffset: Int, + neededTagNumbers: Set, + result: MutableMap, +) { + val absOffset = tiffOffset + ifdOffset + if (absOffset + 2 > buf.limit()) return + + val count = buf.getShort(absOffset).toInt() and 0xFFFF + for (i in 0 until count) { + val entryOffset = absOffset + 2 + i * 12 + if (entryOffset + 12 > buf.limit()) return + + val tagNumber = buf.getShort(entryOffset).toInt() and 0xFFFF + if (tagNumber !in neededTagNumbers) continue + + val format = buf.getShort(entryOffset + 2).toInt() and 0xFFFF + val componentCount = buf.getInt(entryOffset + 4) + + if (format != IFD_FORMAT_STRING && format != IFD_FORMAT_UNDEFINED) continue + if (componentCount <= 0 || componentCount > 1024) continue + + val dataOffset = + if (componentCount <= 4) { + entryOffset + 8 + } else { + tiffOffset + buf.getInt(entryOffset + 8) + } + + if (dataOffset < 0 || dataOffset + componentCount > buf.limit()) continue + + val strBytes = ByteArray(componentCount) + buf.position(dataOffset) + buf.get(strBytes) + + // Trim trailing null bytes + var len = strBytes.size + while (len > 0 && strBytes[len - 1] == 0.toByte()) len-- + if (len == 0) continue + + val tagName = FALLBACK_TAGS[tagNumber] ?: continue + result[tagName] = String(strBytes, 0, len, Charsets.UTF_8).trim() + } +} diff --git a/android/src/main/java/com/lodev09/exify/ExifyTags.kt b/android/src/main/java/com/lodev09/exify/ExifyTags.kt index 7535e9d..3ee5e8f 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyTags.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyTags.kt @@ -2,6 +2,20 @@ package com.lodev09.exify import androidx.exifinterface.media.ExifInterface +/** + * IFD0 string tags that ExifInterface may fail to read when they are + * incorrectly placed inside the ExifIFD by some image editors. + */ +val IFD0_STRING_TAGS = + setOf( + ExifInterface.TAG_MAKE, + ExifInterface.TAG_MODEL, + ExifInterface.TAG_ARTIST, + ExifInterface.TAG_COPYRIGHT, + ExifInterface.TAG_IMAGE_DESCRIPTION, + ExifInterface.TAG_SOFTWARE, + ) + /** * Supported Exif Tags * Note: Latitude, Longitude and Altitude tags are updated separately diff --git a/android/src/main/java/com/lodev09/exify/ExifyUtils.kt b/android/src/main/java/com/lodev09/exify/ExifyUtils.kt index 6421bd1..fabf8a1 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyUtils.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyUtils.kt @@ -2,11 +2,11 @@ package com.lodev09.exify import androidx.exifinterface.media.ExifInterface import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap object ExifyUtils { @JvmStatic - fun formatTags(exif: ExifInterface): ReadableMap { + fun formatTags(exif: ExifInterface): WritableMap { val tags = Arguments.createMap() for ((type, tag) in EXIFY_TAGS) { From a7e3e14c068df1cd19cf1304cfbff04882bc379d Mon Sep 17 00:00:00 2001 From: lodev09 Date: Mon, 23 Feb 2026 01:12:02 +0800 Subject: [PATCH 3/4] fix: normalize EXIF tags across iOS and Android - ISOSpeedRatings returns number[] on Android (was string) - skip invalid Orientation 0 on Android - add BodySerialNumber tag on Android - remap iOS PixelWidth/PixelHeight to PixelXDimension/PixelYDimension - serialize arrays without brackets in Android write path --- .../src/main/java/com/lodev09/exify/ExifyModule.kt | 4 +++- android/src/main/java/com/lodev09/exify/ExifyTags.kt | 3 ++- .../src/main/java/com/lodev09/exify/ExifyUtils.kt | 12 +++++++++++- ios/Exify.mm | 8 +++++++- src/types.ts | 3 ++- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/lodev09/exify/ExifyModule.kt b/android/src/main/java/com/lodev09/exify/ExifyModule.kt index 391738d..3e46303 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyModule.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyModule.kt @@ -182,7 +182,9 @@ class ExifyModule( } ReadableType.Array -> { - exif.setAttribute(tag, tags.getArray(tag).toString()) + val arr = tags.getArray(tag)!! + val values = (0 until arr.size()).joinToString(", ") { arr.getInt(it).toString() } + exif.setAttribute(tag, values) } else -> { diff --git a/android/src/main/java/com/lodev09/exify/ExifyTags.kt b/android/src/main/java/com/lodev09/exify/ExifyTags.kt index 3ee5e8f..9355fe3 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyTags.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyTags.kt @@ -78,13 +78,14 @@ val EXIFY_TAGS = arrayOf("double", ExifInterface.TAG_FOCAL_LENGTH), arrayOf("string", ExifInterface.TAG_LENS_MAKE), arrayOf("string", ExifInterface.TAG_LENS_MODEL), + arrayOf("string", ExifInterface.TAG_BODY_SERIAL_NUMBER), arrayOf("array", ExifInterface.TAG_LENS_SPECIFICATION), arrayOf("int", ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM), arrayOf("int", ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT), arrayOf("double", ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION), arrayOf("double", ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION), arrayOf("int", ExifInterface.TAG_GAIN_CONTROL), - arrayOf("string", ExifInterface.TAG_ISO_SPEED_RATINGS), + arrayOf("int_array", ExifInterface.TAG_ISO_SPEED_RATINGS), arrayOf("string", ExifInterface.TAG_IMAGE_UNIQUE_ID), arrayOf("int", ExifInterface.TAG_LIGHT_SOURCE), arrayOf("string", ExifInterface.TAG_MAKER_NOTE), diff --git a/android/src/main/java/com/lodev09/exify/ExifyUtils.kt b/android/src/main/java/com/lodev09/exify/ExifyUtils.kt index fabf8a1..59f63e7 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyUtils.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyUtils.kt @@ -18,13 +18,23 @@ object ExifyUtils { } "int" -> { - tags.putInt(tag, exif.getAttributeInt(tag, 0)) + val intVal = exif.getAttributeInt(tag, 0) + if (tag == ExifInterface.TAG_ORIENTATION && intVal == 0) continue + tags.putInt(tag, intVal) } "double" -> { tags.putDouble(tag, exif.getAttributeDouble(tag, 0.0)) } + "int_array" -> { + val array = Arguments.createArray() + attribute.split(", ").forEach { part -> + part.trim().toIntOrNull()?.let { array.pushInt(it) } + } + if (array.size() > 0) tags.putArray(tag, array) + } + "array" -> { val array = Arguments.createArray() exif.getAttributeRange(tag)?.forEach { value -> diff --git a/ios/Exify.mm b/ios/Exify.mm index 178588d..eb58c40 100644 --- a/ios/Exify.mm +++ b/ios/Exify.mm @@ -42,7 +42,13 @@ static void addTagEntries(CFStringRef dictionary, NSDictionary *metadata, id value = metadata[key]; if (![value isKindOfClass:[NSDictionary class]] && ![key isEqualToString:compressionKey]) { - tags[key] = value; + NSString *mappedKey = key; + if ([key isEqualToString:@"PixelWidth"]) { + mappedKey = @"PixelXDimension"; + } else if ([key isEqualToString:@"PixelHeight"]) { + mappedKey = @"PixelYDimension"; + } + tags[mappedKey] = value; } } diff --git a/src/types.ts b/src/types.ts index 7e415b5..eb3f6bd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -63,13 +63,14 @@ export interface ExifTags { LightSource?: number; UserComment?: string; GainControl?: number; - ISOSpeedRatings?: string; + ISOSpeedRatings?: number[]; FocalPlaneResolutionUnit?: number; FocalPlaneXResolution?: number; YCbCrCoefficients?: number; FocalLengthIn35mmFilm?: number; LensMake?: string; LensModel?: string; + BodySerialNumber?: string; LensSpecification?: number[]; ISO?: number; FlashpixVersion?: number[]; From 03765710bcaa2227be6efc0a4c1ae640604116b2 Mon Sep 17 00:00:00 2001 From: lodev09 Date: Mon, 23 Feb 2026 01:25:34 +0800 Subject: [PATCH 4/4] fix: extend IFD0 fallback to non-string tags, emit EXIF aliases on iOS - fallback reads SHORT/LONG/RATIONAL IFD0 tags (Orientation, XResolution, YResolution, ResolutionUnit) - detect missing numeric tags with default 0 for fallback - iOS emits standard EXIF aliases alongside CGImageSource keys --- .../java/com/lodev09/exify/ExifFallback.kt | 86 +++++++++++++------ .../java/com/lodev09/exify/ExifyModule.kt | 20 ++++- .../main/java/com/lodev09/exify/ExifyTags.kt | 8 +- ios/Exify.mm | 13 ++- 4 files changed, 93 insertions(+), 34 deletions(-) diff --git a/android/src/main/java/com/lodev09/exify/ExifFallback.kt b/android/src/main/java/com/lodev09/exify/ExifFallback.kt index 7134e47..6a0ffe8 100644 --- a/android/src/main/java/com/lodev09/exify/ExifFallback.kt +++ b/android/src/main/java/com/lodev09/exify/ExifFallback.kt @@ -5,40 +5,47 @@ import java.nio.ByteBuffer import java.nio.ByteOrder /** - * IFD0 string tags that ExifInterface may miss when an image editor + * IFD0 tags that ExifInterface may miss when an image editor * (e.g. ON1 Photo RAW) places them inside the ExifIFD instead of IFD0. * * Maps EXIF tag number → ExifInterface tag name. */ private val FALLBACK_TAGS = mapOf( + 0x010E to "ImageDescription", 0x010F to "Make", 0x0110 to "Model", + 0x0112 to "Orientation", + 0x011A to "XResolution", + 0x011B to "YResolution", + 0x0128 to "ResolutionUnit", + 0x0131 to "Software", 0x013B to "Artist", 0x8298 to "Copyright", - 0x010E to "ImageDescription", - 0x0131 to "Software", ) +private const val IFD_FORMAT_SHORT = 3 +private const val IFD_FORMAT_LONG = 4 +private const val IFD_FORMAT_RATIONAL = 5 private const val IFD_FORMAT_STRING = 2 private const val IFD_FORMAT_UNDEFINED = 7 private const val EXIF_IFD_POINTER_TAG = 0x8769 /** - * Scans raw JPEG bytes for IFD0 string tags that may have been placed in + * Scans raw JPEG bytes for IFD0 tags that may have been placed in * the ExifIFD. Returns a map of tag name → value for any tags found. */ fun readFallbackTags( inputStream: InputStream, missingTags: Set, -): Map { +): Map { if (missingTags.isEmpty()) return emptyMap() val neededTagNumbers = FALLBACK_TAGS.filterValues { it in missingTags }.keys if (neededTagNumbers.isEmpty()) return emptyMap() val bytes = readExifSegment(inputStream) ?: return emptyMap() - val result = mutableMapOf() + val result = mutableMapOf() val app1 = findApp1Exif(bytes) ?: return emptyMap() val tiffOffset = app1.tiffOffset @@ -102,7 +109,10 @@ private fun readExifSegment(inputStream: InputStream): ByteArray? { return out.toByteArray() } -private data class App1Info(val tiffOffset: Int, val byteOrder: ByteOrder) +private data class App1Info( + val tiffOffset: Int, + val byteOrder: ByteOrder, +) private fun findApp1Exif(bytes: ByteArray): App1Info? { if (bytes.size < 4 || bytes[0] != 0xFF.toByte() || bytes[1] != 0xD8.toByte()) return null @@ -166,7 +176,7 @@ private fun scanIfd( tiffOffset: Int, ifdOffset: Int, neededTagNumbers: Set, - result: MutableMap, + result: MutableMap, ) { val absOffset = tiffOffset + ifdOffset if (absOffset + 2 > buf.limit()) return @@ -181,29 +191,55 @@ private fun scanIfd( val format = buf.getShort(entryOffset + 2).toInt() and 0xFFFF val componentCount = buf.getInt(entryOffset + 4) + if (componentCount <= 0) continue + + val tagName = FALLBACK_TAGS[tagNumber] ?: continue + + when (format) { + IFD_FORMAT_SHORT -> { + if (componentCount != 1) continue + val value = buf.getShort(entryOffset + 8).toInt() and 0xFFFF + if (value == 0) continue + result[tagName] = value + } - if (format != IFD_FORMAT_STRING && format != IFD_FORMAT_UNDEFINED) continue - if (componentCount <= 0 || componentCount > 1024) continue + IFD_FORMAT_LONG -> { + if (componentCount != 1) continue + val value = buf.getInt(entryOffset + 8).toLong() and 0xFFFFFFFFL + if (value == 0L) continue + result[tagName] = value.toInt() + } - val dataOffset = - if (componentCount <= 4) { - entryOffset + 8 - } else { - tiffOffset + buf.getInt(entryOffset + 8) + IFD_FORMAT_RATIONAL -> { + if (componentCount != 1) continue + val dataOffset = tiffOffset + buf.getInt(entryOffset + 8) + if (dataOffset < 0 || dataOffset + 8 > buf.limit()) continue + val numerator = buf.getInt(dataOffset).toLong() and 0xFFFFFFFFL + val denominator = buf.getInt(dataOffset + 4).toLong() and 0xFFFFFFFFL + if (denominator == 0L) continue + result[tagName] = numerator.toDouble() / denominator.toDouble() } - if (dataOffset < 0 || dataOffset + componentCount > buf.limit()) continue + IFD_FORMAT_STRING, IFD_FORMAT_UNDEFINED -> { + if (componentCount > 1024) continue + val dataOffset = + if (componentCount <= 4) { + entryOffset + 8 + } else { + tiffOffset + buf.getInt(entryOffset + 8) + } + if (dataOffset < 0 || dataOffset + componentCount > buf.limit()) continue - val strBytes = ByteArray(componentCount) - buf.position(dataOffset) - buf.get(strBytes) + val strBytes = ByteArray(componentCount) + buf.position(dataOffset) + buf.get(strBytes) - // Trim trailing null bytes - var len = strBytes.size - while (len > 0 && strBytes[len - 1] == 0.toByte()) len-- - if (len == 0) continue + var len = strBytes.size + while (len > 0 && strBytes[len - 1] == 0.toByte()) len-- + if (len == 0) continue - val tagName = FALLBACK_TAGS[tagNumber] ?: continue - result[tagName] = String(strBytes, 0, len, Charsets.UTF_8).trim() + result[tagName] = String(strBytes, 0, len, Charsets.UTF_8).trim() + } + } } } diff --git a/android/src/main/java/com/lodev09/exify/ExifyModule.kt b/android/src/main/java/com/lodev09/exify/ExifyModule.kt index 3e46303..1327411 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyModule.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyModule.kt @@ -104,7 +104,12 @@ class ExifyModule( // ExifInterface ignores IFD0 tags placed in ExifIFD (non-standard but common // with some image editors). Fall back to raw EXIF parsing for missing tags. val missingTags = - IFD0_STRING_TAGS.filter { !tags.hasKey(it) }.toSet() + IFD0_FALLBACK_TAGS + .filter { + if (!tags.hasKey(it)) return@filter true + val type = tags.getType(it) + (type == ReadableType.Number && tags.getDouble(it) == 0.0) + }.toSet() if (missingTags.isNotEmpty()) { val fallback = try { @@ -112,7 +117,13 @@ class ExifyModule( } catch (_: Exception) { null } - fallback?.forEach { (tag, value) -> tags.putString(tag, value) } + fallback?.forEach { (tag, value) -> + when (value) { + is String -> tags.putString(tag, value) + is Int -> tags.putInt(tag, value) + is Double -> tags.putDouble(tag, value) + } + } } promise.resolve(tags) @@ -173,7 +184,10 @@ class ExifyModule( exif.setAttribute(tag, value.toBigDecimal().toPlainString()) } } - else -> exif.setAttribute(tag, tags.getDouble(tag).toInt().toString()) + + else -> { + exif.setAttribute(tag, tags.getDouble(tag).toInt().toString()) + } } } diff --git a/android/src/main/java/com/lodev09/exify/ExifyTags.kt b/android/src/main/java/com/lodev09/exify/ExifyTags.kt index 9355fe3..3445b03 100644 --- a/android/src/main/java/com/lodev09/exify/ExifyTags.kt +++ b/android/src/main/java/com/lodev09/exify/ExifyTags.kt @@ -3,10 +3,10 @@ package com.lodev09.exify import androidx.exifinterface.media.ExifInterface /** - * IFD0 string tags that ExifInterface may fail to read when they are + * IFD0 tags that ExifInterface may fail to read when they are * incorrectly placed inside the ExifIFD by some image editors. */ -val IFD0_STRING_TAGS = +val IFD0_FALLBACK_TAGS = setOf( ExifInterface.TAG_MAKE, ExifInterface.TAG_MODEL, @@ -14,6 +14,10 @@ val IFD0_STRING_TAGS = ExifInterface.TAG_COPYRIGHT, ExifInterface.TAG_IMAGE_DESCRIPTION, ExifInterface.TAG_SOFTWARE, + ExifInterface.TAG_ORIENTATION, + ExifInterface.TAG_X_RESOLUTION, + ExifInterface.TAG_Y_RESOLUTION, + ExifInterface.TAG_RESOLUTION_UNIT, ) /** diff --git a/ios/Exify.mm b/ios/Exify.mm index eb58c40..6900cf3 100644 --- a/ios/Exify.mm +++ b/ios/Exify.mm @@ -42,13 +42,18 @@ static void addTagEntries(CFStringRef dictionary, NSDictionary *metadata, id value = metadata[key]; if (![value isKindOfClass:[NSDictionary class]] && ![key isEqualToString:compressionKey]) { - NSString *mappedKey = key; + tags[key] = value; + + // Also emit standard EXIF names for CGImageSource-specific keys if ([key isEqualToString:@"PixelWidth"]) { - mappedKey = @"PixelXDimension"; + tags[@"PixelXDimension"] = value; } else if ([key isEqualToString:@"PixelHeight"]) { - mappedKey = @"PixelYDimension"; + tags[@"PixelYDimension"] = value; + } else if ([key isEqualToString:@"DPIWidth"]) { + tags[@"XResolution"] = value; + } else if ([key isEqualToString:@"DPIHeight"]) { + tags[@"YResolution"] = value; } - tags[mappedKey] = value; } }