From acdb7afa6d424924d7088dffce7f4b471cdd64ac Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 31 Dec 2021 11:41:14 +0900 Subject: [PATCH] info: fixed PNG exif/iptc raw profile extraction --- .../channel/calls/MetadataFetchHandler.kt | 2 +- .../aves/metadata/MetadataExtractorHelper.kt | 63 ++++++++++++------- 2 files changed, 42 insertions(+), 23 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 aced1a04a..53a4f3d7c 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 @@ -207,7 +207,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { val dirs = extractPngProfile(key, valueString) if (dirs?.any() == true) { dirs.forEach { profileDir -> - val profileDirName = profileDir.name + val profileDirName = "${dir.name}/${profileDir.name}" val profileDirMap = metadataMap[profileDirName] ?: HashMap() metadataMap[profileDirName] = profileDirMap profileDirMap.putAll(profileDir.tags.map { Pair(it.tagName, it.description) }) 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 f0d73d1e3..1b518da6d 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 @@ -1,25 +1,34 @@ package deckers.thibault.aves.metadata +import android.util.Log +import com.drew.lang.ByteArrayReader import com.drew.lang.Rational import com.drew.lang.SequentialByteArrayReader import com.drew.metadata.Directory import com.drew.metadata.exif.ExifIFD0Directory +import com.drew.metadata.exif.ExifReader import com.drew.metadata.iptc.IptcReader import com.drew.metadata.png.PngDirectory +import deckers.thibault.aves.utils.LogUtils import java.text.SimpleDateFormat import java.util.* object MetadataExtractorHelper { + private val LOG_TAG = LogUtils.createTag() + const val PNG_ITXT_DIR_NAME = "PNG-iTXt" private const val PNG_TEXT_DIR_NAME = "PNG-tEXt" const val PNG_TIME_DIR_NAME = "PNG-tIME" private const val PNG_ZTXT_DIR_NAME = "PNG-zTXt" + private const val PNG_RAW_PROFILE_EXIF = "Raw profile type exif" + private const val PNG_RAW_PROFILE_IPTC = "Raw profile type iptc" val PNG_LAST_MODIFICATION_TIME_FORMAT = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.ROOT) // Pattern to extract profile name, length, and text data // of raw profiles (EXIF, IPTC, etc.) in PNG `zTXt` chunks // e.g. "iptc [...] 114 [...] 3842494d040400[...]" + // e.g. "exif [...] 134 [...] 4578696600004949[...]" private val PNG_RAW_PROFILE_PATTERN = Regex("^\\n(.*?)\\n\\s*(\\d+)\\n(.*)", RegexOption.DOT_MATCHES_ALL) // extensions @@ -77,22 +86,29 @@ object MetadataExtractorHelper { fun Directory.isPngTextDir(): Boolean = this is PngDirectory && setOf(PNG_ITXT_DIR_NAME, PNG_TEXT_DIR_NAME, PNG_ZTXT_DIR_NAME).contains(this.name) fun extractPngProfile(key: String, valueString: String): Iterable? { - when (key) { - "Raw profile type iptc" -> { - val match = PNG_RAW_PROFILE_PATTERN.matchEntire(valueString) - if (match != null) { - val dataString = match.groupValues[3] - val hexString = dataString.replace(Regex("[\\r\\n]"), "") - val dataBytes = hexStringToByteArray(hexString) - if (dataBytes != null) { - val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE) - if (start != -1) { - val segmentBytes = dataBytes.copyOfRange(fromIndex = start, toIndex = dataBytes.size) - val metadata = com.drew.metadata.Metadata() - IptcReader().extract(SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.size.toLong()) - return metadata.directories + if (key == PNG_RAW_PROFILE_EXIF || key == PNG_RAW_PROFILE_IPTC) { + val match = PNG_RAW_PROFILE_PATTERN.matchEntire(valueString) + if (match != null) { + val dataString = match.groupValues[3] + val hexString = dataString.replace(Regex("[\\r\\n]"), "") + val dataBytes = hexString.decodeHex() + if (dataBytes != null) { + val metadata = com.drew.metadata.Metadata() + when (key) { + PNG_RAW_PROFILE_EXIF -> { + if (ExifReader.startsWithJpegExifPreamble(dataBytes)) { + ExifReader().extract(ByteArrayReader(dataBytes), metadata, ExifReader.JPEG_SEGMENT_PREAMBLE.length) + } + } + PNG_RAW_PROFILE_IPTC -> { + val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE) + if (start != -1) { + val segmentBytes = dataBytes.copyOfRange(fromIndex = start, toIndex = dataBytes.size) + IptcReader().extract(SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.size.toLong()) + } } } + return metadata.directories } } } @@ -101,15 +117,18 @@ object MetadataExtractorHelper { // convenience methods - private fun hexStringToByteArray(hexString: String): ByteArray? { - if (hexString.length % 2 != 0) return null + private fun String.decodeHex(): ByteArray? { + if (length % 2 != 0) return null - val dataBytes = ByteArray(hexString.length / 2) - var i = 0 - while (i < hexString.length) { - dataBytes[i / 2] = hexString.substring(i, i + 2).toByte(16) - i += 2 + try { + val byteIterator = chunkedSequence(2) + .map { it.toInt(16).toByte() } + .iterator() + + return ByteArray(length / 2) { byteIterator.next() } + } catch (e: NumberFormatException) { + Log.w(LOG_TAG, "failed to decode hex string=$this", e) } - return dataBytes + return null } } \ No newline at end of file