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,