Skip to content

Commit 0376571

Browse files
committed
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
1 parent a7e3e14 commit 0376571

4 files changed

Lines changed: 93 additions & 34 deletions

File tree

android/src/main/java/com/lodev09/exify/ExifFallback.kt

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,47 @@ import java.nio.ByteBuffer
55
import java.nio.ByteOrder
66

77
/**
8-
* IFD0 string tags that ExifInterface may miss when an image editor
8+
* IFD0 tags that ExifInterface may miss when an image editor
99
* (e.g. ON1 Photo RAW) places them inside the ExifIFD instead of IFD0.
1010
*
1111
* Maps EXIF tag number → ExifInterface tag name.
1212
*/
1313
private val FALLBACK_TAGS =
1414
mapOf(
15+
0x010E to "ImageDescription",
1516
0x010F to "Make",
1617
0x0110 to "Model",
18+
0x0112 to "Orientation",
19+
0x011A to "XResolution",
20+
0x011B to "YResolution",
21+
0x0128 to "ResolutionUnit",
22+
0x0131 to "Software",
1723
0x013B to "Artist",
1824
0x8298 to "Copyright",
19-
0x010E to "ImageDescription",
20-
0x0131 to "Software",
2125
)
2226

27+
private const val IFD_FORMAT_SHORT = 3
28+
private const val IFD_FORMAT_LONG = 4
29+
private const val IFD_FORMAT_RATIONAL = 5
2330
private const val IFD_FORMAT_STRING = 2
2431
private const val IFD_FORMAT_UNDEFINED = 7
2532
private const val EXIF_IFD_POINTER_TAG = 0x8769
2633

2734
/**
28-
* Scans raw JPEG bytes for IFD0 string tags that may have been placed in
35+
* Scans raw JPEG bytes for IFD0 tags that may have been placed in
2936
* the ExifIFD. Returns a map of tag name → value for any tags found.
3037
*/
3138
fun readFallbackTags(
3239
inputStream: InputStream,
3340
missingTags: Set<String>,
34-
): Map<String, String> {
41+
): Map<String, Any> {
3542
if (missingTags.isEmpty()) return emptyMap()
3643

3744
val neededTagNumbers = FALLBACK_TAGS.filterValues { it in missingTags }.keys
3845
if (neededTagNumbers.isEmpty()) return emptyMap()
3946

4047
val bytes = readExifSegment(inputStream) ?: return emptyMap()
41-
val result = mutableMapOf<String, String>()
48+
val result = mutableMapOf<String, Any>()
4249

4350
val app1 = findApp1Exif(bytes) ?: return emptyMap()
4451
val tiffOffset = app1.tiffOffset
@@ -102,7 +109,10 @@ private fun readExifSegment(inputStream: InputStream): ByteArray? {
102109
return out.toByteArray()
103110
}
104111

105-
private data class App1Info(val tiffOffset: Int, val byteOrder: ByteOrder)
112+
private data class App1Info(
113+
val tiffOffset: Int,
114+
val byteOrder: ByteOrder,
115+
)
106116

107117
private fun findApp1Exif(bytes: ByteArray): App1Info? {
108118
if (bytes.size < 4 || bytes[0] != 0xFF.toByte() || bytes[1] != 0xD8.toByte()) return null
@@ -166,7 +176,7 @@ private fun scanIfd(
166176
tiffOffset: Int,
167177
ifdOffset: Int,
168178
neededTagNumbers: Set<Int>,
169-
result: MutableMap<String, String>,
179+
result: MutableMap<String, Any>,
170180
) {
171181
val absOffset = tiffOffset + ifdOffset
172182
if (absOffset + 2 > buf.limit()) return
@@ -181,29 +191,55 @@ private fun scanIfd(
181191

182192
val format = buf.getShort(entryOffset + 2).toInt() and 0xFFFF
183193
val componentCount = buf.getInt(entryOffset + 4)
194+
if (componentCount <= 0) continue
195+
196+
val tagName = FALLBACK_TAGS[tagNumber] ?: continue
197+
198+
when (format) {
199+
IFD_FORMAT_SHORT -> {
200+
if (componentCount != 1) continue
201+
val value = buf.getShort(entryOffset + 8).toInt() and 0xFFFF
202+
if (value == 0) continue
203+
result[tagName] = value
204+
}
184205

185-
if (format != IFD_FORMAT_STRING && format != IFD_FORMAT_UNDEFINED) continue
186-
if (componentCount <= 0 || componentCount > 1024) continue
206+
IFD_FORMAT_LONG -> {
207+
if (componentCount != 1) continue
208+
val value = buf.getInt(entryOffset + 8).toLong() and 0xFFFFFFFFL
209+
if (value == 0L) continue
210+
result[tagName] = value.toInt()
211+
}
187212

188-
val dataOffset =
189-
if (componentCount <= 4) {
190-
entryOffset + 8
191-
} else {
192-
tiffOffset + buf.getInt(entryOffset + 8)
213+
IFD_FORMAT_RATIONAL -> {
214+
if (componentCount != 1) continue
215+
val dataOffset = tiffOffset + buf.getInt(entryOffset + 8)
216+
if (dataOffset < 0 || dataOffset + 8 > buf.limit()) continue
217+
val numerator = buf.getInt(dataOffset).toLong() and 0xFFFFFFFFL
218+
val denominator = buf.getInt(dataOffset + 4).toLong() and 0xFFFFFFFFL
219+
if (denominator == 0L) continue
220+
result[tagName] = numerator.toDouble() / denominator.toDouble()
193221
}
194222

195-
if (dataOffset < 0 || dataOffset + componentCount > buf.limit()) continue
223+
IFD_FORMAT_STRING, IFD_FORMAT_UNDEFINED -> {
224+
if (componentCount > 1024) continue
225+
val dataOffset =
226+
if (componentCount <= 4) {
227+
entryOffset + 8
228+
} else {
229+
tiffOffset + buf.getInt(entryOffset + 8)
230+
}
231+
if (dataOffset < 0 || dataOffset + componentCount > buf.limit()) continue
196232

197-
val strBytes = ByteArray(componentCount)
198-
buf.position(dataOffset)
199-
buf.get(strBytes)
233+
val strBytes = ByteArray(componentCount)
234+
buf.position(dataOffset)
235+
buf.get(strBytes)
200236

201-
// Trim trailing null bytes
202-
var len = strBytes.size
203-
while (len > 0 && strBytes[len - 1] == 0.toByte()) len--
204-
if (len == 0) continue
237+
var len = strBytes.size
238+
while (len > 0 && strBytes[len - 1] == 0.toByte()) len--
239+
if (len == 0) continue
205240

206-
val tagName = FALLBACK_TAGS[tagNumber] ?: continue
207-
result[tagName] = String(strBytes, 0, len, Charsets.UTF_8).trim()
241+
result[tagName] = String(strBytes, 0, len, Charsets.UTF_8).trim()
242+
}
243+
}
208244
}
209245
}

android/src/main/java/com/lodev09/exify/ExifyModule.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,26 @@ class ExifyModule(
104104
// ExifInterface ignores IFD0 tags placed in ExifIFD (non-standard but common
105105
// with some image editors). Fall back to raw EXIF parsing for missing tags.
106106
val missingTags =
107-
IFD0_STRING_TAGS.filter { !tags.hasKey(it) }.toSet()
107+
IFD0_FALLBACK_TAGS
108+
.filter {
109+
if (!tags.hasKey(it)) return@filter true
110+
val type = tags.getType(it)
111+
(type == ReadableType.Number && tags.getDouble(it) == 0.0)
112+
}.toSet()
108113
if (missingTags.isNotEmpty()) {
109114
val fallback =
110115
try {
111116
openInputStream(uri, photoUri, scheme)?.use { readFallbackTags(it, missingTags) }
112117
} catch (_: Exception) {
113118
null
114119
}
115-
fallback?.forEach { (tag, value) -> tags.putString(tag, value) }
120+
fallback?.forEach { (tag, value) ->
121+
when (value) {
122+
is String -> tags.putString(tag, value)
123+
is Int -> tags.putInt(tag, value)
124+
is Double -> tags.putDouble(tag, value)
125+
}
126+
}
116127
}
117128

118129
promise.resolve(tags)
@@ -173,7 +184,10 @@ class ExifyModule(
173184
exif.setAttribute(tag, value.toBigDecimal().toPlainString())
174185
}
175186
}
176-
else -> exif.setAttribute(tag, tags.getDouble(tag).toInt().toString())
187+
188+
else -> {
189+
exif.setAttribute(tag, tags.getDouble(tag).toInt().toString())
190+
}
177191
}
178192
}
179193

android/src/main/java/com/lodev09/exify/ExifyTags.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@ package com.lodev09.exify
33
import androidx.exifinterface.media.ExifInterface
44

55
/**
6-
* IFD0 string tags that ExifInterface may fail to read when they are
6+
* IFD0 tags that ExifInterface may fail to read when they are
77
* incorrectly placed inside the ExifIFD by some image editors.
88
*/
9-
val IFD0_STRING_TAGS =
9+
val IFD0_FALLBACK_TAGS =
1010
setOf(
1111
ExifInterface.TAG_MAKE,
1212
ExifInterface.TAG_MODEL,
1313
ExifInterface.TAG_ARTIST,
1414
ExifInterface.TAG_COPYRIGHT,
1515
ExifInterface.TAG_IMAGE_DESCRIPTION,
1616
ExifInterface.TAG_SOFTWARE,
17+
ExifInterface.TAG_ORIENTATION,
18+
ExifInterface.TAG_X_RESOLUTION,
19+
ExifInterface.TAG_Y_RESOLUTION,
20+
ExifInterface.TAG_RESOLUTION_UNIT,
1721
)
1822

1923
/**

ios/Exify.mm

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,18 @@ static void addTagEntries(CFStringRef dictionary, NSDictionary *metadata,
4242
id value = metadata[key];
4343
if (![value isKindOfClass:[NSDictionary class]] &&
4444
![key isEqualToString:compressionKey]) {
45-
NSString *mappedKey = key;
45+
tags[key] = value;
46+
47+
// Also emit standard EXIF names for CGImageSource-specific keys
4648
if ([key isEqualToString:@"PixelWidth"]) {
47-
mappedKey = @"PixelXDimension";
49+
tags[@"PixelXDimension"] = value;
4850
} else if ([key isEqualToString:@"PixelHeight"]) {
49-
mappedKey = @"PixelYDimension";
51+
tags[@"PixelYDimension"] = value;
52+
} else if ([key isEqualToString:@"DPIWidth"]) {
53+
tags[@"XResolution"] = value;
54+
} else if ([key isEqualToString:@"DPIHeight"]) {
55+
tags[@"YResolution"] = value;
5056
}
51-
tags[mappedKey] = value;
5257
}
5358
}
5459

0 commit comments

Comments
 (0)