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]
|
||||
|
||||
- Cataloguing: detect/filter `Ultra HDR`
|
||||
- Info: show metadata from JPEG MPF
|
||||
|
||||
### Changed
|
||||
|
||||
- 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.getSafeLocalizedText
|
||||
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.isPanorama
|
||||
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.isPngTextDir
|
||||
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.utils.ContextUtils.queryContentPropValue
|
||||
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 }
|
||||
|
||||
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 AviDirectory
|
||||
}.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) })
|
||||
}
|
||||
}
|
||||
|
@ -551,6 +558,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
if (xmpMeta.isMotionPhoto()) {
|
||||
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) {
|
||||
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
|
||||
if (!isLargeMp4(mimeType, sizeBytes)) {
|
||||
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_MULTIPAGE = 1 shl 4
|
||||
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 = ";"
|
||||
|
||||
// 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 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 HDRGM_NS_URI = "http://ns.adobe.com/hdr-gain-map/1.0/"
|
||||
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
|
||||
|
||||
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_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime")
|
||||
|
||||
// motion photo
|
||||
// container
|
||||
|
||||
val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
|
||||
val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
|
||||
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_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
|
||||
// cf https://developers.google.com/streetview/spherical-metadata
|
||||
|
@ -180,6 +188,35 @@ object XMP {
|
|||
|
||||
// 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 {
|
||||
try {
|
||||
// GCamera motion photo
|
||||
|
|
|
@ -27,6 +27,7 @@ import com.drew.metadata.xmp.XmpReader
|
|||
import deckers.thibault.aves.metadata.ExifGeoTiffTags
|
||||
import deckers.thibault.aves.metadata.GeoTiffKeys
|
||||
import deckers.thibault.aves.metadata.Metadata
|
||||
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.IOException
|
||||
|
@ -97,6 +98,7 @@ object Helper {
|
|||
val readers = ArrayList<JpegSegmentMetadataReader>().apply {
|
||||
addAll(JpegMetadataReader.ALL_READERS.filter { it !is XmpReader })
|
||||
add(SafeXmpReader())
|
||||
add(MpfReader())
|
||||
}
|
||||
|
||||
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/multipage.dart';
|
||||
import 'package:aves/ref/bursts.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
|
@ -12,10 +11,7 @@ extension ExtraAvesEntryMultipage on AvesEntry {
|
|||
|
||||
bool get isBurst => burstEntries?.isNotEmpty == true;
|
||||
|
||||
// for backward compatibility
|
||||
bool get _isMotionPhotoLegacy => isMultiPage && !isBurst && mimeType == MimeTypes.jpeg;
|
||||
|
||||
bool get isMotionPhoto => (catalogMetadata?.isMotionPhoto ?? false) || _isMotionPhotoLegacy;
|
||||
bool get isMotionPhoto => catalogMetadata?.isMotionPhoto ?? false;
|
||||
|
||||
String? getBurstKey(List<String> patterns) {
|
||||
final key = BurstPatterns.getKeyForName(filenameWithoutExtension, patterns);
|
||||
|
|
|
@ -74,6 +74,8 @@ extension ExtraAvesEntryProps on AvesEntry {
|
|||
|
||||
bool get is360 => catalogMetadata?.is360 ?? false;
|
||||
|
||||
bool get isHdr => (catalogMetadata?.hasHdrGainMap ?? false);
|
||||
|
||||
// trash
|
||||
|
||||
bool get isExpiredTrash {
|
||||
|
|
|
@ -13,7 +13,8 @@ class TypeFilter extends CollectionFilter {
|
|||
|
||||
static const _animated = 'animated'; // subset of `image/gif` and `image/webp`
|
||||
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 _raw = 'raw'; // specific image formats
|
||||
static const _sphericalVideo = 'spherical_video'; // subset of videos
|
||||
|
@ -24,6 +25,7 @@ class TypeFilter extends CollectionFilter {
|
|||
|
||||
static final animated = TypeFilter._private(_animated);
|
||||
static final geotiff = TypeFilter._private(_geotiff);
|
||||
static final hdr = TypeFilter._private(_hdr);
|
||||
static final motionPhoto = TypeFilter._private(_motionPhoto);
|
||||
static final panorama = TypeFilter._private(_panorama);
|
||||
static final raw = TypeFilter._private(_raw);
|
||||
|
@ -40,6 +42,9 @@ class TypeFilter extends CollectionFilter {
|
|||
case _geotiff:
|
||||
_test = (entry) => entry.isGeotiff;
|
||||
_icon = AIcons.geo;
|
||||
case _hdr:
|
||||
_test = (entry) => entry.isHdr;
|
||||
_icon = AIcons.hdr;
|
||||
case _motionPhoto:
|
||||
_test = (entry) => entry.isMotionPhoto;
|
||||
_icon = AIcons.motionPhoto;
|
||||
|
@ -83,11 +88,12 @@ class TypeFilter extends CollectionFilter {
|
|||
final l10n = context.l10n;
|
||||
return switch (itemType) {
|
||||
_animated => l10n.filterTypeAnimatedLabel,
|
||||
_geotiff => l10n.filterTypeGeotiffLabel,
|
||||
_hdr => 'HDR',
|
||||
_motionPhoto => l10n.filterTypeMotionPhotoLabel,
|
||||
_panorama => l10n.filterTypePanoramaLabel,
|
||||
_raw => l10n.filterTypeRawLabel,
|
||||
_sphericalVideo => l10n.filterTypeSphericalVideoLabel,
|
||||
_geotiff => l10n.filterTypeGeotiffLabel,
|
||||
_ => itemType,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||
class CatalogMetadata {
|
||||
final int id;
|
||||
final int? dateMillis;
|
||||
final bool isAnimated, isGeotiff, is360, isMultiPage, isMotionPhoto;
|
||||
final bool isAnimated, isGeotiff, is360, isMultiPage, isMotionPhoto, hasHdrGainMap;
|
||||
bool isFlipped;
|
||||
int? rotationDegrees;
|
||||
final String? mimeType, xmpSubjects, xmpTitle;
|
||||
|
@ -19,6 +19,7 @@ class CatalogMetadata {
|
|||
static const _is360Mask = 1 << 3;
|
||||
static const _isMultiPageMask = 1 << 4;
|
||||
static const _isMotionPhotoMask = 1 << 5;
|
||||
static const _hasHdrGainMapMask = 1 << 6;
|
||||
|
||||
CatalogMetadata({
|
||||
required this.id,
|
||||
|
@ -30,6 +31,7 @@ class CatalogMetadata {
|
|||
this.is360 = false,
|
||||
this.isMultiPage = false,
|
||||
this.isMotionPhoto = false,
|
||||
this.hasHdrGainMap = false,
|
||||
this.rotationDegrees,
|
||||
this.xmpSubjects,
|
||||
this.xmpTitle,
|
||||
|
@ -71,6 +73,7 @@ class CatalogMetadata {
|
|||
is360: is360,
|
||||
isMultiPage: isMultiPage ?? this.isMultiPage,
|
||||
isMotionPhoto: isMotionPhoto,
|
||||
hasHdrGainMap: hasHdrGainMap,
|
||||
rotationDegrees: rotationDegrees ?? this.rotationDegrees,
|
||||
xmpSubjects: xmpSubjects,
|
||||
xmpTitle: xmpTitle,
|
||||
|
@ -92,6 +95,7 @@ class CatalogMetadata {
|
|||
is360: flags & _is360Mask != 0,
|
||||
isMultiPage: flags & _isMultiPageMask != 0,
|
||||
isMotionPhoto: flags & _isMotionPhotoMask != 0,
|
||||
hasHdrGainMap: flags & _hasHdrGainMapMask != 0,
|
||||
// `rotationDegrees` should default to `sourceRotationDegrees`, not 0
|
||||
rotationDegrees: map['rotationDegrees'],
|
||||
xmpSubjects: map['xmpSubjects'] ?? '',
|
||||
|
@ -106,7 +110,7 @@ class CatalogMetadata {
|
|||
'id': id,
|
||||
'mimeType': mimeType,
|
||||
'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,
|
||||
'xmpSubjects': xmpSubjects,
|
||||
'xmpTitle': xmpTitle,
|
||||
|
|
|
@ -169,6 +169,7 @@ class AIcons {
|
|||
// thumbnail overlay
|
||||
static const animated = Icons.slideshow;
|
||||
static const geo = Icons.language_outlined;
|
||||
static const hdr = Icons.hdr_on_outlined;
|
||||
static const motionPhoto = Icons.motion_photos_on_outlined;
|
||||
static const multiPage = Icons.burst_mode_outlined;
|
||||
static const panorama = Icons.vrpano_outlined;
|
||||
|
|
|
@ -98,8 +98,9 @@ class GridThemeData {
|
|||
if (entry.is360) const PanoramaIcon(),
|
||||
],
|
||||
if (entry.isMultiPage) ...[
|
||||
if (entry.isHdr) const HdrIcon(),
|
||||
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.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 {
|
||||
const PanoramaIcon({super.key});
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
|
|||
TypeFilter.panorama,
|
||||
TypeFilter.sphericalVideo,
|
||||
TypeFilter.geotiff,
|
||||
TypeFilter.hdr,
|
||||
TypeFilter.raw,
|
||||
MimeFilter(MimeTypes.svg),
|
||||
];
|
||||
|
|
|
@ -131,9 +131,10 @@ class ViewerDebugPage extends StatelessWidget {
|
|||
'isSvg': '${entry.isSvg}',
|
||||
'isVideo': '${entry.isVideo}',
|
||||
'isCatalogued': '${entry.isCatalogued}',
|
||||
'is360': '${entry.is360}',
|
||||
'isAnimated': '${entry.isAnimated}',
|
||||
'isGeotiff': '${entry.isGeotiff}',
|
||||
'is360': '${entry.is360}',
|
||||
'isHdr': '${entry.isHdr}',
|
||||
'isMultiPage': '${entry.isMultiPage}',
|
||||
'isMotionPhoto': '${entry.isMotionPhoto}',
|
||||
'canEdit': '${entry.canEdit}',
|
||||
|
|
|
@ -122,6 +122,7 @@ class _BasicSectionState extends State<BasicSection> {
|
|||
MimeFilter(entry.mimeType),
|
||||
if (entry.isAnimated) TypeFilter.animated,
|
||||
if (entry.isGeotiff) TypeFilter.geotiff,
|
||||
if (entry.isHdr) TypeFilter.hdr,
|
||||
if (entry.isMotionPhoto) TypeFilter.motionPhoto,
|
||||
if (entry.isRaw) TypeFilter.raw,
|
||||
if (entry.isImage && entry.is360) TypeFilter.panorama,
|
||||
|
|
Loading…
Reference in a new issue