From 85957348b2baad6eea5839ae550b065d389b32c5 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 30 Mar 2022 13:05:03 +0900 Subject: [PATCH] info: parse geotiff keys --- .../channel/calls/MetadataFetchHandler.kt | 217 +++++++++++------- .../thibault/aves/metadata/ExifTags.kt | 4 +- .../thibault/aves/metadata/GeoTiffTags.kt | 60 ++++- .../thibault/aves/metadata/Metadata.kt | 2 + .../aves/metadata/MetadataExtractorHelper.kt | 65 +++++- 5 files changed, 254 insertions(+), 94 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index 2d4b722c2..13220cb60 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -40,12 +40,16 @@ import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeRational import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMillis import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDescription import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeInt +import deckers.thibault.aves.metadata.Metadata.DIR_DNG +import deckers.thibault.aves.metadata.Metadata.DIR_EXIF_GEOTIFF import deckers.thibault.aves.metadata.Metadata.DIR_PNG_TEXTUAL_DATA import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode import deckers.thibault.aves.metadata.Metadata.isFlippedForExifCode import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_ITXT_DIR_NAME import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_LAST_MODIFICATION_TIME_FORMAT import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_TIME_DIR_NAME +import deckers.thibault.aves.metadata.MetadataExtractorHelper.containsGeoTiffTags +import deckers.thibault.aves.metadata.MetadataExtractorHelper.extractGeoKeys import deckers.thibault.aves.metadata.MetadataExtractorHelper.extractPngProfile import deckers.thibault.aves.metadata.MetadataExtractorHelper.getDateDigitizedMillis import deckers.thibault.aves.metadata.MetadataExtractorHelper.getDateModifiedMillis @@ -55,7 +59,6 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString -import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff import deckers.thibault.aves.metadata.MetadataExtractorHelper.isPngTextDir import deckers.thibault.aves.metadata.XMP.getSafeDateMillis import deckers.thibault.aves.metadata.XMP.getSafeInt @@ -83,6 +86,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import java.nio.charset.Charset import java.nio.charset.StandardCharsets +import java.text.DecimalFormat import java.text.ParseException import kotlin.math.roundToInt import kotlin.math.roundToLong @@ -95,6 +99,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { "getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) } "getCatalogMetadata" -> ioScope.launch { safe(call, result, ::getCatalogMetadata) } "getOverlayMetadata" -> ioScope.launch { safe(call, result, ::getOverlayMetadata) } + "getGeoTiffInfo" -> ioScope.launch { safe(call, result, ::getGeoTiffInfo) } "getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) } "getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) } "getIptc" -> ioScope.launch { safe(call, result, ::getIptc) } @@ -168,76 +173,95 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { // tags val tags = dir.tags - if (dir is ExifDirectoryBase) { - when { - dir.isGeoTiff() -> { - // split GeoTIFF tags in their own directory - val geoTiffDirMap = metadataMap["GeoTIFF"] ?: HashMap() - metadataMap["GeoTIFF"] = geoTiffDirMap - val byGeoTiff = tags.groupBy { ExifTags.isGeoTiffTag(it.tagType) } - byGeoTiff[true]?.map { exifTagMapper(it) }?.let { geoTiffDirMap.putAll(it) } - byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) } - } - mimeType == MimeTypes.DNG -> { - // split DNG tags in their own directory - val dngDirMap = metadataMap["DNG"] ?: HashMap() - metadataMap["DNG"] = dngDirMap - val byDng = tags.groupBy { ExifTags.isDngTag(it.tagType) } - byDng[true]?.map { exifTagMapper(it) }?.let { dngDirMap.putAll(it) } - byDng[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) } - } - else -> dirMap.putAll(tags.map { exifTagMapper(it) }) - } - } else if (dir.isPngTextDir()) { - metadataMap.remove(thisDirName) - dirMap = metadataMap[DIR_PNG_TEXTUAL_DATA] ?: HashMap() - metadataMap[DIR_PNG_TEXTUAL_DATA] = dirMap - - for (tag in tags) { - val tagType = tag.tagType - if (tagType == PngDirectory.TAG_TEXTUAL_DATA) { - val pairs = dir.getObject(tagType) as List<*> - val textPairs = pairs.map { pair -> - val kv = pair as KeyValuePair - val key = kv.key - // `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1 - val charset = if (baseDirName == PNG_ITXT_DIR_NAME) { - @SuppressLint("ObsoleteSdkInt") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - StandardCharsets.UTF_8 - } else { - Charset.forName("UTF-8") - } - } else { - kv.value.charset - } - val valueString = String(kv.value.bytes, charset) - val dirs = extractPngProfile(key, valueString) - if (dirs?.any() == true) { - dirs.forEach { profileDir -> - val profileDirName = "${dir.name}/${profileDir.name}" - val profileDirMap = metadataMap[profileDirName] ?: HashMap() - metadataMap[profileDirName] = profileDirMap - val profileTags = profileDir.tags - if (profileDir is ExifDirectoryBase) { - profileDirMap.putAll(profileTags.map { exifTagMapper(it) }) - } else { - profileDirMap.putAll(profileTags.map { Pair(it.tagName, it.description) }) + when { + dir is ExifDirectoryBase -> { + when { + dir.containsGeoTiffTags() -> { + // split GeoTIFF tags in their own directory + val geoTiffDirMap = metadataMap[DIR_EXIF_GEOTIFF] ?: HashMap() + metadataMap[DIR_EXIF_GEOTIFF] = geoTiffDirMap + val byGeoTiff = tags.groupBy { ExifTags.isGeoTiffTag(it.tagType) } + byGeoTiff[true]?.flatMap { tag -> + when (tag.tagType) { + ExifGeoTiffTags.TAG_GEO_KEY_DIRECTORY -> { + val geoTiffTags = (dir as ExifIFD0Directory).extractGeoKeys(dir.getIntArray(tag.tagType)) + geoTiffTags.map { geoTag -> + val name = GeoTiffKeys.getTagName(geoTag.key) ?: "0x${geoTag.key.toString(16)}" + val value = geoTag.value + val description = if (value is DoubleArray) value.joinToString(" ") { doubleFormat.format(it) } else "$value" + Pair(name, description) + } } + // skip `Geo double/ascii params`, as their content is split and presented through various GeoTIFF keys + ExifGeoTiffTags.TAG_GEO_DOUBLE_PARAMS, + ExifGeoTiffTags.TAG_GEO_ASCII_PARAMS -> ArrayList() + else -> listOf(exifTagMapper(tag)) } - null - } else { - Pair(key, valueString) - } + }?.let { geoTiffDirMap.putAll(it) } + byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) } } - dirMap.putAll(textPairs.filterNotNull()) - } else { - dirMap[tag.tagName] = tag.description + mimeType == MimeTypes.DNG -> { + // split DNG tags in their own directory + val dngDirMap = metadataMap[DIR_DNG] ?: HashMap() + metadataMap[DIR_DNG] = dngDirMap + val byDng = tags.groupBy { ExifTags.isDngTag(it.tagType) } + byDng[true]?.map { exifTagMapper(it) }?.let { dngDirMap.putAll(it) } + byDng[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) } + } + else -> dirMap.putAll(tags.map { exifTagMapper(it) }) } } - } else { - dirMap.putAll(tags.map { Pair(it.tagName, it.description) }) + dir.isPngTextDir() -> { + metadataMap.remove(thisDirName) + dirMap = metadataMap[DIR_PNG_TEXTUAL_DATA] ?: HashMap() + metadataMap[DIR_PNG_TEXTUAL_DATA] = dirMap + + for (tag in tags) { + val tagType = tag.tagType + if (tagType == PngDirectory.TAG_TEXTUAL_DATA) { + val pairs = dir.getObject(tagType) as List<*> + val textPairs = pairs.map { pair -> + val kv = pair as KeyValuePair + val key = kv.key + // `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1 + val charset = if (baseDirName == PNG_ITXT_DIR_NAME) { + @SuppressLint("ObsoleteSdkInt") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + StandardCharsets.UTF_8 + } else { + Charset.forName("UTF-8") + } + } else { + kv.value.charset + } + val valueString = String(kv.value.bytes, charset) + val dirs = extractPngProfile(key, valueString) + if (dirs?.any() == true) { + dirs.forEach { profileDir -> + val profileDirName = "${dir.name}/${profileDir.name}" + val profileDirMap = metadataMap[profileDirName] ?: HashMap() + metadataMap[profileDirName] = profileDirMap + val profileTags = profileDir.tags + if (profileDir is ExifDirectoryBase) { + profileDirMap.putAll(profileTags.map { exifTagMapper(it) }) + } else { + profileDirMap.putAll(profileTags.map { Pair(it.tagName, it.description) }) + } + } + null + } else { + Pair(key, valueString) + } + } + dirMap.putAll(textPairs.filterNotNull()) + } else { + dirMap[tag.tagName] = tag.description + } + } + } + else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) }) } + if (dir is XmpDirectory) { try { for (prop in dir.xmpMeta) { @@ -300,9 +324,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } catch (e: Exception) { - Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { - Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -557,7 +581,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { MimeTypes.TIFF -> { // identification of GeoTIFF for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { - if (dir.isGeoTiff()) flags = flags or MASK_IS_GEOTIFF + if (dir.containsGeoTiffTags()) flags = flags or MASK_IS_GEOTIFF } } } @@ -570,9 +594,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } catch (e: Exception) { - Log.w(LOG_TAG, "failed to get catalog metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { - Log.w(LOG_TAG, "failed to get catalog metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -696,9 +720,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } catch (e: Exception) { - Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { - Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -722,6 +746,43 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { result.success(metadataMap) } + private fun getGeoTiffInfo(call: MethodCall, result: MethodChannel.Result) { + val mimeType = call.argument("mimeType") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val sizeBytes = call.argument("sizeBytes")?.toLong() + if (mimeType == null || uri == null) { + result.error("getGeoTiffInfo-args", "failed because of missing arguments", null) + return + } + + if (canReadWithMetadataExtractor(mimeType)) { + try { + Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> + val metadata = ImageMetadataReader.readMetadata(input) + val fields = HashMap() + for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { + if (dir.containsGeoTiffTags()) { + fields.putAll(dir.tags.map { it.tagType }.filter { ExifTags.isGeoTiffTag(it) }.map { + val value = when (it) { + ExifGeoTiffTags.TAG_GEO_ASCII_PARAMS -> dir.getString(it) + else -> dir.getObject(it) + } + Pair(it, value) + }) + } + } + result.success(fields) + return + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } catch (e: NoClassDefFoundError) { + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + } + } + result.error("getGeoTiffInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null) + } + private fun getMultiPageInfo(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } @@ -774,12 +835,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { return } } catch (e: Exception) { - Log.w(LOG_TAG, "failed to read XMP", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { - Log.w(LOG_TAG, "failed to read XMP", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } - result.error("getPanoramaInfo-empty", "failed to read XMP for mimeType=$mimeType uri=$uri", null) + result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null) } private fun getIptc(call: MethodCall, result: MethodChannel.Result) { @@ -961,9 +1022,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { } } } catch (e: Exception) { - Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } catch (e: NoClassDefFoundError) { - Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) + Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e) } } @@ -974,6 +1035,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { private val LOG_TAG = LogUtils.createTag() const val CHANNEL = "deckers.thibault/aves/metadata_fetch" + private val doubleFormat = DecimalFormat("0.###") + private val allMetadataRedundantDirNames = setOf( "MP4", "MP4 Metadata", diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifTags.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifTags.kt index ddcf16ebd..4002cadc8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifTags.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifTags.kt @@ -44,12 +44,12 @@ object ExifTags { TAG_GDAL_NO_DATA to "GDAL No Data", ).apply { putAll(DngTags.tagNameMap) - putAll(GeoTiffTags.tagNameMap) + putAll(ExifGeoTiffTags.tagNameMap) } fun isDngTag(tag: Int) = DngTags.tags.contains(tag) - fun isGeoTiffTag(tag: Int) = GeoTiffTags.tags.contains(tag) + fun isGeoTiffTag(tag: Int) = ExifGeoTiffTags.tags.contains(tag) fun getTagName(tag: Int): String? { return tagNameMap[tag] diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GeoTiffTags.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GeoTiffTags.kt index 208165fc3..d80f09c10 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GeoTiffTags.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GeoTiffTags.kt @@ -1,17 +1,61 @@ package deckers.thibault.aves.metadata -object GeoTiffTags { +object GeoTiffKeys { + // not a standard tag + const val GEOTIFF_VERSION = 0 + + private const val MODEL_TYPE = 0x0400 + private const val RASTER_TYPE = 0x0401 + private const val CITATION = 0x0402 + private const val GEOG_TYPE = 0x0800 + private const val GEOG_CITATION = 0x0801 + private const val GEOG_ANGULAR_UNITS = 0x0806 + private const val PROJ_CS_TYPE = 0x0c00 + private const val PROJ_CS_CITATION = 0x0c01 + private const val PROJECTION = 0x0c02 + private const val PROJ_COORD_TRANS = 0x0c03 + private const val PROJ_LINEAR_UNITS = 0x0c04 + private const val PROJ_STD_PARALLEL_1 = 0x0c06 + private const val PROJ_NAT_ORIGIN_LONG = 0x0c08 + private const val PROJ_FALSE_EASTING = 0x0c0a + private const val PROJ_FALSE_NORTHING = 0x0c0b + + private val tagNameMap = hashMapOf( + GEOTIFF_VERSION to "GeoTIFF Version", + MODEL_TYPE to "Model Type", + RASTER_TYPE to "Raster Type", + CITATION to "Citation", + GEOG_TYPE to "Geographic Type", + GEOG_CITATION to "Geographic Citation", + GEOG_ANGULAR_UNITS to "Geographic Angular Units", + PROJ_CS_TYPE to "Projected Coordinate System Type", + PROJ_CS_CITATION to "Projected Coordinate System Citation", + PROJECTION to "Projection", + PROJ_COORD_TRANS to "Projected Coordinate Transform", + PROJ_LINEAR_UNITS to "Projection Linear Units", + PROJ_STD_PARALLEL_1 to "Projection Standard Parallel 1", + PROJ_NAT_ORIGIN_LONG to "Projection Natural Origin Longitude", + PROJ_FALSE_EASTING to "Projection False Easting", + PROJ_FALSE_NORTHING to "Projection False Northing", + ) + + fun getTagName(tag: Int): String? { + return tagNameMap[tag] + } +} + +object ExifGeoTiffTags { // ModelPixelScaleTag (optional) // Tag = 33550 (830E.H) // Type = DOUBLE // Count = 3 const val TAG_MODEL_PIXEL_SCALE = 0x830e - // ModelTiepointTag (conditional) + // ModelTiePointTag (conditional) // Tag = 33922 (8482.H) // Type = DOUBLE - // Count = 6*K, K = number of tiepoints - const val TAG_MODEL_TIEPOINT = 0x8482 + // Count = 6*K, K = number of tie points + const val TAG_MODEL_TIE_POINT = 0x8482 // ModelTransformationTag (conditional) // Tag = 34264 (85D8.H) @@ -29,22 +73,22 @@ object GeoTiffTags { // Tag = 34736 (87BO.H) // Type = DOUBLE // Count = variable - private const val TAG_GEO_DOUBLE_PARAMS = 0x87b0 + const val TAG_GEO_DOUBLE_PARAMS = 0x87b0 // GeoAsciiParamsTag (optional) // Tag = 34737 (87B1.H) // Type = ASCII // Count = variable - private const val TAG_GEO_ASCII_PARAMS = 0x87b1 + const val TAG_GEO_ASCII_PARAMS = 0x87b1 val tagNameMap = hashMapOf( TAG_GEO_ASCII_PARAMS to "Geo Ascii Params", TAG_GEO_DOUBLE_PARAMS to "Geo Double Params", TAG_GEO_KEY_DIRECTORY to "Geo Key Directory", TAG_MODEL_PIXEL_SCALE to "Model Pixel Scale", - TAG_MODEL_TIEPOINT to "Model Tiepoint", + TAG_MODEL_TIE_POINT to "Model Tie Points", TAG_MODEL_TRANSFORMATION to "Model Transformation", ) val tags = tagNameMap.keys -} \ No newline at end of file +} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt index 805b7e3df..93c5795f6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt @@ -29,6 +29,8 @@ object Metadata { const val DIR_XMP = "XMP" // from metadata-extractor const val DIR_MEDIA = "Media" // custom const val DIR_COVER_ART = "Cover" // custom + const val DIR_DNG = "DNG" // custom + const val DIR_EXIF_GEOTIFF = "GeoTIFF" // custom const val DIR_PNG_TEXTUAL_DATA = "PNG Textual Data" // custom // types of metadata diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt index 8a244d3d0..95d9046d2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MetadataExtractorHelper.kt @@ -5,6 +5,7 @@ import com.drew.lang.ByteArrayReader import com.drew.lang.Rational import com.drew.lang.SequentialByteArrayReader import com.drew.metadata.Directory +import com.drew.metadata.StringValue import com.drew.metadata.exif.ExifDirectoryBase import com.drew.metadata.exif.ExifIFD0Directory import com.drew.metadata.exif.ExifReader @@ -93,19 +94,69 @@ object MetadataExtractorHelper { - If the ModelTransformationTag is included in an IFD, then a ModelPixelScaleTag SHALL NOT be included - If the ModelPixelScaleTag is included in an IFD, then a ModelTiepointTag SHALL also be included. */ - fun ExifDirectoryBase.isGeoTiff(): Boolean { - if (!this.containsTag(GeoTiffTags.TAG_GEO_KEY_DIRECTORY)) return false + fun ExifDirectoryBase.containsGeoTiffTags(): Boolean { + if (!this.containsTag(ExifGeoTiffTags.TAG_GEO_KEY_DIRECTORY)) return false - val modelTiepoint = this.containsTag(GeoTiffTags.TAG_MODEL_TIEPOINT) - val modelTransformation = this.containsTag(GeoTiffTags.TAG_MODEL_TRANSFORMATION) - if (!modelTiepoint && !modelTransformation) return false + val modelTiePoints = this.containsTag(ExifGeoTiffTags.TAG_MODEL_TIE_POINT) + val modelTransformation = this.containsTag(ExifGeoTiffTags.TAG_MODEL_TRANSFORMATION) + if (!modelTiePoints && !modelTransformation) return false - val modelPixelScale = this.containsTag(GeoTiffTags.TAG_MODEL_PIXEL_SCALE) - if ((modelTransformation && modelPixelScale) || (modelPixelScale && !modelTiepoint)) return false + val modelPixelScale = this.containsTag(ExifGeoTiffTags.TAG_MODEL_PIXEL_SCALE) + if ((modelTransformation && modelPixelScale) || (modelPixelScale && !modelTiePoints)) return false return true } + // TODO TLAD use `GeoTiffDirectory` from the Java version of `metadata-extractor` when available + // adapted from https://github.com/drewnoakes/metadata-extractor-dotnet/blob/master/MetadataExtractor/Formats/Exif/ExifTiffHandler.cs + fun ExifIFD0Directory.extractGeoKeys(geoKeys: IntArray): HashMap { + val fields = HashMap() + if (geoKeys.size < 4) return fields + + var i = 0 + val directoryVersion = geoKeys[i++] + val revision = geoKeys[i++] + val minorRevision = geoKeys[i++] + val numberOfKeys = geoKeys[i++] + + fields[GeoTiffKeys.GEOTIFF_VERSION] = "$directoryVersion.$revision.$minorRevision" + + for (j in 0 until numberOfKeys) { + val keyId = geoKeys[i++] + val tiffTagLocation = geoKeys[i++] + val valueCount = geoKeys[i++] + val valueOffset = geoKeys[i++] + + try { + if (tiffTagLocation == 0) { + fields[keyId] = valueOffset + } else { + val sourceValue = getObject(tiffTagLocation) + if (sourceValue is StringValue) { + if (valueOffset + valueCount <= sourceValue.bytes.size) { + fields[keyId] = String(sourceValue.bytes, valueOffset, valueCount).trimEnd('|') + } else { + Log.w(LOG_TAG, "GeoTIFF key $keyId with offset $valueOffset and count $valueCount extends beyond length of source value (${sourceValue.bytes.size})") + } + } else if (sourceValue.javaClass.isArray) { + val sourceArray = sourceValue as DoubleArray + if (valueOffset + valueCount < sourceArray.size) { + fields[keyId] = sourceArray.copyOfRange(valueOffset, valueOffset + valueCount) + } else { + Log.w(LOG_TAG, "GeoTIFF key $keyId with offset $valueOffset and count $valueCount extends beyond length of source value (${sourceArray.size})") + } + } else { + Log.w(LOG_TAG, "GeoTIFF key $keyId references tag $tiffTagLocation which has unsupported type of ${sourceValue?.javaClass}") + } + } + } catch (e: Exception) { + Log.e(LOG_TAG, "failed to extract GeoTiff fields from keys", e) + } + } + + return fields + } + // PNG fun Directory.isPngTextDir(): Boolean = this is PngDirectory && setOf(PNG_ITXT_DIR_NAME, PNG_TEXT_DIR_NAME, PNG_ZTXT_DIR_NAME).contains(this.name)