#838 detect/filter Ultra HDR;

info: show MPF metadata
This commit is contained in:
Thibault Deckers 2023-12-09 15:40:57 +01:00
parent 31cd54879d
commit 33ae168eda
17 changed files with 309 additions and 13 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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"
}
}

View file

@ -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);

View file

@ -74,6 +74,8 @@ extension ExtraAvesEntryProps on AvesEntry {
bool get is360 => catalogMetadata?.is360 ?? false;
bool get isHdr => (catalogMetadata?.hasHdrGainMap ?? false);
// trash
bool get isExpiredTrash {

View file

@ -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,
};
}

View file

@ -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,

View file

@ -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;

View file

@ -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),

View file

@ -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});

View file

@ -58,6 +58,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
TypeFilter.panorama,
TypeFilter.sphericalVideo,
TypeFilter.geotiff,
TypeFilter.hdr,
TypeFilter.raw,
MimeFilter(MimeTypes.svg),
];

View file

@ -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}',

View file

@ -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,