info: format XMP keys and some values (enums in Exif/Photoshop/TIFF)
This commit is contained in:
parent
f899f563e8
commit
ca670e4ee9
8 changed files with 957 additions and 132 deletions
639
lib/ref/exif.dart
Normal file
639
lib/ref/exif.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
75
lib/widgets/fullscreen/info/metadata/xmp_ns/exif.dart
Normal file
75
lib/widgets/fullscreen/info/metadata/xmp_ns/exif.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
25
lib/widgets/fullscreen/info/metadata/xmp_ns/iptc.dart
Normal file
25
lib/widgets/fullscreen/info/metadata/xmp_ns/iptc.dart
Normal 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,
|
||||
),
|
||||
];
|
||||
}
|
44
lib/widgets/fullscreen/info/metadata/xmp_ns/photoshop.dart
Normal file
44
lib/widgets/fullscreen/info/metadata/xmp_ns/photoshop.dart
Normal 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)';
|
||||
}
|
||||
}
|
||||
}
|
29
lib/widgets/fullscreen/info/metadata/xmp_ns/tiff.dart
Normal file
29
lib/widgets/fullscreen/info/metadata/xmp_ns/tiff.dart
Normal 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;
|
||||
}
|
||||
}
|
79
lib/widgets/fullscreen/info/metadata/xmp_ns/xmp.dart
Normal file
79
lib/widgets/fullscreen/info/metadata/xmp_ns/xmp.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in a new issue