info: improved listing for directories with same name

This commit is contained in:
Thibault Deckers 2021-10-09 20:33:10 +09:00
parent b84fde14af
commit 34f8b9cef9
2 changed files with 146 additions and 99 deletions

View file

@ -52,10 +52,10 @@ import deckers.thibault.aves.metadata.XMP.isPanorama
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern
import deckers.thibault.aves.utils.StorageUtils
@ -104,113 +104,125 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java)
val uuidDirCount = HashMap<String, Int>()
for (dir in metadata.directories.filter {
val dirByName = metadata.directories.filter {
it.tagCount > 0
&& it !is FileTypeDirectory
&& it !is AviDirectory
}) {
// directory name
var dirName = dir.name
if (dir is Mp4UuidBoxDirectory) {
val uuid = dir.getString(Mp4UuidBoxDirectory.TAG_UUID).substringBefore('-')
dirName += " $uuid"
val count = uuidDirCount[uuid] ?: 0
uuidDirCount[uuid] = count + 1
if (count > 0) {
dirName += " ($count)"
}
}
}.groupBy { dir -> dir.name }
for (dirEntry in dirByName) {
val baseDirName = dirEntry.key
// exclude directories known to be redundant with info derived on the Dart side
// they are excluded by name instead of runtime type because excluding `Mp4Directory`
// would also exclude derived directories, such as `Mp4UuidBoxDirectory`
if (allMetadataRedundantDirNames.contains(dirName)) continue
if (allMetadataRedundantDirNames.contains(baseDirName)) continue
// optional parent to distinguish child directories of the same type
dir.parent?.name?.let { dirName = "$it/$dirName" }
val sameNameDirs = dirEntry.value
val sameNameDirCount = sameNameDirs.size
for (dirIndex in 0 until sameNameDirCount) {
val dir = sameNameDirs[dirIndex]
val dirMap = metadataMap[dirName] ?: HashMap()
metadataMap[dirName] = dirMap
// directory name
var thisDirName = baseDirName
if (dir is Mp4UuidBoxDirectory) {
val uuid = dir.getString(Mp4UuidBoxDirectory.TAG_UUID).substringBefore('-')
thisDirName += " $uuid"
// tags
val tags = dir.tags
if (mimeType == MimeTypes.TIFF && (dir is ExifIFD0Directory || dir is ExifThumbnailDirectory)) {
fun tagMapper(it: Tag): Pair<String, String> {
val name = if (it.hasTagName()) {
it.tagName
val count = uuidDirCount[uuid] ?: 0
uuidDirCount[uuid] = count + 1
if (count > 0) {
thisDirName += " ($count)"
}
} else if (sameNameDirCount > 1 && !allMetadataMergeableDirNames.contains(baseDirName)) {
// optional count for multiple directories of the same type
thisDirName = "$thisDirName[${dirIndex + 1}]"
}
// optional parent to distinguish child directories of the same type
dir.parent?.name?.let { thisDirName = "$it/$thisDirName" }
val dirMap = metadataMap[thisDirName] ?: HashMap()
metadataMap[thisDirName] = dirMap
// tags
val tags = dir.tags
if (mimeType == MimeTypes.TIFF && (dir is ExifIFD0Directory || dir is ExifThumbnailDirectory)) {
fun tagMapper(it: Tag): Pair<String, String> {
val name = if (it.hasTagName()) {
it.tagName
} else {
TiffTags.getTagName(it.tagType) ?: it.tagName
}
return Pair(name, it.description)
}
if (dir is ExifIFD0Directory && dir.isGeoTiff()) {
// split GeoTIFF tags in their own directory
val byGeoTiff = tags.groupBy { TiffTags.isGeoTiffTag(it.tagType) }
metadataMap["GeoTIFF"] = HashMap<String, String>().apply {
byGeoTiff[true]?.map { tagMapper(it) }?.let { putAll(it) }
}
byGeoTiff[false]?.map { tagMapper(it) }?.let { dirMap.putAll(it) }
} else {
TiffTags.getTagName(it.tagType) ?: it.tagName
dirMap.putAll(tags.map { tagMapper(it) })
}
return Pair(name, it.description)
}
if (dir is ExifIFD0Directory && dir.isGeoTiff()) {
// split GeoTIFF tags in their own directory
val byGeoTiff = tags.groupBy { TiffTags.isGeoTiffTag(it.tagType) }
metadataMap["GeoTIFF"] = HashMap<String, String>().apply {
byGeoTiff[true]?.map { tagMapper(it) }?.let { putAll(it) }
}
byGeoTiff[false]?.map { tagMapper(it) }?.let { dirMap.putAll(it) }
} else {
dirMap.putAll(tags.map { tagMapper(it) })
dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
}
} else {
dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
}
if (dir is XmpDirectory) {
try {
for (prop in dir.xmpMeta) {
if (prop is XMPPropertyInfo) {
val path = prop.path
if (path?.isNotEmpty() == true) {
val value = if (XMP.isDataPath(path)) "[skipped]" else prop.value
if (value?.isNotEmpty() == true) {
dirMap[path] = value
if (dir is XmpDirectory) {
try {
for (prop in dir.xmpMeta) {
if (prop is XMPPropertyInfo) {
val path = prop.path
if (path?.isNotEmpty() == true) {
val value = if (XMP.isDataPath(path)) "[skipped]" else prop.value
if (value?.isNotEmpty() == true) {
dirMap[path] = value
}
}
}
}
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
}
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
// remove this stat as it is not actual XMP data
dirMap.remove(dir.getTagName(XmpDirectory.TAG_XMP_VALUE_COUNT))
}
// remove this stat as it is not actual XMP data
dirMap.remove(dir.getTagName(XmpDirectory.TAG_XMP_VALUE_COUNT))
}
if (dir is Mp4UuidBoxDirectory) {
when (dir.getString(Mp4UuidBoxDirectory.TAG_UUID)) {
GSpherical.SPHERICAL_VIDEO_V1_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
metadataMap.remove(dirName)
}
QuickTimeMetadata.PROF_UUID -> {
// redundant with info derived on the Dart side
metadataMap.remove(dirName)
}
QuickTimeMetadata.USMT_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
if (blocks.isNotEmpty()) {
metadataMap.remove(dirName)
dirName = "QuickTime User Media"
val usmt = metadataMap[dirName] ?: HashMap()
metadataMap[dirName] = usmt
if (dir is Mp4UuidBoxDirectory) {
when (dir.getString(Mp4UuidBoxDirectory.TAG_UUID)) {
GSpherical.SPHERICAL_VIDEO_V1_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
metadataMap.remove(thisDirName)
}
QuickTimeMetadata.PROF_UUID -> {
// redundant with info derived on the Dart side
metadataMap.remove(thisDirName)
}
QuickTimeMetadata.USMT_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
if (blocks.isNotEmpty()) {
metadataMap.remove(thisDirName)
thisDirName = "QuickTime User Media"
val usmt = metadataMap[thisDirName] ?: HashMap()
metadataMap[thisDirName] = usmt
blocks.forEach {
var key = it.type
var value = it.value
val language = it.language
blocks.forEach {
var key = it.type
var value = it.value
val language = it.language
var i = 0
while (usmt.containsKey(key)) {
key = it.type + " (${++i})"
var i = 0
while (usmt.containsKey(key)) {
key = it.type + " (${++i})"
}
if (language != "und") {
value += " ($language)"
}
usmt[key] = value
}
if (language != "und") {
value += " ($language)"
}
usmt[key] = value
}
}
}
@ -767,6 +779,16 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
"QuickTime Sound",
"QuickTime Video",
)
private val allMetadataMergeableDirNames = setOf(
"Exif SubIFD",
"GIF Control",
"GIF Image",
"HEIF",
"ICC Profile",
"IPTC",
"WebP",
"XMP",
)
// catalog metadata
private const val KEY_MIME_TYPE = "mimeType"

View file

@ -40,9 +40,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
ValueNotifier<Map<String, MetadataDirectory>> get metadataNotifier => widget.metadataNotifier;
// directory names may contain the name of their parent directory
// if so, they are separated by this character
static const parentChildSeparator = '/';
// directory names may contain the name of their parent directory (as prefix + '/')
// directory names may contain an index (as suffix in '[]')
static final directoryNamePattern = RegExp(r'^((?<parent>.*?)/)?(?<name>.*?)(\[(?<index>\d+)\])?$');
@override
void initState() {
@ -138,14 +138,27 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
var directoryName = dirKV.key as String;
String? parent;
final parts = directoryName.split(parentChildSeparator);
if (parts.length > 1) {
parent = parts[0];
directoryName = parts[1];
int? index;
final match = directoryNamePattern.firstMatch(directoryName);
if (match != null) {
parent = match.namedGroup('parent');
final nameMatch = match.namedGroup('name');
if (nameMatch != null) {
directoryName = nameMatch;
}
final indexMatch = match.namedGroup('index');
if (indexMatch != null) {
index = int.tryParse(indexMatch);
}
}
final rawTags = dirKV.value as Map;
return MetadataDirectory(directoryName, parent, _toSortedTags(rawTags));
return MetadataDirectory(
directoryName,
_toSortedTags(rawTags),
parent: parent,
index: index,
);
}).toList();
if (entry.isVideo || (entry.mimeType == MimeTypes.heif && entry.isMultiPage)) {
@ -157,6 +170,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) {
title = '${dir.parent}/$title';
}
if (dir.index != null) {
title += ' ${dir.index}';
}
return MapEntry(title, dir);
}).toList()
..sort((a, b) => compareAsciiUpperCase(a.key, b.key));
@ -171,7 +187,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
final formattedMediaTags = VideoMetadataFormatter.formatInfo(mediaInfo);
if (formattedMediaTags.isNotEmpty) {
// overwrite generic directory found from the platform side
directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, null, _toSortedTags(formattedMediaTags)));
directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, _toSortedTags(formattedMediaTags)));
}
if (mediaInfo.containsKey(Keys.streams)) {
@ -210,7 +226,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream);
if (formattedStreamTags.isNotEmpty) {
final color = stringToColor(typeText);
directories.add(MetadataDirectory(dirName, null, _toSortedTags(formattedStreamTags), color: color));
directories.add(MetadataDirectory(dirName, _toSortedTags(formattedStreamTags), color: color));
}
}
}
@ -232,7 +248,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
final names = value.whereNotNull().toSet().toList()..sort(compareAsciiUpperCase);
return MapEntry(key, '$count items: ${names.join(', ')}');
});
directories.add(MetadataDirectory('Attachments', null, _toSortedTags(rawTags)));
directories.add(MetadataDirectory('Attachments', _toSortedTags(rawTags)));
}
}
}
@ -254,6 +270,7 @@ class MetadataDirectory {
final String name;
final Color? color;
final String? parent;
final int? index;
final SplayTreeMap<String, String> allTags;
final SplayTreeMap<String, String> tags;
@ -265,14 +282,22 @@ class MetadataDirectory {
const MetadataDirectory(
this.name,
this.parent,
this.allTags, {
SplayTreeMap<String, String>? tags,
this.color,
this.parent,
this.index,
}) : tags = tags ?? allTags;
MetadataDirectory filterKeys(bool Function(String key) testKey) {
final filteredTags = SplayTreeMap.of(Map.fromEntries(allTags.entries.where((kv) => testKey(kv.key))));
return MetadataDirectory(name, parent, tags, tags: filteredTags, color: color);
return MetadataDirectory(
name,
tags,
tags: filteredTags,
color: color,
parent: parent,
index: index,
);
}
}