parent
31cd54879d
commit
33ae168eda
17 changed files with 309 additions and 13 deletions
|
@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
|
- Cataloguing: detect/filter `Ultra HDR`
|
||||||
|
- Info: show metadata from JPEG MPF
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- upgraded Flutter to stable v3.16.3
|
- upgraded Flutter to stable v3.16.3
|
||||||
|
|
|
@ -57,6 +57,7 @@ import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeInt
|
import deckers.thibault.aves.metadata.XMP.getSafeInt
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
|
||||||
import deckers.thibault.aves.metadata.XMP.getSafeString
|
import deckers.thibault.aves.metadata.XMP.getSafeString
|
||||||
|
import deckers.thibault.aves.metadata.XMP.hasHdrGainMap
|
||||||
import deckers.thibault.aves.metadata.XMP.isMotionPhoto
|
import deckers.thibault.aves.metadata.XMP.isMotionPhoto
|
||||||
import deckers.thibault.aves.metadata.XMP.isPanorama
|
import deckers.thibault.aves.metadata.XMP.isPanorama
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
||||||
|
@ -76,6 +77,8 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeRational
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
|
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
|
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
|
||||||
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
|
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
|
||||||
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
|
||||||
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfDirectory
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
|
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
@ -225,7 +228,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
foundMp4Uuid = metadata.directories.any { it is Mp4UuidBoxDirectory && it.tagCount > 0 }
|
foundMp4Uuid = metadata.directories.any { it is Mp4UuidBoxDirectory && it.tagCount > 0 }
|
||||||
|
|
||||||
val dirByName = metadata.directories.filter {
|
val dirByName = metadata.directories.filter {
|
||||||
(it.tagCount > 0 || it.errorCount > 0)
|
(it.tagCount > 0 || it.errorCount > 0 || it is MpEntryDirectory)
|
||||||
&& it !is FileTypeDirectory
|
&& it !is FileTypeDirectory
|
||||||
&& it !is AviDirectory
|
&& it !is AviDirectory
|
||||||
}.groupBy { dir -> dir.name }
|
}.groupBy { dir -> dir.name }
|
||||||
|
@ -344,6 +347,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dir is MpEntryDirectory -> {
|
||||||
|
dirMap.putAll(dir.describe())
|
||||||
|
}
|
||||||
|
|
||||||
else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
|
else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -551,6 +558,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
if (xmpMeta.isMotionPhoto()) {
|
if (xmpMeta.isMotionPhoto()) {
|
||||||
flags = flags or MASK_IS_MULTIPAGE or MASK_IS_MOTION_PHOTO
|
flags = flags or MASK_IS_MULTIPAGE or MASK_IS_MOTION_PHOTO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// identification of embedded gain map
|
||||||
|
if (xmpMeta.hasHdrGainMap()) {
|
||||||
|
flags = flags or MASK_HAS_HDR_GAIN_MAP
|
||||||
|
}
|
||||||
} catch (e: XMPException) {
|
} catch (e: XMPException) {
|
||||||
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
|
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
|
||||||
}
|
}
|
||||||
|
@ -623,6 +635,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JPEG Multi-Picture Format
|
||||||
|
for (dir in metadata.getDirectoriesOfType(MpfDirectory::class.java)) {
|
||||||
|
val imageCount = dir.getNumberOfImages()
|
||||||
|
if (imageCount > 1) {
|
||||||
|
flags = flags or MASK_IS_MULTIPAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// XMP
|
// XMP
|
||||||
if (!isLargeMp4(mimeType, sizeBytes)) {
|
if (!isLargeMp4(mimeType, sizeBytes)) {
|
||||||
metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach {
|
metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach {
|
||||||
|
@ -1297,6 +1317,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
private const val MASK_IS_360 = 1 shl 3
|
private const val MASK_IS_360 = 1 shl 3
|
||||||
private const val MASK_IS_MULTIPAGE = 1 shl 4
|
private const val MASK_IS_MULTIPAGE = 1 shl 4
|
||||||
private const val MASK_IS_MOTION_PHOTO = 1 shl 5
|
private const val MASK_IS_MOTION_PHOTO = 1 shl 5
|
||||||
|
private const val MASK_HAS_HDR_GAIN_MAP = 1 shl 6
|
||||||
private const val XMP_SUBJECTS_SEPARATOR = ";"
|
private const val XMP_SUBJECTS_SEPARATOR = ";"
|
||||||
|
|
||||||
// overlay metadata
|
// overlay metadata
|
||||||
|
|
|
@ -49,6 +49,7 @@ object XMP {
|
||||||
private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/"
|
private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/"
|
||||||
private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/"
|
private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/"
|
||||||
private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/"
|
private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/"
|
||||||
|
private const val HDRGM_NS_URI = "http://ns.adobe.com/hdr-gain-map/1.0/"
|
||||||
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
|
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
|
||||||
|
|
||||||
val DC_SUBJECT_PROP_NAME = XMPPropName(DC_NS_URI, "subject")
|
val DC_SUBJECT_PROP_NAME = XMPPropName(DC_NS_URI, "subject")
|
||||||
|
@ -83,13 +84,20 @@ object XMP {
|
||||||
val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length")
|
val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length")
|
||||||
val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime")
|
val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime")
|
||||||
|
|
||||||
// motion photo
|
// container
|
||||||
|
|
||||||
val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
|
val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
|
||||||
val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
|
val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
|
||||||
val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item")
|
val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item")
|
||||||
val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length")
|
val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length")
|
||||||
val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime")
|
val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime")
|
||||||
|
private val GCONTAINER_ITEM_SEMANTIC_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Semantic")
|
||||||
|
|
||||||
|
private const val ITEM_SEMANTIC_GAIN_MAP = "GainMap"
|
||||||
|
|
||||||
|
// HDR gain map
|
||||||
|
|
||||||
|
private val HDRGM_VERSION_PROP_NAME = XMPPropName(HDRGM_NS_URI, "Version")
|
||||||
|
|
||||||
// panorama
|
// panorama
|
||||||
// cf https://developers.google.com/streetview/spherical-metadata
|
// cf https://developers.google.com/streetview/spherical-metadata
|
||||||
|
@ -180,6 +188,35 @@ object XMP {
|
||||||
|
|
||||||
// extensions
|
// extensions
|
||||||
|
|
||||||
|
fun XMPMeta.hasHdrGainMap(): Boolean {
|
||||||
|
try {
|
||||||
|
// standard HDR gain map
|
||||||
|
if (doesPropExist(HDRGM_VERSION_PROP_NAME)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// `Ultra HDR`
|
||||||
|
if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
|
||||||
|
val count = countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
|
||||||
|
for (i in 1 until count + 1) {
|
||||||
|
val semantic = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_SEMANTIC_PROP_NAME))?.value
|
||||||
|
if (semantic == ITEM_SEMANTIC_GAIN_MAP) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} catch (e: XMPException) {
|
||||||
|
if (e.errorCode != XMPError.BADSCHEMA) {
|
||||||
|
// `BADSCHEMA` code is reported when we check a property
|
||||||
|
// from a non standard namespace, and that namespace is not declared in the XMP
|
||||||
|
Log.w(LOG_TAG, "failed to check HDR props from XMP", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
fun XMPMeta.isMotionPhoto(): Boolean {
|
fun XMPMeta.isMotionPhoto(): Boolean {
|
||||||
try {
|
try {
|
||||||
// GCamera motion photo
|
// GCamera motion photo
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.drew.metadata.xmp.XmpReader
|
||||||
import deckers.thibault.aves.metadata.ExifGeoTiffTags
|
import deckers.thibault.aves.metadata.ExifGeoTiffTags
|
||||||
import deckers.thibault.aves.metadata.GeoTiffKeys
|
import deckers.thibault.aves.metadata.GeoTiffKeys
|
||||||
import deckers.thibault.aves.metadata.Metadata
|
import deckers.thibault.aves.metadata.Metadata
|
||||||
|
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -97,6 +98,7 @@ object Helper {
|
||||||
val readers = ArrayList<JpegSegmentMetadataReader>().apply {
|
val readers = ArrayList<JpegSegmentMetadataReader>().apply {
|
||||||
addAll(JpegMetadataReader.ALL_READERS.filter { it !is XmpReader })
|
addAll(JpegMetadataReader.ALL_READERS.filter { it !is XmpReader })
|
||||||
add(SafeXmpReader())
|
add(SafeXmpReader())
|
||||||
|
add(MpfReader())
|
||||||
}
|
}
|
||||||
|
|
||||||
val metadata = com.drew.metadata.Metadata()
|
val metadata = com.drew.metadata.Metadata()
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
package deckers.thibault.aves.metadata.metadataextractor.mpf
|
||||||
|
|
||||||
|
import com.drew.metadata.Directory
|
||||||
|
import com.drew.metadata.TagDescriptor
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
|
|
||||||
|
class MpEntryDirectory(val id: Int, val entry: MpEntry) : Directory() {
|
||||||
|
private val descriptor = MpEntryDescriptor(this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
setDescriptor(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun describe(): Map<String, String> {
|
||||||
|
return HashMap<String, String>().apply {
|
||||||
|
put("Flags", descriptor.getFlagsDescription(entry.flags))
|
||||||
|
put("Format", descriptor.getFormatDescription(entry.format))
|
||||||
|
put("Type", descriptor.getTypeDescription(entry.type))
|
||||||
|
put("Size", "${entry.size} bytes")
|
||||||
|
put("Offset", "${entry.dataOffset} bytes")
|
||||||
|
put("Dependent Image 1 Entry Number", "${entry.dep1}")
|
||||||
|
put("Dependent Image 2 Entry Number", "${entry.dep2}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "MPF Image #$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTagNameMap(): HashMap<Int, String> {
|
||||||
|
return _tagNameMap
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val _tagNameMap = HashMap<Int, String>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MpEntryDescriptor(directory: MpEntryDirectory?) : TagDescriptor<MpEntryDirectory>(directory) {
|
||||||
|
fun getFlagsDescription(flags: Int): String {
|
||||||
|
val flagStrings = ArrayList<String>().apply {
|
||||||
|
if (flags and FLAG_REPRESENTATIVE != 0) add("representative image")
|
||||||
|
if (flags and FLAG_DEPENDENT_CHILD != 0) add("dependent child image")
|
||||||
|
if (flags and FLAG_DEPENDENT_PARENT != 0) add("dependent parent image")
|
||||||
|
}
|
||||||
|
return if (flagStrings.isEmpty()) "none" else flagStrings.joinToString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFormatDescription(format: Int): String {
|
||||||
|
return when (format) {
|
||||||
|
0 -> MimeTypes.JPEG
|
||||||
|
else -> "Unknown ($format)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTypeDescription(type: Int): String {
|
||||||
|
return when (type) {
|
||||||
|
0x030000 -> "Baseline MP Primary Image"
|
||||||
|
0x010001 -> "Large Thumbnail (VGA equivalent)"
|
||||||
|
0x010002 -> "Large Thumbnail (full HD equivalent)"
|
||||||
|
0x020001 -> "Multi-frame Panorama"
|
||||||
|
0x020002 -> "Multi-frame Disparity"
|
||||||
|
0x020003 -> "Multi-angle"
|
||||||
|
0x000000 -> "Undefined"
|
||||||
|
else -> "Unknown ($type)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FLAG_REPRESENTATIVE = 1 shl 2
|
||||||
|
private const val FLAG_DEPENDENT_CHILD = 1 shl 3
|
||||||
|
private const val FLAG_DEPENDENT_PARENT = 1 shl 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MpEntry(val flags: Int, val format: Int, val type: Int, val size: Long, val dataOffset: Long, val dep1: Short, val dep2: Short)
|
|
@ -0,0 +1,38 @@
|
||||||
|
package deckers.thibault.aves.metadata.metadataextractor.mpf
|
||||||
|
|
||||||
|
import com.drew.metadata.Directory
|
||||||
|
import com.drew.metadata.TagDescriptor
|
||||||
|
|
||||||
|
class MpfDirectory : Directory() {
|
||||||
|
init {
|
||||||
|
setDescriptor(MpfDescriptor(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "MPF"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTagNameMap(): HashMap<Int, String> {
|
||||||
|
return _tagNameMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNumberOfImages() = getInt(TAG_NUMBER_OF_IMAGES)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG_MPF_VERSION = 0xb000
|
||||||
|
const val TAG_NUMBER_OF_IMAGES = 0xb001
|
||||||
|
const val TAG_MP_ENTRY = 0xb002
|
||||||
|
private const val TAG_IMAGE_UID_LIST = 0xb003
|
||||||
|
private const val TAG_TOTAL_FRAMES = 0xb004
|
||||||
|
|
||||||
|
private val _tagNameMap = HashMap<Int, String>().apply {
|
||||||
|
put(TAG_MPF_VERSION, "MPF Version")
|
||||||
|
put(TAG_NUMBER_OF_IMAGES, "Number Of Images")
|
||||||
|
put(TAG_MP_ENTRY, "MP Entry")
|
||||||
|
put(TAG_IMAGE_UID_LIST, "Image UID List")
|
||||||
|
put(TAG_TOTAL_FRAMES, "Total Frames")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MpfDescriptor(directory: MpfDirectory?) : TagDescriptor<MpfDirectory>(directory)
|
|
@ -0,0 +1,95 @@
|
||||||
|
package deckers.thibault.aves.metadata.metadataextractor.mpf
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.drew.imaging.jpeg.JpegSegmentMetadataReader
|
||||||
|
import com.drew.imaging.jpeg.JpegSegmentType
|
||||||
|
import com.drew.lang.ByteArrayReader
|
||||||
|
import com.drew.lang.RandomAccessReader
|
||||||
|
import com.drew.metadata.Metadata
|
||||||
|
import com.drew.metadata.MetadataReader
|
||||||
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
|
||||||
|
class MpfReader : JpegSegmentMetadataReader, MetadataReader {
|
||||||
|
override fun getSegmentTypes(): Iterable<JpegSegmentType> {
|
||||||
|
return listOf(JpegSegmentType.APP2)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun readJpegSegments(segments: Iterable<ByteArray>, metadata: Metadata, segmentType: JpegSegmentType) {
|
||||||
|
for (segmentBytes in segments) {
|
||||||
|
// Skip segments not starting with the required header
|
||||||
|
if (segmentBytes.size >= PREAMBLE.length && PREAMBLE == String(segmentBytes, 0, PREAMBLE.length)) {
|
||||||
|
extract(ByteArrayReader(segmentBytes), metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun extract(reader: RandomAccessReader, metadata: Metadata) {
|
||||||
|
val directory = MpfDirectory()
|
||||||
|
metadata.addDirectory(directory)
|
||||||
|
|
||||||
|
val baseOffset = 4
|
||||||
|
|
||||||
|
// MP Format Identifier (4Byte)
|
||||||
|
// MP header
|
||||||
|
// - MP Endian (4Byte)
|
||||||
|
val byteOrderIdentifier = reader.getInt16(baseOffset)
|
||||||
|
if (byteOrderIdentifier.toInt() == 0x4d4d) { // "MM"
|
||||||
|
reader.isMotorolaByteOrder = true
|
||||||
|
} else if (byteOrderIdentifier.toInt() == 0x4949) { // "II"
|
||||||
|
reader.isMotorolaByteOrder = false
|
||||||
|
}
|
||||||
|
// - Offset to First IFD (4Byte)
|
||||||
|
val firstIfdOffset = reader.getInt32(baseOffset + 4)
|
||||||
|
|
||||||
|
// [in primary image only] MP Index IFD:
|
||||||
|
// - Count (2Byte)
|
||||||
|
var offset = baseOffset + firstIfdOffset
|
||||||
|
val tagCount = reader.getInt16(offset)
|
||||||
|
offset += 2
|
||||||
|
// - MP Index Fields (Overall Structure Info.)
|
||||||
|
var imageCount = 0
|
||||||
|
for (tag in 0..<tagCount) {
|
||||||
|
when (val tagId = reader.getUInt16(offset)) {
|
||||||
|
MpfDirectory.TAG_MPF_VERSION -> directory.setString(tagId, reader.getString(offset + 8, 4, Charsets.US_ASCII))
|
||||||
|
MpfDirectory.TAG_NUMBER_OF_IMAGES -> {
|
||||||
|
imageCount = reader.getInt32(offset + 8)
|
||||||
|
directory.setInt(tagId, imageCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
MpfDirectory.TAG_MP_ENTRY -> {
|
||||||
|
var mpEntryOffset = baseOffset + reader.getInt32(offset + 8)
|
||||||
|
for (index in 0..<imageCount) {
|
||||||
|
// individual image
|
||||||
|
val attribute = reader.getUInt32(mpEntryOffset)
|
||||||
|
val flags = (attribute shr 27 and 0x1f).toInt()
|
||||||
|
val format = (attribute shr 24 and 0x7).toInt()
|
||||||
|
val type = (attribute and 0xffffff).toInt()
|
||||||
|
val size = reader.getUInt32(mpEntryOffset + 4)
|
||||||
|
val dataOffset = reader.getUInt32(mpEntryOffset + 8)
|
||||||
|
val dep1 = reader.getInt16(mpEntryOffset + 12)
|
||||||
|
val dep2 = reader.getInt16(mpEntryOffset + 14)
|
||||||
|
metadata.addDirectory(MpEntryDirectory(index + 1, MpEntry(flags, format, type, size, dataOffset, dep1, dep2)))
|
||||||
|
mpEntryOffset += 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Log.d(LOG_TAG, "unknown tag=$tagId")
|
||||||
|
}
|
||||||
|
offset += 12
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Offset of Next IFD (4Byte)
|
||||||
|
// Value (MP Index IFD)
|
||||||
|
|
||||||
|
// [in primary & other images] MP Attributes IFD:
|
||||||
|
// - Count (2Byte)
|
||||||
|
// - MP Attribute Fields (Details of Specific Image Usage)
|
||||||
|
// - Offset of Next IFD
|
||||||
|
// Value (MP Attribute IFD)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val LOG_TAG = LogUtils.createTag<MpfReader>()
|
||||||
|
private const val PREAMBLE = "MPF"
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ import 'dart:async';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/multipage.dart';
|
import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/ref/bursts.dart';
|
import 'package:aves/ref/bursts.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
@ -12,10 +11,7 @@ extension ExtraAvesEntryMultipage on AvesEntry {
|
||||||
|
|
||||||
bool get isBurst => burstEntries?.isNotEmpty == true;
|
bool get isBurst => burstEntries?.isNotEmpty == true;
|
||||||
|
|
||||||
// for backward compatibility
|
bool get isMotionPhoto => catalogMetadata?.isMotionPhoto ?? false;
|
||||||
bool get _isMotionPhotoLegacy => isMultiPage && !isBurst && mimeType == MimeTypes.jpeg;
|
|
||||||
|
|
||||||
bool get isMotionPhoto => (catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
|
|
||||||
|
|
||||||
String? getBurstKey(List<String> patterns) {
|
String? getBurstKey(List<String> patterns) {
|
||||||
final key = BurstPatterns.getKeyForName(filenameWithoutExtension, patterns);
|
final key = BurstPatterns.getKeyForName(filenameWithoutExtension, patterns);
|
||||||
|
|
|
@ -74,6 +74,8 @@ extension ExtraAvesEntryProps on AvesEntry {
|
||||||
|
|
||||||
bool get is360 => catalogMetadata?.is360 ?? false;
|
bool get is360 => catalogMetadata?.is360 ?? false;
|
||||||
|
|
||||||
|
bool get isHdr => (catalogMetadata?.hasHdrGainMap ?? false);
|
||||||
|
|
||||||
// trash
|
// trash
|
||||||
|
|
||||||
bool get isExpiredTrash {
|
bool get isExpiredTrash {
|
||||||
|
|
|
@ -13,7 +13,8 @@ class TypeFilter extends CollectionFilter {
|
||||||
|
|
||||||
static const _animated = 'animated'; // subset of `image/gif` and `image/webp`
|
static const _animated = 'animated'; // subset of `image/gif` and `image/webp`
|
||||||
static const _geotiff = 'geotiff'; // subset of `image/tiff`
|
static const _geotiff = 'geotiff'; // subset of `image/tiff`
|
||||||
static const _motionPhoto = 'motion_photo'; // subset of `image/jpeg`
|
static const _hdr = 'hdr'; // subset of `image/jpeg`
|
||||||
|
static const _motionPhoto = 'motion_photo'; // subset of images (jpeg, heic)
|
||||||
static const _panorama = 'panorama'; // subset of images
|
static const _panorama = 'panorama'; // subset of images
|
||||||
static const _raw = 'raw'; // specific image formats
|
static const _raw = 'raw'; // specific image formats
|
||||||
static const _sphericalVideo = 'spherical_video'; // subset of videos
|
static const _sphericalVideo = 'spherical_video'; // subset of videos
|
||||||
|
@ -24,6 +25,7 @@ class TypeFilter extends CollectionFilter {
|
||||||
|
|
||||||
static final animated = TypeFilter._private(_animated);
|
static final animated = TypeFilter._private(_animated);
|
||||||
static final geotiff = TypeFilter._private(_geotiff);
|
static final geotiff = TypeFilter._private(_geotiff);
|
||||||
|
static final hdr = TypeFilter._private(_hdr);
|
||||||
static final motionPhoto = TypeFilter._private(_motionPhoto);
|
static final motionPhoto = TypeFilter._private(_motionPhoto);
|
||||||
static final panorama = TypeFilter._private(_panorama);
|
static final panorama = TypeFilter._private(_panorama);
|
||||||
static final raw = TypeFilter._private(_raw);
|
static final raw = TypeFilter._private(_raw);
|
||||||
|
@ -40,6 +42,9 @@ class TypeFilter extends CollectionFilter {
|
||||||
case _geotiff:
|
case _geotiff:
|
||||||
_test = (entry) => entry.isGeotiff;
|
_test = (entry) => entry.isGeotiff;
|
||||||
_icon = AIcons.geo;
|
_icon = AIcons.geo;
|
||||||
|
case _hdr:
|
||||||
|
_test = (entry) => entry.isHdr;
|
||||||
|
_icon = AIcons.hdr;
|
||||||
case _motionPhoto:
|
case _motionPhoto:
|
||||||
_test = (entry) => entry.isMotionPhoto;
|
_test = (entry) => entry.isMotionPhoto;
|
||||||
_icon = AIcons.motionPhoto;
|
_icon = AIcons.motionPhoto;
|
||||||
|
@ -83,11 +88,12 @@ class TypeFilter extends CollectionFilter {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return switch (itemType) {
|
return switch (itemType) {
|
||||||
_animated => l10n.filterTypeAnimatedLabel,
|
_animated => l10n.filterTypeAnimatedLabel,
|
||||||
|
_geotiff => l10n.filterTypeGeotiffLabel,
|
||||||
|
_hdr => 'HDR',
|
||||||
_motionPhoto => l10n.filterTypeMotionPhotoLabel,
|
_motionPhoto => l10n.filterTypeMotionPhotoLabel,
|
||||||
_panorama => l10n.filterTypePanoramaLabel,
|
_panorama => l10n.filterTypePanoramaLabel,
|
||||||
_raw => l10n.filterTypeRawLabel,
|
_raw => l10n.filterTypeRawLabel,
|
||||||
_sphericalVideo => l10n.filterTypeSphericalVideoLabel,
|
_sphericalVideo => l10n.filterTypeSphericalVideoLabel,
|
||||||
_geotiff => l10n.filterTypeGeotiffLabel,
|
|
||||||
_ => itemType,
|
_ => itemType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||||
class CatalogMetadata {
|
class CatalogMetadata {
|
||||||
final int id;
|
final int id;
|
||||||
final int? dateMillis;
|
final int? dateMillis;
|
||||||
final bool isAnimated, isGeotiff, is360, isMultiPage, isMotionPhoto;
|
final bool isAnimated, isGeotiff, is360, isMultiPage, isMotionPhoto, hasHdrGainMap;
|
||||||
bool isFlipped;
|
bool isFlipped;
|
||||||
int? rotationDegrees;
|
int? rotationDegrees;
|
||||||
final String? mimeType, xmpSubjects, xmpTitle;
|
final String? mimeType, xmpSubjects, xmpTitle;
|
||||||
|
@ -19,6 +19,7 @@ class CatalogMetadata {
|
||||||
static const _is360Mask = 1 << 3;
|
static const _is360Mask = 1 << 3;
|
||||||
static const _isMultiPageMask = 1 << 4;
|
static const _isMultiPageMask = 1 << 4;
|
||||||
static const _isMotionPhotoMask = 1 << 5;
|
static const _isMotionPhotoMask = 1 << 5;
|
||||||
|
static const _hasHdrGainMapMask = 1 << 6;
|
||||||
|
|
||||||
CatalogMetadata({
|
CatalogMetadata({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
@ -30,6 +31,7 @@ class CatalogMetadata {
|
||||||
this.is360 = false,
|
this.is360 = false,
|
||||||
this.isMultiPage = false,
|
this.isMultiPage = false,
|
||||||
this.isMotionPhoto = false,
|
this.isMotionPhoto = false,
|
||||||
|
this.hasHdrGainMap = false,
|
||||||
this.rotationDegrees,
|
this.rotationDegrees,
|
||||||
this.xmpSubjects,
|
this.xmpSubjects,
|
||||||
this.xmpTitle,
|
this.xmpTitle,
|
||||||
|
@ -71,6 +73,7 @@ class CatalogMetadata {
|
||||||
is360: is360,
|
is360: is360,
|
||||||
isMultiPage: isMultiPage ?? this.isMultiPage,
|
isMultiPage: isMultiPage ?? this.isMultiPage,
|
||||||
isMotionPhoto: isMotionPhoto,
|
isMotionPhoto: isMotionPhoto,
|
||||||
|
hasHdrGainMap: hasHdrGainMap,
|
||||||
rotationDegrees: rotationDegrees ?? this.rotationDegrees,
|
rotationDegrees: rotationDegrees ?? this.rotationDegrees,
|
||||||
xmpSubjects: xmpSubjects,
|
xmpSubjects: xmpSubjects,
|
||||||
xmpTitle: xmpTitle,
|
xmpTitle: xmpTitle,
|
||||||
|
@ -92,6 +95,7 @@ class CatalogMetadata {
|
||||||
is360: flags & _is360Mask != 0,
|
is360: flags & _is360Mask != 0,
|
||||||
isMultiPage: flags & _isMultiPageMask != 0,
|
isMultiPage: flags & _isMultiPageMask != 0,
|
||||||
isMotionPhoto: flags & _isMotionPhotoMask != 0,
|
isMotionPhoto: flags & _isMotionPhotoMask != 0,
|
||||||
|
hasHdrGainMap: flags & _hasHdrGainMapMask != 0,
|
||||||
// `rotationDegrees` should default to `sourceRotationDegrees`, not 0
|
// `rotationDegrees` should default to `sourceRotationDegrees`, not 0
|
||||||
rotationDegrees: map['rotationDegrees'],
|
rotationDegrees: map['rotationDegrees'],
|
||||||
xmpSubjects: map['xmpSubjects'] ?? '',
|
xmpSubjects: map['xmpSubjects'] ?? '',
|
||||||
|
@ -106,7 +110,7 @@ class CatalogMetadata {
|
||||||
'id': id,
|
'id': id,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
'dateMillis': dateMillis,
|
'dateMillis': dateMillis,
|
||||||
'flags': (isAnimated ? _isAnimatedMask : 0) | (isFlipped ? _isFlippedMask : 0) | (isGeotiff ? _isGeotiffMask : 0) | (is360 ? _is360Mask : 0) | (isMultiPage ? _isMultiPageMask : 0) | (isMotionPhoto ? _isMotionPhotoMask : 0),
|
'flags': (isAnimated ? _isAnimatedMask : 0) | (isFlipped ? _isFlippedMask : 0) | (isGeotiff ? _isGeotiffMask : 0) | (is360 ? _is360Mask : 0) | (isMultiPage ? _isMultiPageMask : 0) | (isMotionPhoto ? _isMotionPhotoMask : 0) | (hasHdrGainMap ? _hasHdrGainMapMask : 0),
|
||||||
'rotationDegrees': rotationDegrees,
|
'rotationDegrees': rotationDegrees,
|
||||||
'xmpSubjects': xmpSubjects,
|
'xmpSubjects': xmpSubjects,
|
||||||
'xmpTitle': xmpTitle,
|
'xmpTitle': xmpTitle,
|
||||||
|
|
|
@ -169,6 +169,7 @@ class AIcons {
|
||||||
// thumbnail overlay
|
// thumbnail overlay
|
||||||
static const animated = Icons.slideshow;
|
static const animated = Icons.slideshow;
|
||||||
static const geo = Icons.language_outlined;
|
static const geo = Icons.language_outlined;
|
||||||
|
static const hdr = Icons.hdr_on_outlined;
|
||||||
static const motionPhoto = Icons.motion_photos_on_outlined;
|
static const motionPhoto = Icons.motion_photos_on_outlined;
|
||||||
static const multiPage = Icons.burst_mode_outlined;
|
static const multiPage = Icons.burst_mode_outlined;
|
||||||
static const panorama = Icons.vrpano_outlined;
|
static const panorama = Icons.vrpano_outlined;
|
||||||
|
|
|
@ -98,8 +98,9 @@ class GridThemeData {
|
||||||
if (entry.is360) const PanoramaIcon(),
|
if (entry.is360) const PanoramaIcon(),
|
||||||
],
|
],
|
||||||
if (entry.isMultiPage) ...[
|
if (entry.isMultiPage) ...[
|
||||||
|
if (entry.isHdr) const HdrIcon(),
|
||||||
if (entry.isMotionPhoto && showMotionPhoto) const MotionPhotoIcon(),
|
if (entry.isMotionPhoto && showMotionPhoto) const MotionPhotoIcon(),
|
||||||
if (!entry.isMotionPhoto) MultiPageIcon(entry: entry),
|
if (!entry.isHdr && !entry.isMotionPhoto) MultiPageIcon(entry: entry),
|
||||||
],
|
],
|
||||||
if (entry.isGeotiff) const GeoTiffIcon(),
|
if (entry.isGeotiff) const GeoTiffIcon(),
|
||||||
if (entry.trashed && showTrash) TrashIcon(trashDaysLeft: entry.trashDaysLeft),
|
if (entry.trashed && showTrash) TrashIcon(trashDaysLeft: entry.trashDaysLeft),
|
||||||
|
|
|
@ -65,6 +65,17 @@ class GeoTiffIcon extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HdrIcon extends StatelessWidget {
|
||||||
|
const HdrIcon({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const OverlayIcon(
|
||||||
|
icon: AIcons.hdr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PanoramaIcon extends StatelessWidget {
|
class PanoramaIcon extends StatelessWidget {
|
||||||
const PanoramaIcon({super.key});
|
const PanoramaIcon({super.key});
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
|
||||||
TypeFilter.panorama,
|
TypeFilter.panorama,
|
||||||
TypeFilter.sphericalVideo,
|
TypeFilter.sphericalVideo,
|
||||||
TypeFilter.geotiff,
|
TypeFilter.geotiff,
|
||||||
|
TypeFilter.hdr,
|
||||||
TypeFilter.raw,
|
TypeFilter.raw,
|
||||||
MimeFilter(MimeTypes.svg),
|
MimeFilter(MimeTypes.svg),
|
||||||
];
|
];
|
||||||
|
|
|
@ -131,9 +131,10 @@ class ViewerDebugPage extends StatelessWidget {
|
||||||
'isSvg': '${entry.isSvg}',
|
'isSvg': '${entry.isSvg}',
|
||||||
'isVideo': '${entry.isVideo}',
|
'isVideo': '${entry.isVideo}',
|
||||||
'isCatalogued': '${entry.isCatalogued}',
|
'isCatalogued': '${entry.isCatalogued}',
|
||||||
|
'is360': '${entry.is360}',
|
||||||
'isAnimated': '${entry.isAnimated}',
|
'isAnimated': '${entry.isAnimated}',
|
||||||
'isGeotiff': '${entry.isGeotiff}',
|
'isGeotiff': '${entry.isGeotiff}',
|
||||||
'is360': '${entry.is360}',
|
'isHdr': '${entry.isHdr}',
|
||||||
'isMultiPage': '${entry.isMultiPage}',
|
'isMultiPage': '${entry.isMultiPage}',
|
||||||
'isMotionPhoto': '${entry.isMotionPhoto}',
|
'isMotionPhoto': '${entry.isMotionPhoto}',
|
||||||
'canEdit': '${entry.canEdit}',
|
'canEdit': '${entry.canEdit}',
|
||||||
|
|
|
@ -122,6 +122,7 @@ class _BasicSectionState extends State<BasicSection> {
|
||||||
MimeFilter(entry.mimeType),
|
MimeFilter(entry.mimeType),
|
||||||
if (entry.isAnimated) TypeFilter.animated,
|
if (entry.isAnimated) TypeFilter.animated,
|
||||||
if (entry.isGeotiff) TypeFilter.geotiff,
|
if (entry.isGeotiff) TypeFilter.geotiff,
|
||||||
|
if (entry.isHdr) TypeFilter.hdr,
|
||||||
if (entry.isMotionPhoto) TypeFilter.motionPhoto,
|
if (entry.isMotionPhoto) TypeFilter.motionPhoto,
|
||||||
if (entry.isRaw) TypeFilter.raw,
|
if (entry.isRaw) TypeFilter.raw,
|
||||||
if (entry.isImage && entry.is360) TypeFilter.panorama,
|
if (entry.isImage && entry.is360) TypeFilter.panorama,
|
||||||
|
|
Loading…
Reference in a new issue