info: fixed PNG exif/iptc raw profile extraction
This commit is contained in:
parent
30d875f1cf
commit
acdb7afa6d
2 changed files with 42 additions and 23 deletions
|
@ -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) })
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue