diff --git a/CHANGELOG.md b/CHANGELOG.md
index 341b43394..f37ccc550 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+- Cataloguing: detect/filter `Ultra HDR`
+- Info: show metadata from JPEG MPF
+
### Changed
- upgraded Flutter to stable v3.16.3
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
index e04dc7241..d4845d5af 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt
@@ -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
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
index e593dd16c..2744a70aa 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt
@@ -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
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt
index 6bbd418ff..3ee6f18fc 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt
@@ -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().apply {
addAll(JpegMetadataReader.ALL_READERS.filter { it !is XmpReader })
add(SafeXmpReader())
+ add(MpfReader())
}
val metadata = com.drew.metadata.Metadata()
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/mpf/MpEntryDirectory.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/mpf/MpEntryDirectory.kt
new file mode 100644
index 000000000..7b8dcccbd
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/mpf/MpEntryDirectory.kt
@@ -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 {
+ return HashMap().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 {
+ return _tagNameMap
+ }
+
+ companion object {
+ private val _tagNameMap = HashMap()
+ }
+}
+
+class MpEntryDescriptor(directory: MpEntryDirectory?) : TagDescriptor(directory) {
+ fun getFlagsDescription(flags: Int): String {
+ val flagStrings = ArrayList().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)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/mpf/MpfDirectory.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/mpf/MpfDirectory.kt
new file mode 100644
index 000000000..53904b56f
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/mpf/MpfDirectory.kt
@@ -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 {
+ 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().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(directory)
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/mpf/MpfReader.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/mpf/MpfReader.kt
new file mode 100644
index 000000000..423ff56d4
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/mpf/MpfReader.kt
@@ -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 {
+ return listOf(JpegSegmentType.APP2)
+ }
+
+ override fun readJpegSegments(segments: Iterable, 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.. 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.. 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()
+ private const val PREAMBLE = "MPF"
+ }
+}
diff --git a/lib/model/entry/extensions/multipage.dart b/lib/model/entry/extensions/multipage.dart
index 2352b27f0..319237d63 100644
--- a/lib/model/entry/extensions/multipage.dart
+++ b/lib/model/entry/extensions/multipage.dart
@@ -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 patterns) {
final key = BurstPatterns.getKeyForName(filenameWithoutExtension, patterns);
diff --git a/lib/model/entry/extensions/props.dart b/lib/model/entry/extensions/props.dart
index 16e71676a..cae217b57 100644
--- a/lib/model/entry/extensions/props.dart
+++ b/lib/model/entry/extensions/props.dart
@@ -74,6 +74,8 @@ extension ExtraAvesEntryProps on AvesEntry {
bool get is360 => catalogMetadata?.is360 ?? false;
+ bool get isHdr => (catalogMetadata?.hasHdrGainMap ?? false);
+
// trash
bool get isExpiredTrash {
diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart
index a095887dc..9b4e331e1 100644
--- a/lib/model/filters/type.dart
+++ b/lib/model/filters/type.dart
@@ -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,
};
}
diff --git a/lib/model/metadata/catalog.dart b/lib/model/metadata/catalog.dart
index 0d389eae2..2e059f6ec 100644
--- a/lib/model/metadata/catalog.dart
+++ b/lib/model/metadata/catalog.dart
@@ -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,
diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart
index ca9191812..3a53625c6 100644
--- a/lib/theme/icons.dart
+++ b/lib/theme/icons.dart
@@ -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;
diff --git a/lib/widgets/common/grid/theme.dart b/lib/widgets/common/grid/theme.dart
index 134308a0b..e691afaa5 100644
--- a/lib/widgets/common/grid/theme.dart
+++ b/lib/widgets/common/grid/theme.dart
@@ -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),
diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart
index f108ca0fe..504ba9fa9 100644
--- a/lib/widgets/common/identity/aves_icons.dart
+++ b/lib/widgets/common/identity/aves_icons.dart
@@ -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});
diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart
index 6e37f4eb9..955a55904 100644
--- a/lib/widgets/search/search_delegate.dart
+++ b/lib/widgets/search/search_delegate.dart
@@ -58,6 +58,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
TypeFilter.panorama,
TypeFilter.sphericalVideo,
TypeFilter.geotiff,
+ TypeFilter.hdr,
TypeFilter.raw,
MimeFilter(MimeTypes.svg),
];
diff --git a/lib/widgets/viewer/debug/debug_page.dart b/lib/widgets/viewer/debug/debug_page.dart
index 833bda5c2..7dc76d47d 100644
--- a/lib/widgets/viewer/debug/debug_page.dart
+++ b/lib/widgets/viewer/debug/debug_page.dart
@@ -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}',
diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart
index ae9726151..f3420fc6a 100644
--- a/lib/widgets/viewer/info/basic_section.dart
+++ b/lib/widgets/viewer/info/basic_section.dart
@@ -122,6 +122,7 @@ class _BasicSectionState extends State {
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,