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) val dirs = extractPngProfile(key, valueString)
if (dirs?.any() == true) { if (dirs?.any() == true) {
dirs.forEach { profileDir -> dirs.forEach { profileDir ->
val profileDirName = profileDir.name val profileDirName = "${dir.name}/${profileDir.name}"
val profileDirMap = metadataMap[profileDirName] ?: HashMap() val profileDirMap = metadataMap[profileDirName] ?: HashMap()
metadataMap[profileDirName] = profileDirMap metadataMap[profileDirName] = profileDirMap
profileDirMap.putAll(profileDir.tags.map { Pair(it.tagName, it.description) }) profileDirMap.putAll(profileDir.tags.map { Pair(it.tagName, it.description) })

View file

@ -1,25 +1,34 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
import android.util.Log
import com.drew.lang.ByteArrayReader
import com.drew.lang.Rational import com.drew.lang.Rational
import com.drew.lang.SequentialByteArrayReader import com.drew.lang.SequentialByteArrayReader
import com.drew.metadata.Directory import com.drew.metadata.Directory
import com.drew.metadata.exif.ExifIFD0Directory import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifReader
import com.drew.metadata.iptc.IptcReader import com.drew.metadata.iptc.IptcReader
import com.drew.metadata.png.PngDirectory import com.drew.metadata.png.PngDirectory
import deckers.thibault.aves.utils.LogUtils
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
object MetadataExtractorHelper { object MetadataExtractorHelper {
private val LOG_TAG = LogUtils.createTag<MetadataExtractorHelper>()
const val PNG_ITXT_DIR_NAME = "PNG-iTXt" const val PNG_ITXT_DIR_NAME = "PNG-iTXt"
private const val PNG_TEXT_DIR_NAME = "PNG-tEXt" private const val PNG_TEXT_DIR_NAME = "PNG-tEXt"
const val PNG_TIME_DIR_NAME = "PNG-tIME" const val PNG_TIME_DIR_NAME = "PNG-tIME"
private const val PNG_ZTXT_DIR_NAME = "PNG-zTXt" 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) val PNG_LAST_MODIFICATION_TIME_FORMAT = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.ROOT)
// Pattern to extract profile name, length, and text data // Pattern to extract profile name, length, and text data
// of raw profiles (EXIF, IPTC, etc.) in PNG `zTXt` chunks // of raw profiles (EXIF, IPTC, etc.) in PNG `zTXt` chunks
// e.g. "iptc [...] 114 [...] 3842494d040400[...]" // 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) private val PNG_RAW_PROFILE_PATTERN = Regex("^\\n(.*?)\\n\\s*(\\d+)\\n(.*)", RegexOption.DOT_MATCHES_ALL)
// extensions // 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 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>? { fun extractPngProfile(key: String, valueString: String): Iterable<Directory>? {
when (key) { if (key == PNG_RAW_PROFILE_EXIF || key == PNG_RAW_PROFILE_IPTC) {
"Raw profile type iptc" -> { val match = PNG_RAW_PROFILE_PATTERN.matchEntire(valueString)
val match = PNG_RAW_PROFILE_PATTERN.matchEntire(valueString) if (match != null) {
if (match != null) { val dataString = match.groupValues[3]
val dataString = match.groupValues[3] val hexString = dataString.replace(Regex("[\\r\\n]"), "")
val hexString = dataString.replace(Regex("[\\r\\n]"), "") val dataBytes = hexString.decodeHex()
val dataBytes = hexStringToByteArray(hexString) if (dataBytes != null) {
if (dataBytes != null) { val metadata = com.drew.metadata.Metadata()
val start = dataBytes.indexOf(Metadata.IPTC_MARKER_BYTE) when (key) {
if (start != -1) { PNG_RAW_PROFILE_EXIF -> {
val segmentBytes = dataBytes.copyOfRange(fromIndex = start, toIndex = dataBytes.size) if (ExifReader.startsWithJpegExifPreamble(dataBytes)) {
val metadata = com.drew.metadata.Metadata() ExifReader().extract(ByteArrayReader(dataBytes), metadata, ExifReader.JPEG_SEGMENT_PREAMBLE.length)
IptcReader().extract(SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.size.toLong()) }
return metadata.directories }
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 // convenience methods
private fun hexStringToByteArray(hexString: String): ByteArray? { private fun String.decodeHex(): ByteArray? {
if (hexString.length % 2 != 0) return null if (length % 2 != 0) return null
val dataBytes = ByteArray(hexString.length / 2) try {
var i = 0 val byteIterator = chunkedSequence(2)
while (i < hexString.length) { .map { it.toInt(16).toByte() }
dataBytes[i / 2] = hexString.substring(i, i + 2).toByte(16) .iterator()
i += 2
return ByteArray(length / 2) { byteIterator.next() }
} catch (e: NumberFormatException) {
Log.w(LOG_TAG, "failed to decode hex string=$this", e)
} }
return dataBytes return null
} }
} }