info: format XMP keys and some values (enums in Exif/Photoshop/TIFF)

This commit is contained in:
Thibault Deckers 2020-12-08 11:19:52 +09:00
parent f899f563e8
commit ca670e4ee9
8 changed files with 957 additions and 132 deletions

639
lib/ref/exif.dart Normal file
View file

@ -0,0 +1,639 @@
class Exif {
static String getColorSpaceDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return 'sRGB';
case 65535:
return 'Uncalibrated';
default:
return 'Unknown ($value)';
}
}
static String getContrastDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Normal';
case 1:
return 'Soft';
case 2:
return 'Hard';
default:
return 'Unknown ($value)';
}
}
// adapted from `metadata-extractor`
static String getCompressionDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return 'Uncompressed';
case 2:
return 'CCITT 1D';
case 3:
return 'T4/Group 3 Fax';
case 4:
return 'T6/Group 4 Fax';
case 5:
return 'LZW';
case 6:
return 'JPEG (old-style)';
case 7:
return 'JPEG';
case 8:
return 'Adobe Deflate';
case 9:
return 'JBIG B&W';
case 10:
return 'JBIG Color';
case 99:
return 'JPEG';
case 262:
return 'Kodak 262';
case 32766:
return 'Next';
case 32767:
return 'Sony ARW Compressed';
case 32769:
return 'Packed RAW';
case 32770:
return 'Samsung SRW Compressed';
case 32771:
return 'CCIRLEW';
case 32772:
return 'Samsung SRW Compressed 2';
case 32773:
return 'PackBits';
case 32809:
return 'Thunderscan';
case 32867:
return 'Kodak KDC Compressed';
case 32895:
return 'IT8CTPAD';
case 32896:
return 'IT8LW';
case 32897:
return 'IT8MP';
case 32898:
return 'IT8BL';
case 32908:
return 'PixarFilm';
case 32909:
return 'PixarLog';
case 32946:
return 'Deflate';
case 32947:
return 'DCS';
case 34661:
return 'JBIG';
case 34676:
return 'SGILog';
case 34677:
return 'SGILog24';
case 34712:
return 'JPEG 2000';
case 34713:
return 'Nikon NEF Compressed';
case 34715:
return 'JBIG2 TIFF FX';
case 34718:
return 'Microsoft Document Imaging (MDI) Binary Level Codec';
case 34719:
return 'Microsoft Document Imaging (MDI) Progressive Transform Codec';
case 34720:
return 'Microsoft Document Imaging (MDI) Vector';
case 34892:
return 'Lossy JPEG';
case 65000:
return 'Kodak DCR Compressed';
case 65535:
return 'Pentax PEF Compressed';
default:
return 'Unknown ($value)';
}
}
static String getCustomRenderedDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Normal process';
case 1:
return 'Custom process';
default:
return 'Unknown ($value)';
}
}
static String getExifVersionDescription(String valueString) {
if (valueString?.length == 4) {
final major = int.tryParse(valueString.substring(0, 2));
final minor = int.tryParse(valueString.substring(2, 4));
if (major != null && minor != null) {
return '$major.$minor';
}
}
return valueString;
}
static String getExposureModeDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Auto exposure';
case 1:
return 'Manual exposure';
case 2:
return 'Auto bracket';
default:
return 'Unknown ($value)';
}
}
static String getExposureProgramDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return 'Manual';
case 2:
return 'Normal program';
case 3:
return 'Aperture priority';
case 4:
return 'Shutter priority';
case 5:
return 'Creative program';
case 6:
return 'Action program';
case 7:
return 'Portrait mode';
case 8:
return 'Landscape mode';
default:
return 'Unknown ($value)';
}
}
// adapted from `metadata-extractor`
static String getFileSourceDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return 'Film Scanner';
case 2:
return 'Reflection Print Scanner';
case 3:
return 'Digital Still Camera (DSC)';
default:
return 'Unknown ($value)';
}
}
static String getLightSourceDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Unknown';
case 1:
return 'Daylight';
case 2:
return 'Fluorescent';
case 3:
return 'Tungsten (Incandescent)';
case 4:
return 'Flash';
case 9:
return 'Fine Weather';
case 10:
return 'Cloudy Weather';
case 11:
return 'Shade';
case 12:
return 'Daylight Fluorescent (D 5700 7100K)';
case 13:
return 'Day White Fluorescent (N 4600 5400K)';
case 14:
return 'Cool White Fluorescent (W 3900 4500K)';
case 15:
return 'White Fluorescent (WW 3200 3700K)';
case 16:
return 'Warm White Fluorescent (WW 2600 - 3250K)';
case 17:
return 'Standard light A';
case 18:
return 'Standard light B';
case 19:
return 'Standard light C';
case 20:
return 'D55';
case 21:
return 'D65';
case 22:
return 'D75';
case 23:
return 'D50';
case 24:
return 'ISO Studio Tungsten';
case 255:
return 'Other';
default:
return 'Unknown ($value)';
}
}
// adapted from `metadata-extractor`
static String getOrientationDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return 'Top, left side (Horizontal / normal)';
case 2:
return 'Top, right side (Mirror horizontal)';
case 3:
return 'Bottom, right side (Rotate 180)';
case 4:
return 'Bottom, left side (Mirror vertical)';
case 5:
return 'Left side, top (Mirror horizontal and rotate 270 CW)';
case 6:
return 'Right side, top (Rotate 90 CW)';
case 7:
return 'Right side, bottom (Mirror horizontal and rotate 90 CW)';
case 8:
return 'Left side, bottom (Rotate 270 CW)';
default:
return 'Unknown ($value)';
}
}
// adapted from `metadata-extractor`
static String getPhotometricInterpretationDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'WhiteIsZero';
case 1:
return 'BlackIsZero';
case 2:
return 'RGB';
case 3:
return 'RGB Palette';
case 4:
return 'Transparency Mask';
case 5:
return 'CMYK';
case 6:
return 'YCbCr';
case 8:
return 'CIELab';
case 9:
return 'ICCLab';
case 10:
return 'ITULab';
case 32803:
return 'Color Filter Array';
case 32844:
return 'Pixar LogL';
case 32845:
return 'Pixar LogLuv';
case 32892:
return 'Linear Raw';
default:
return 'Unknown ($value)';
}
}
// adapted from `metadata-extractor`
static String getPlanarConfigurationDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return 'Chunky (contiguous for each subsampling pixel)';
case 2:
return 'Separate (Y-plane/Cb-plane/Cr-plane format)';
default:
return 'Unknown ($value)';
}
}
// adapted from `metadata-extractor`
static String getResolutionUnitDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return '(No unit)';
case 2:
return 'Inch';
case 3:
return 'cm';
default:
return 'Unknown ($value)';
}
}
static String getGainControlDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'None';
case 1:
return 'Low gain up';
case 2:
return 'High gain up';
case 3:
return 'Low gain down';
case 4:
return 'High gain down';
default:
return 'Unknown ($value)';
}
}
static String getMeteringModeDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Unknown';
case 1:
return 'Average';
case 2:
return 'Center weighted average';
case 3:
return 'Spot';
case 4:
return 'Multi-spot';
case 5:
return 'Pattern';
case 6:
return 'Partial';
case 255:
return 'Other';
default:
return 'Unknown ($value)';
}
}
static String getSaturationDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Normal';
case 1:
return 'Low saturation';
case 2:
return 'High saturation';
default:
return 'Unknown ($value)';
}
}
static String getSceneCaptureTypeDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Standard';
case 1:
return 'Landscape';
case 2:
return 'Portrait';
case 3:
return 'Night scene';
default:
return 'Unknown ($value)';
}
}
static String getSceneTypeDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return 'Directly photographed image';
default:
return 'Unknown ($value)';
}
}
static String getSensingMethodDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return 'Not defined';
case 2:
return 'One-chip colour area sensor';
case 3:
return 'Two-chip colour area sensor';
case 4:
return 'Three-chip colour area sensor';
case 5:
return 'Colour sequential area sensor';
case 7:
return 'Trilinear sensor';
case 8:
return 'Colour sequential linear sensor';
default:
return 'Unknown ($value)';
}
}
static String getSharpnessDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Normal';
case 1:
return 'Soft';
case 2:
return 'Hard';
default:
return 'Unknown ($value)';
}
}
static String getSubjectDistanceRangeDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Unknown';
case 1:
return 'Macro';
case 2:
return 'Close view';
case 3:
return 'Distant view';
default:
return 'Unknown ($value)';
}
}
static String getWhiteBalanceDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Auto';
case 1:
return 'Manual';
default:
return 'Unknown ($value)';
}
}
static String getYCbCrPositioningDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 1:
return 'Centered';
case 2:
return 'Co-sited';
default:
return 'Unknown ($value)';
}
}
// Flash
static String getFlashModeDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Unknown';
case 1:
return 'Compulsory flash firing';
case 2:
return 'Compulsory flash suppression';
case 3:
return 'Auto mode';
default:
return 'Unknown ($value)';
}
}
static String getFlashReturnDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'No strobe return detection';
case 2:
return 'Strobe return light not detected';
case 3:
return 'Strobe return light detected';
default:
return 'Unknown ($value)';
}
}
// GPS
static String getGPSAltitudeRefDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Above sea level';
case 1:
return 'Below sea level';
default:
return 'Unknown ($value)';
}
}
static String getGPSDifferentialDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Without correction';
case 1:
return 'Correction applied';
default:
return 'Unknown ($value)';
}
}
static String getGPSDirectionRefDescription(String value) {
switch (value) {
case 'T':
return 'True direction';
case 'M':
return 'Magnetic direction';
default:
return 'Unknown ($value)';
}
}
static String getGPSMeasureModeDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 2:
return 'Two-dimensional measurement';
case 3:
return 'Three-dimensional measurement';
default:
return 'Unknown ($value)';
}
}
static String getGPSDestDistanceRefDescription(String value) {
switch (value) {
case 'K':
return 'kilometers';
case 'M':
return 'miles';
case 'N':
return 'knots';
default:
return 'Unknown ($value)';
}
}
static String getGPSSpeedRefDescription(String value) {
switch (value) {
case 'K':
return 'kilometers per hour';
case 'M':
return 'miles per hour';
case 'N':
return 'knots';
default:
return 'Unknown ($value)';
}
}
static String getGPSStatusDescription(String value) {
switch (value) {
case 'A':
return 'Measurement in progress';
case 'V':
return 'Measurement is interoperability';
default:
return 'Unknown ($value)';
}
}
}

View file

@ -3,11 +3,42 @@ import 'package:aves/ref/xmp.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/identity/highlight_title.dart';
import 'package:aves/widgets/fullscreen/info/common.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_structs.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class XmpProp {
final String path, value;
final String displayKey;
static final sentenceCaseStep1 = RegExp(r'([A-Z][a-z]|\[)');
static final sentenceCaseStep2 = RegExp(r'([a-z])([A-Z])');
XmpProp(this.path, this.value) : displayKey = formatKey(path);
bool get isOpenable => XMP.dataProps.contains(path);
static String formatKey(String propPath) {
return propPath.splitMapJoin(XMP.structFieldSeparator,
onMatch: (match) => ' ${match.group(0)} ',
onNonMatch: (s) {
// strip namespace
var key = s.split(XMP.propNamespaceSeparator).last;
// uppercase first letter
key = key.replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase());
// sentence case
return key.replaceAllMapped(sentenceCaseStep1, (m) => ' ${m.group(1)}').replaceAllMapped(sentenceCaseStep2, (m) => '${m.group(1)} ${m.group(2)}').trim();
});
}
@override
String toString() {
return '$runtimeType#${shortHash(this)}{path=$path, value=$value}';
}
}
class XmpNamespace {
final String namespace;
@ -16,34 +47,32 @@ class XmpNamespace {
String get displayTitle => XMP.namespaces[namespace] ?? namespace;
List<Widget> buildNamespaceSection({
@required List<MapEntry<String, String>> props,
@required List<MapEntry<String, String>> rawProps,
@required void Function(String propPath) openEmbeddedData,
}) {
final linkHandlers = <String, InfoLinkHandler>{};
final entries = props
.map((prop) {
final propPath = prop.key;
final value = formatValue(prop.value);
if (extractData(propPath, value)) return null;
final props = rawProps
.map((kv) {
final prop = XmpProp(kv.key, kv.value);
if (extractData(prop)) return null;
final displayKey = _formatKey(propPath);
if (XMP.dataProps.contains(propPath)) {
if (prop.isOpenable) {
linkHandlers.putIfAbsent(
displayKey,
() => InfoLinkHandler(linkText: 'Open', onTap: () => openEmbeddedData(propPath)),
prop.displayKey,
() => InfoLinkHandler(linkText: 'Open', onTap: () => openEmbeddedData(prop.path)),
);
}
return MapEntry(displayKey, value);
return prop;
})
.where((e) => e != null)
.toList()
..sort((a, b) => compareAsciiUpperCaseNatural(a.key, b.key));
..sort((a, b) => compareAsciiUpperCaseNatural(a.displayKey, b.displayKey));
final content = [
if (entries.isNotEmpty)
if (props.isNotEmpty)
InfoRowGroup(
Map.fromEntries(entries),
Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, formatValue(prop)))),
maxValueLength: Constants.infoGroupMaxValueLength,
linkHandlers: linkHandlers,
),
@ -66,42 +95,33 @@ class XmpNamespace {
: [];
}
String _formatKey(String propPath) {
return propPath.splitMapJoin(XMP.structFieldSeparator, onNonMatch: (s) {
// strip namespace
final key = s.split(XMP.propNamespaceSeparator).last;
// uppercase first letter
return key.replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase());
});
}
bool _extractStruct(String propPath, String value, RegExp pattern, Map<String, String> store) {
final matches = pattern.allMatches(propPath);
bool extractStruct(XmpProp prop, RegExp pattern, Map<String, String> store) {
final matches = pattern.allMatches(prop.path);
if (matches.isEmpty) return false;
final match = matches.first;
final field = _formatKey(match.group(1));
store[field] = value;
final field = XmpProp.formatKey(match.group(1));
store[field] = formatValue(prop);
return true;
}
bool _extractIndexedStruct(String propPath, String value, RegExp pattern, Map<int, Map<String, String>> store) {
final matches = pattern.allMatches(propPath);
bool extractIndexedStruct(XmpProp prop, RegExp pattern, Map<int, Map<String, String>> store) {
final matches = pattern.allMatches(prop.path);
if (matches.isEmpty) return false;
final match = matches.first;
final index = int.parse(match.group(1));
final field = _formatKey(match.group(2));
final field = XmpProp.formatKey(match.group(2));
final fields = store.putIfAbsent(index, () => <String, String>{});
fields[field] = value;
fields[field] = formatValue(prop);
return true;
}
bool extractData(String propPath, String value) => false;
bool extractData(XmpProp prop) => false;
List<Widget> buildFromExtractedData() => [];
String formatValue(String value) => value;
String formatValue(XmpProp prop) => prop.value;
// identity
@ -119,100 +139,3 @@ class XmpNamespace {
return '$runtimeType#${shortHash(this)}{namespace=$namespace}';
}
}
class XmpBasicNamespace extends XmpNamespace {
static const ns = 'xmp';
static final thumbnailsPattern = RegExp(r'xmp:Thumbnails\[(\d+)\]/(.*)');
final thumbnails = <int, Map<String, String>>{};
XmpBasicNamespace() : super(ns);
@override
bool extractData(String propPath, String value) => _extractIndexedStruct(propPath, value, thumbnailsPattern, thumbnails);
@override
List<Widget> buildFromExtractedData() => [
if (thumbnails.isNotEmpty)
XmpStructArrayCard(
title: 'Thumbnail',
structByIndex: thumbnails,
)
];
}
class XmpIptcCoreNamespace extends XmpNamespace {
static const ns = 'Iptc4xmpCore';
static final creatorContactInfoPattern = RegExp(r'Iptc4xmpCore:CreatorContactInfo/(.*)');
final creatorContactInfo = <String, String>{};
XmpIptcCoreNamespace() : super(ns);
@override
bool extractData(String propPath, String value) => _extractStruct(propPath, value, creatorContactInfoPattern, creatorContactInfo);
@override
List<Widget> buildFromExtractedData() => [
if (creatorContactInfo.isNotEmpty)
XmpStructCard(
title: 'Creator Contact Info',
struct: creatorContactInfo,
),
];
}
class XmpMMNamespace extends XmpNamespace {
static const ns = 'xmpMM';
static const didPrefix = 'xmp.did:';
static const iidPrefix = 'xmp.iid:';
static final derivedFromPattern = RegExp(r'xmpMM:DerivedFrom/(.*)');
static final historyPattern = RegExp(r'xmpMM:History\[(\d+)\]/(.*)');
final derivedFrom = <String, String>{};
final history = <int, Map<String, String>>{};
XmpMMNamespace() : super(ns);
@override
bool extractData(String propPath, String value) => _extractStruct(propPath, value, derivedFromPattern, derivedFrom) || _extractIndexedStruct(propPath, value, historyPattern, history);
@override
List<Widget> buildFromExtractedData() => [
if (derivedFrom.isNotEmpty)
XmpStructCard(
title: 'Derived From',
struct: derivedFrom,
),
if (history.isNotEmpty)
XmpStructArrayCard(
title: 'History',
structByIndex: history,
),
];
@override
String formatValue(String value) {
if (value.startsWith(didPrefix)) return value.replaceFirst(didPrefix, '');
if (value.startsWith(iidPrefix)) return value.replaceFirst(iidPrefix, '');
return value;
}
}
class XmpNoteNamespace extends XmpNamespace {
static const ns = 'xmpNote';
// `xmpNote:HasExtendedXMP` is structural and should not be displayed to users
static const hasExtendedXmp = '$ns:HasExtendedXMP';
XmpNoteNamespace() : super(ns);
@override
bool extractData(String propPath, String value) {
return propPath == hasExtendedXmp;
}
}

View file

@ -0,0 +1,75 @@
import 'package:aves/ref/exif.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/exif.md
class XmpExifNamespace extends XmpNamespace {
static const ns = 'exif';
XmpExifNamespace() : super(ns);
@override
String formatValue(XmpProp prop) {
final v = prop.value;
switch (prop.path) {
case 'exif:ColorSpace':
return Exif.getColorSpaceDescription(v);
case 'exif:Contrast':
return Exif.getContrastDescription(v);
case 'exif:CustomRendered':
return Exif.getCustomRenderedDescription(v);
case 'exif:ExifVersion':
case 'exif:FlashpixVersion':
return Exif.getExifVersionDescription(v);
case 'exif:ExposureMode':
return Exif.getExposureModeDescription(v);
case 'exif:ExposureProgram':
return Exif.getExposureProgramDescription(v);
case 'exif:FileSource':
return Exif.getFileSourceDescription(v);
case 'exif:Flash/exif:Mode':
return Exif.getFlashModeDescription(v);
case 'exif:Flash/exif:Return':
return Exif.getFlashReturnDescription(v);
case 'exif:FocalPlaneResolutionUnit':
return Exif.getResolutionUnitDescription(v);
case 'exif:GainControl':
return Exif.getGainControlDescription(v);
case 'exif:LightSource':
return Exif.getLightSourceDescription(v);
case 'exif:MeteringMode':
return Exif.getMeteringModeDescription(v);
case 'exif:Saturation':
return Exif.getSaturationDescription(v);
case 'exif:SceneCaptureType':
return Exif.getSceneCaptureTypeDescription(v);
case 'exif:SceneType':
return Exif.getSceneTypeDescription(v);
case 'exif:SensingMethod':
return Exif.getSensingMethodDescription(v);
case 'exif:Sharpness':
return Exif.getSharpnessDescription(v);
case 'exif:SubjectDistanceRange':
return Exif.getSubjectDistanceRangeDescription(v);
case 'exif:WhiteBalance':
return Exif.getWhiteBalanceDescription(v);
case 'exif:GPSAltitudeRef':
return Exif.getGPSAltitudeRefDescription(v);
case 'exif:GPSDestBearingRef':
case 'exif:GPSImgDirectionRef':
case 'exif:GPSTrackRef':
return Exif.getGPSDirectionRefDescription(v);
case 'exif:GPSDestDistanceRef':
return Exif.getGPSDestDistanceRefDescription(v);
case 'exif:GPSDifferential':
return Exif.getGPSDifferentialDescription(v);
case 'exif:GPSMeasureMode':
return Exif.getGPSMeasureModeDescription(v);
case 'exif:GPSSpeedRef':
return Exif.getGPSSpeedRefDescription(v);
case 'exif:GPSStatus':
return Exif.getGPSStatusDescription(v);
default:
return v;
}
}
}

View file

@ -0,0 +1,25 @@
import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_structs.dart';
import 'package:flutter/material.dart';
class XmpIptcCoreNamespace extends XmpNamespace {
static const ns = 'Iptc4xmpCore';
static final creatorContactInfoPattern = RegExp(r'Iptc4xmpCore:CreatorContactInfo/(.*)');
final creatorContactInfo = <String, String>{};
XmpIptcCoreNamespace() : super(ns);
@override
bool extractData(XmpProp prop) => extractStruct(prop, creatorContactInfoPattern, creatorContactInfo);
@override
List<Widget> buildFromExtractedData() => [
if (creatorContactInfo.isNotEmpty)
XmpStructCard(
title: 'Creator Contact Info',
struct: creatorContactInfo,
),
];
}

View file

@ -0,0 +1,44 @@
// cf photoshop:ColorMode
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/photoshop.md
import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
class XmpPhotoshopNamespace extends XmpNamespace {
static const ns = 'photoshop';
XmpPhotoshopNamespace() : super(ns);
@override
String formatValue(XmpProp prop) {
final value = prop.value;
switch (prop.path) {
case 'photoshop:ColorMode':
return getColorModeDescription(value);
}
return value;
}
static String getColorModeDescription(String valueString) {
final value = int.tryParse(valueString);
if (value == null) return valueString;
switch (value) {
case 0:
return 'Bitmap';
case 1:
return 'Gray scale';
case 2:
return 'Indexed colour';
case 3:
return 'RGB colour';
case 4:
return 'CMYK colour';
case 7:
return 'Multi-channel';
case 8:
return 'Duotone';
case 9:
return 'LAB colour';
default:
return 'Unknown ($value)';
}
}
}

View file

@ -0,0 +1,29 @@
import 'package:aves/ref/exif.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
// cf https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/tiff.md
class XmpTiffNamespace extends XmpNamespace {
static const ns = 'tiff';
XmpTiffNamespace() : super(ns);
@override
String formatValue(XmpProp prop) {
final value = prop.value;
switch (prop.path) {
case 'tiff:Compression':
return Exif.getCompressionDescription(value);
case 'tiff:Orientation':
return Exif.getOrientationDescription(value);
case 'tiff:PhotometricInterpretation':
return Exif.getPhotometricInterpretationDescription(value);
case 'tiff:PlanarConfiguration':
return Exif.getPlanarConfigurationDescription(value);
case 'tiff:ResolutionUnit':
return Exif.getResolutionUnitDescription(value);
case 'tiff:YCbCrPositioning':
return Exif.getYCbCrPositioningDescription(value);
}
return value;
}
}

View file

@ -0,0 +1,79 @@
import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_structs.dart';
import 'package:flutter/material.dart';
class XmpBasicNamespace extends XmpNamespace {
static const ns = 'xmp';
static final thumbnailsPattern = RegExp(r'xmp:Thumbnails\[(\d+)\]/(.*)');
final thumbnails = <int, Map<String, String>>{};
XmpBasicNamespace() : super(ns);
@override
bool extractData(XmpProp prop) => extractIndexedStruct(prop, thumbnailsPattern, thumbnails);
@override
List<Widget> buildFromExtractedData() => [
if (thumbnails.isNotEmpty)
XmpStructArrayCard(
title: 'Thumbnail',
structByIndex: thumbnails,
)
];
}
class XmpMMNamespace extends XmpNamespace {
static const ns = 'xmpMM';
static const didPrefix = 'xmp.did:';
static const iidPrefix = 'xmp.iid:';
static final derivedFromPattern = RegExp(r'xmpMM:DerivedFrom/(.*)');
static final historyPattern = RegExp(r'xmpMM:History\[(\d+)\]/(.*)');
final derivedFrom = <String, String>{};
final history = <int, Map<String, String>>{};
XmpMMNamespace() : super(ns);
@override
bool extractData(XmpProp prop) => extractStruct(prop, derivedFromPattern, derivedFrom) || extractIndexedStruct(prop, historyPattern, history);
@override
List<Widget> buildFromExtractedData() => [
if (derivedFrom.isNotEmpty)
XmpStructCard(
title: 'Derived From',
struct: derivedFrom,
),
if (history.isNotEmpty)
XmpStructArrayCard(
title: 'History',
structByIndex: history,
),
];
@override
String formatValue(XmpProp prop) {
final value = prop.value;
if (value.startsWith(didPrefix)) return value.replaceFirst(didPrefix, '');
if (value.startsWith(iidPrefix)) return value.replaceFirst(iidPrefix, '');
return value;
}
}
class XmpNoteNamespace extends XmpNamespace {
static const ns = 'xmpNote';
// `xmpNote:HasExtendedXMP` is structural and should not be displayed to users
static const hasExtendedXmp = '$ns:HasExtendedXMP';
XmpNoteNamespace() : super(ns);
@override
bool extractData(XmpProp prop) {
return prop.path == hasExtendedXmp;
}
}

View file

@ -12,6 +12,11 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_namespaces.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/exif.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/iptc.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/photoshop.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/tiff.dart';
import 'package:aves/widgets/fullscreen/info/metadata/xmp_ns/xmp.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:pedantic/pedantic.dart';
@ -45,12 +50,18 @@ class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
switch (namespace) {
case XmpBasicNamespace.ns:
return XmpBasicNamespace();
case XmpExifNamespace.ns:
return XmpExifNamespace();
case XmpIptcCoreNamespace.ns:
return XmpIptcCoreNamespace();
case XmpMMNamespace.ns:
return XmpMMNamespace();
case XmpNoteNamespace.ns:
return XmpNoteNamespace();
case XmpPhotoshopNamespace.ns:
return XmpPhotoshopNamespace();
case XmpTiffNamespace.ns:
return XmpTiffNamespace();
default:
return XmpNamespace(namespace);
}
@ -68,7 +79,7 @@ class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
crossAxisAlignment: CrossAxisAlignment.start,
children: sections.entries
.expand((kv) => kv.key.buildNamespaceSection(
props: kv.value,
rawProps: kv.value,
openEmbeddedData: _openEmbeddedData,
))
.toList(),