info: fixed PNG exif/iptc raw profile extraction

This commit is contained in:
Thibault Deckers 2021-12-31 11:41:14 +09:00
parent 30d875f1cf
commit acdb7afa6d
2 changed files with 42 additions and 23 deletions

View file

@ -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) })

View file

@ -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<MetadataExtractorHelper>()
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<Directory>? {
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
}
}