add exif plugin

This commit is contained in:
otsmr 2026-05-31 01:24:27 +02:00
parent e0c6a9617a
commit 72d9bd6320
59 changed files with 17635 additions and 57 deletions

View file

@ -1,6 +1,7 @@
adaptive_number: ea9178fdd4d82ac45cf0ec966ac870dae661124f adaptive_number: ea9178fdd4d82ac45cf0ec966ac870dae661124f
dots_indicator: 508f5883ac79bdbc10254092de3f28f571d261cd dots_indicator: 508f5883ac79bdbc10254092de3f28f571d261cd
ed25519_edwards: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf ed25519_edwards: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf
exif: bf170d5639f0b6fcb0947060cf8bd7b623df9069
flutter_markdown_plus: dc1185c933fbf9dba559ef6c91586ff1503be3ee flutter_markdown_plus: dc1185c933fbf9dba559ef6c91586ff1503be3ee
flutter_sharing_intent: aa1672f547d6579585fa27df0b28ffa2a2544aaa flutter_sharing_intent: aa1672f547d6579585fa27df0b28ffa2a2544aaa
hand_signature: 1beedb164d093643365b0832277c377353c7464f hand_signature: 1beedb164d093643365b0832277c377353c7464f
@ -18,4 +19,5 @@ qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
qr_flutter: d5e7206396105d643113618290bbcc755d05f492 qr_flutter: d5e7206396105d643113618290bbcc755d05f492
restart_app: 66897cb67e235bab85421647bfae036acb4438cb restart_app: 66897cb67e235bab85421647bfae036acb4438cb
screen_protector: 019c04d622d7b610d2903d3a347edc3ba76a6ed0 screen_protector: 019c04d622d7b610d2903d3a347edc3ba76a6ed0
sprintf: f1e74f2f4c339d983f9d011b4ba1df4ec8b8857c
x25519: ecb1d357714537bba6e276ef45f093846d4beaee x25519: ecb1d357714537bba6e276ef45f093846d4beaee

View file

@ -59,4 +59,11 @@ screen_protector:
flutter_markdown_plus: flutter_markdown_plus:
git: https://github.com/foresightmobile/flutter_markdown_plus.git git: https://github.com/foresightmobile/flutter_markdown_plus.git
exif:
git: https://github.com/bigflood/dartexif.git
dependencies:
sprintf:
git: https://github.com/Naddiseo/dart-sprintf.git

21
exif/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 bigflood
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
exif/lib/exif.dart Normal file
View file

@ -0,0 +1,5 @@
library exif;
export 'src/exif_types.dart';
export 'src/print_exif.dart' show printExifOfBytes;
export 'src/read_exif.dart' show readExifFromBytes, readExifFromFile;

View file

@ -0,0 +1,301 @@
import 'dart:typed_data';
import 'package:exif/src/exifheader.dart';
import 'package:exif/src/field_types.dart';
import 'package:exif/src/makernote_apple.dart';
import 'package:exif/src/makernote_canon.dart';
import 'package:exif/src/makernote_casio.dart';
import 'package:exif/src/makernote_fujifilm.dart';
import 'package:exif/src/makernote_nikon.dart';
import 'package:exif/src/makernote_olympus.dart';
import 'package:exif/src/reader.dart';
import 'package:exif/src/tags_info.dart';
import 'package:exif/src/util.dart';
class DecodeMakerNote {
final Map<String, IfdTagImpl> tags;
final IfdReader file;
void Function(int ifd, String ifdName,
{Map<int, MakerTag>? tagDict, bool relative}) dumpIfdFunc;
DecodeMakerNote(this.tags, this.file, this.dumpIfdFunc);
// deal with MakerNote contained in EXIF IFD
// (Some apps use MakerNote tags but do not use a format for which we
// have a description, do not process these).
void decode() {
final note = tags['EXIF MakerNote'];
if (note == null) {
return;
}
// Some apps use MakerNote tags but do not use a format for which we
// have a description, so just do a raw dump for these.
final make = tags['Image Make']?.tag.printable ?? '';
if (make == '') {
return;
}
_decodeMakerNote(note: note, make: make);
}
// Decode all the camera-specific MakerNote formats
// Note is the data that comprises this MakerNote.
// The MakerNote will likely have pointers in it that point to other
// parts of the file. We'll use this.offset as the starting point for
// most of those pointers, since they are relative to the beginning
// of the file.
// If the MakerNote is in a newer format, it may use relative addressing
// within the MakerNote. In that case we'll use relative addresses for
// the pointers.
// As an aside: it's not just to be annoying that the manufacturers use
// relative offsets. It's so that if the makernote has to be moved by the
// picture software all of the offsets don't have to be adjusted. Overall,
// this is probably the right strategy for makernotes, though the spec is
// ambiguous.
// The spec does not appear to imagine that makernotes would
// follow EXIF format internally. Once they did, it's ambiguous whether
// the offsets should be from the header at the start of all the EXIF info,
// or from the header at the start of the makernote.
void _decodeMakerNote({required IfdTagImpl note, required String make}) {
if (_decodeNikon(note, make)) {
return;
}
if (_decodeOlympus(note, make)) {
return;
}
if (_decodeCasio(note, make)) {
return;
}
if (_decodeFujifilm(note, make)) {
return;
}
if (_decodeApple(note, make)) {
return;
}
if (_decodeCanon(note, make)) {
return;
}
}
bool _decodeNikon(IfdTagImpl note, String make) {
// Nikon
// The maker note usually starts with the word Nikon, followed by the
// type of the makernote (1 or 2, as a short). If the word Nikon is
// not at the start of the makernote, it's probably type 2, since some
// cameras work that way.
if (!make.contains('NIKON')) {
return false;
}
if (listHasPrefix(
note.tag.values.toList(), [78, 105, 107, 111, 110, 0, 1])) {
// Looks like a type 1 Nikon MakerNote
_dumpIfd(note.fieldOffset + 8, tagDict: MakerNoteNikon.tagsOld);
} else if (listHasPrefix(
note.tag.values.toList(), [78, 105, 107, 111, 110, 0, 2])) {
// Looks like a labeled type 2 Nikon MakerNote
if (!listHasPrefix(note.tag.values.toList(), [0, 42], start: 12) &&
!listHasPrefix(note.tag.values.toList(), [42, 0], start: 12)) {
throw const FormatException("Missing marker tag '42' in MakerNote.");
// skip the Makernote label and the TIFF header
}
_dumpIfd(note.fieldOffset + 10 + 8,
tagDict: MakerNoteNikon.tagsNew, relative: true);
} else {
// E99x or D1
// Looks like an unlabeled type 2 Nikon MakerNote
_dumpIfd(note.fieldOffset, tagDict: MakerNoteNikon.tagsNew);
}
return true;
}
bool _decodeOlympus(IfdTagImpl note, String make) {
if (make.startsWith('OLYMPUS')) {
_dumpIfd(note.fieldOffset + 8, tagDict: MakerNoteOlympus.tags);
// TODO
//for i in (('MakerNote Tag 0x2020', makernote.OLYMPUS_TAG_0x2020),):
// this.decode_olympus_tag(tags[i[0]].values, i[1])
//return
return true;
}
return false;
}
bool _decodeCasio(IfdTagImpl note, String make) {
if (make.contains('CASIO') || make.contains('Casio')) {
_dumpIfd(note.fieldOffset, tagDict: MakerNoteCasio.tags);
return true;
}
return false;
}
bool _decodeFujifilm(IfdTagImpl note, String make) {
if (make != 'FUJIFILM') {
return false;
}
// bug: everything else is "Motorola" endian, but the MakerNote
// is "Intel" endian
const endian = Endian.little;
// bug: IFD offsets are from beginning of MakerNote, not
// beginning of file header
final newBaseOffset = file.baseOffset + note.fieldOffset;
// process note with bogus values (note is actually at offset 12)
_dumpIfd2(12,
tagDict: MakerNoteFujifilm.tags,
baseOffset: newBaseOffset,
endian: endian);
return true;
}
bool _decodeApple(IfdTagImpl note, String make) {
if (!_makerIsApple(note, make)) {
return false;
}
final newBaseOffset = file.baseOffset + note.fieldOffset + 14;
_dumpIfd2(0,
tagDict: MakerNoteApple.tags,
baseOffset: newBaseOffset,
endian: file.endian);
return true;
}
bool _makerIsApple(IfdTagImpl note, String make) =>
make == 'Apple' &&
listHasPrefix(note.tag.values.toList(),
[65, 112, 112, 108, 101, 32, 105, 79, 83, 0]);
bool _decodeCanon(IfdTagImpl note, String make) {
if (make != 'Canon') {
return false;
}
_dumpIfd(note.fieldOffset, tagDict: MakerNoteCanon.tags);
MakerNoteCanon.tagsXxx.forEach((name, makerTags) {
final tag = tags[name];
if (tag != null) {
_canonDecodeTag(
tag.tag.values.toList().whereType<int>().toList(), makerTags);
tags.remove(name);
}
});
final cannonTag = tags[MakerNoteCanon.cameraInfoTagName];
if (cannonTag != null) {
_canonDecodeCameraInfo(cannonTag);
tags.remove(MakerNoteCanon.cameraInfoTagName);
}
return true;
}
// TODO Decode Olympus MakerNote tag based on offset within tag
// void _olympus_decode_tag(List<int> value, mn_tags) {}
// Decode Canon MakerNote tag based on offset within tag.
// See http://www.burren.cx/david/canon.html by David Burren
void _canonDecodeTag(List<int> value, Map<int, MakerTag> mnTags) {
for (int i = 1; i < value.length; i++) {
final tag = mnTags[i] ?? MakerTag.make('Unknown');
final name = tag.name;
String val;
if (tag.map != null) {
val = tag.map![value[i]] ?? 'Unknown';
} else {
val = value[i].toString();
}
// it's not a real IFD Tag but we fake one to make everybody
// happy. this will have a "proprietary" type
tags['MakerNote $name'] = IfdTagImpl(printable: val);
}
}
// Decode the variable length encoded camera info section.
void _canonDecodeCameraInfo(IfdTagImpl cameraInfoTag) {
final modelTag = tags['Image Model'];
if (modelTag == null) {
return;
}
final model = modelTag.tag.values.toString();
Map<int, CameraInfo>? cameraInfoTags;
for (final modelNameRegExp in MakerNoteCanon.cameraInfoModelMap.keys) {
final tagDesc = MakerNoteCanon.cameraInfoModelMap[modelNameRegExp];
if (RegExp(modelNameRegExp).hasMatch(model)) {
cameraInfoTags = tagDesc;
break;
}
}
if (cameraInfoTags == null) {
return;
}
// We are assuming here that these are all unsigned bytes (Byte or
// Unknown)
if (cameraInfoTag.fieldType != FieldType.byte &&
cameraInfoTag.fieldType != FieldType.undefined) {
return;
}
if (cameraInfoTag.tag.values is! List<int>) {
return;
}
final cameraInfo = cameraInfoTag.tag.values as List<int>;
// Look for each data value and decode it appropriately.
for (final entry in cameraInfoTags.entries) {
final offset = entry.key;
final tag = entry.value;
final tagSize = tag.tagSize;
if (cameraInfo.length < offset + tagSize) {
continue;
}
final packedTagValue = cameraInfo.sublist(offset, offset + tagSize);
final tagValue = s2nLittleEndian(packedTagValue);
tags['MakerNote ${tag.tagName}'] =
IfdTagImpl(printable: tag.function(tagValue));
}
}
void _dumpIfd(int ifd,
{required Map<int, MakerTag> tagDict, bool relative = false}) {
dumpIfdFunc(ifd, 'MakerNote', tagDict: tagDict, relative: relative);
}
void _dumpIfd2(int ifd,
{required Map<int, MakerTag>? tagDict,
bool relative = false,
required int baseOffset,
required Endian endian}) {
final originalEndian = file.endian;
final originalOffset = file.baseOffset;
file.endian = endian;
file.baseOffset = baseOffset;
dumpIfdFunc(ifd, 'MakerNote', tagDict: tagDict, relative: relative);
file.endian = originalEndian;
file.baseOffset = originalOffset;
}
}

View file

@ -0,0 +1,116 @@
import 'dart:typed_data';
import 'package:exif/src/exifheader.dart';
import 'package:exif/src/field_types.dart';
import 'package:exif/src/reader.dart';
class Thumbnail {
final Map<String, IfdTagImpl> tags;
final IfdReader file;
Thumbnail(this.tags, this.file);
// Extract uncompressed TIFF thumbnail.
// Take advantage of the pre-existing layout in the thumbnail IFD as
// much as possible
List<int>? extractTiffThumbnail(int thumbIfd) {
final thumb = tags['Thumbnail Compression'];
if (thumb == null || thumb.tag.printable != 'Uncompressed TIFF') {
return null;
}
List<int> tiff;
int stripOff = 0;
int stripLen = 0;
final entries = file.readInt(thumbIfd, 2);
// this is header plus offset to IFD ...
if (file.endian == Endian.big) {
tiff = 'MM\x00*\x00\x00\x00\x08'.codeUnits;
} else {
tiff = 'II*\x00\x08\x00\x00\x00'.codeUnits;
// ... plus thumbnail IFD data plus a null "next IFD" pointer
}
tiff.addAll(file.readSlice(thumbIfd, entries * 12 + 2));
tiff.addAll([0, 0, 0, 0]);
// fix up large value offset pointers into data area
for (int i = 0; i < entries; i++) {
final entry = thumbIfd + 2 + 12 * i;
final tag = file.readInt(entry, 2);
final fieldType = file.readInt(entry + 2, 2);
final typeLength = fieldTypes[fieldType].length;
final count = file.readInt(entry + 4, 4);
final oldOffset = file.readInt(entry + 8, 4);
// start of the 4-byte pointer area in entry
final ptr = i * 12 + 18;
// remember strip offsets location
if (tag == 0x0111) {
stripOff = ptr;
stripLen = count * typeLength;
// is it in the data area?
}
if (count * typeLength > 4) {
// update offset pointer (nasty "strings are immutable" crap)
// should be able to say "tiff[ptr:ptr+4]=newOffset"
final tiff0 = tiff;
final newOffset = tiff0.length;
tiff = tiff0.sublist(0, ptr);
tiff.addAll(file.offsetToBytes(newOffset, 4));
tiff.addAll(tiff0.sublist(ptr + 4));
// remember strip offsets location
if (tag == 0x0111) {
stripOff = newOffset;
stripLen = 4;
}
// get original data and store it
tiff.addAll(file.readSlice(oldOffset, count * typeLength));
}
}
// add pixel strips and update strip offset info
final oldOffsets = tags['Thumbnail StripOffsets']?.tag.values.toList();
final oldCounts = tags['Thumbnail StripByteCounts']?.tag.values.toList();
if (oldOffsets == null || oldCounts == null) {
return null;
}
for (int i = 0; i < oldOffsets.length; i++) {
// update offset pointer (more nasty "strings are immutable" crap)
final tiff0 = tiff;
final offset = file.offsetToBytes(tiff0.length, stripLen);
tiff = tiff0.sublist(0, stripOff);
tiff.addAll(offset);
tiff.addAll(tiff0.sublist(stripOff + stripLen));
stripOff += stripLen;
// add pixel strip to end
tiff.addAll(file.readSlice(oldOffsets[i] as int, oldCounts[i] as int));
}
return tiff;
}
// Extract JPEG thumbnail.
// (Thankfully the JPEG data is stored as a unit.)
List<int>? extractJpegThumbnail() {
final thumbFmt = tags['Thumbnail JPEGInterchangeFormat'];
final thumbFmtLen = tags['Thumbnail JPEGInterchangeFormatLength'];
if (thumbFmt != null && thumbFmtLen != null) {
final size = thumbFmtLen.tag.values.firstAsInt();
final values = file.readSlice(thumbFmt.tag.values.firstAsInt(), size);
return values;
}
// Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote
// since it's not allowed in a uncompressed TIFF IFD
final thumbnail = tags['MakerNote JPEGThumbnail'];
if (thumbnail != null) {
final values = file.readSlice(
thumbnail.tag.values.firstAsInt(), thumbnail.fieldLength);
return values;
}
return null;
}
}

View file

@ -0,0 +1,149 @@
import 'dart:typed_data';
class IfdTag {
/// tag ID number
final int tag;
final String tagType;
/// printable version of data
final String printable;
/// list of data items (int(char or number) or Ratio)
final IfdValues values;
IfdTag({
required this.tag,
required this.tagType,
required this.printable,
required this.values,
});
@override
String toString() => printable;
}
abstract class IfdValues {
const IfdValues();
List toList();
int get length;
int firstAsInt();
}
class IfdNone extends IfdValues {
const IfdNone();
@override
List toList() => [];
@override
int get length => 0;
@override
int firstAsInt() => 0;
@override
String toString() => "[]";
}
class IfdRatios extends IfdValues {
final List<Ratio> ratios;
const IfdRatios(this.ratios);
@override
List toList() => ratios;
@override
int get length => ratios.length;
@override
int firstAsInt() => ratios[0].toInt();
@override
String toString() => ratios.toString();
}
class IfdInts extends IfdValues {
final List<int> ints;
const IfdInts(this.ints);
@override
List toList() => ints;
@override
int get length => ints.length;
@override
int firstAsInt() => ints[0];
@override
String toString() => ints.toString();
}
class IfdBytes extends IfdValues {
final Uint8List bytes;
IfdBytes(this.bytes);
IfdBytes.empty() : bytes = Uint8List(0);
IfdBytes.fromList(List<int> list) : bytes = Uint8List.fromList(list);
@override
List toList() => bytes;
@override
int get length => bytes.length;
@override
int firstAsInt() => bytes[0];
@override
String toString() => bytes.toString();
}
/// Ratio object that eventually will be able to reduce itself to lowest
/// common denominator for printing.
class Ratio {
final int numerator;
final int denominator;
factory Ratio(int num, int den) {
if (den < 0) {
num *= -1;
den *= -1;
}
final d = num.gcd(den);
if (d > 1) {
num = num ~/ d;
den = den ~/ d;
}
return Ratio._internal(num, den);
}
Ratio._internal(this.numerator, this.denominator);
@override
String toString() =>
(denominator == 1) ? '$numerator' : '$numerator/$denominator';
int toInt() => numerator ~/ denominator;
double toDouble() => numerator / denominator;
}
class ExifData {
final Map<String, IfdTag> tags;
final List<String> warnings;
const ExifData(this.tags, this.warnings);
ExifData.withWarning(String warning) : this(const {}, [warning]);
}

View file

@ -0,0 +1,173 @@
import 'package:exif/src/exif_thumbnail.dart';
import 'package:exif/src/exif_types.dart';
import 'package:exif/src/field_types.dart';
import 'package:exif/src/reader.dart';
import 'package:exif/src/tags.dart';
import 'package:exif/src/tags_info.dart';
import 'package:exif/src/values_to_printable.dart';
import 'package:sprintf/sprintf.dart' show sprintf;
const defaultStopTag = 'UNDEF';
// To ignore when quick processing
const ignoreTags = [
0x9286, // user comment
0x927C, // MakerNote Tags
0x02BC, // XPM
];
// Eases dealing with tags.
class IfdTagImpl {
final IfdTag tag;
final FieldType fieldType;
// offset of start of field in bytes from beginning of IFD
int fieldOffset;
// length of data field in bytes
int fieldLength;
IfdTagImpl({
this.fieldType = FieldType.proprietary,
this.fieldOffset = 0,
this.fieldLength = 0,
String printable = '',
int tag = -1,
IfdValues values = const IfdNone(),
}) : tag = IfdTag(
tag: tag,
tagType: fieldType.name,
printable: printable,
values: values,
);
}
/// Handle an EXIF header.
class ExifHeader {
bool strict;
bool debug;
bool detailed;
bool truncateTags;
Map<String, IfdTagImpl> tags = {};
List<String> warnings = [];
IfdReader file;
ExifHeader({
required this.file,
required this.strict,
this.debug = false,
this.detailed = true,
this.truncateTags = true,
});
// Return a list of entries in the given IFD.
void dumpIfd(int ifd, String ifdName,
{Map<int, MakerTag>? tagDict, bool relative = false, String? stopTag}) {
stopTag ??= defaultStopTag;
tagDict ??= StandardTags.tags;
// make sure we can process the entries
List<IfdEntry> entries;
try {
entries = file.readIfdEntries(ifd, relative: relative);
} catch (e) {
warnings.add("Possibly corrupted IFD: $ifd");
return;
}
for (final entry in entries) {
// get tag name early to avoid errors, help debug
final MakerTag? tagEntry = tagDict[entry.tag];
String tagName;
if (tagEntry != null) {
tagName = tagEntry.name;
} else {
tagName = sprintf('Tag 0x%04X', [entry.tag]);
}
// ignore certain tags for faster processing
if (detailed || !ignoreTags.contains(entry.tag)) {
processTag(
ifd: ifd,
ifdName: ifdName,
tagEntry: tagEntry,
entry: entry,
tagName: tagName,
relative: relative,
stopTag: stopTag);
if (tagName == stopTag) {
break;
}
}
}
}
void processTag(
{required int ifd,
required String ifdName,
required MakerTag? tagEntry,
required IfdEntry entry,
required String tagName,
required bool relative,
required String? stopTag}) {
// unknown field type
if (!entry.fieldType.isValid) {
if (!strict) {
return;
} else {
throw FormatException(sprintf(
'Unknown type %d in tag 0x%04X', [entry.fieldType, entry.tag]));
}
}
final values = file.readField(entry, tagName: tagName);
// now 'values' is either a string or an array
final printable = ValuesToPrintable.convert(values, entry,
tagEntry: tagEntry, truncateTags: truncateTags);
if (printable.malformed) {
warnings.add("Possibly corrupted field $tagName in $ifdName IFD");
}
final makerTags = tagEntry?.tags;
if (makerTags != null) {
try {
dumpIfd(values.firstAsInt(), makerTags.name,
tagDict: makerTags.tags, stopTag: stopTag);
} on RangeError {
warnings.add('No values found for ${makerTags.name} SubIFD');
}
}
tags['$ifdName $tagName'] = IfdTagImpl(
printable: printable.value,
tag: entry.tag,
fieldType: entry.fieldType,
values: values,
fieldOffset: entry.fieldOffset,
fieldLength: entry.count * entry.fieldType.length);
// var t = tags[ifd_name + ' ' + tag_name];
}
void extractTiffThumbnail(int thumbIfd) {
final values = Thumbnail(tags, file).extractTiffThumbnail(thumbIfd);
if (values != null) {
tags['TIFFThumbnail'] = IfdTagImpl(values: IfdBytes.fromList(values));
}
}
void extractJpegThumbnail() {
final values = Thumbnail(tags, file).extractJpegThumbnail();
if (values != null) {
tags['JPEGThumbnail'] = IfdTagImpl(values: IfdBytes.fromList(values));
}
}
void parseXmp(String xmpString) {
tags['Image ApplicationNotes'] =
IfdTagImpl(printable: xmpString, fieldType: FieldType.byte);
}
}

View file

@ -0,0 +1,65 @@
class FieldType {
final int _value;
final int length;
final String abbr;
final String name;
final bool isValid;
final bool isSigned;
const FieldType(this._value, this.length, this.abbr, this.name,
{this.isValid = true, this.isSigned = false});
factory FieldType.ofValue(int v) {
if (v < 0 || v >= fieldTypes.length) {
return FieldType(v, 0, 'X', 'Unknown', isValid: false);
}
return fieldTypes[v];
}
@override
bool operator ==(Object other) =>
other is FieldType && _value == other._value;
@override
int get hashCode => _value.hashCode;
static const proprietary =
FieldType(0, 0, 'X', 'Proprietary', isValid: false); // no such type
static const byte = FieldType(1, 1, 'B', 'Byte');
static const ascii = FieldType(2, 1, 'A', 'ASCII');
static const short = FieldType(3, 2, 'S', 'Short');
static const long = FieldType(4, 4, 'L', 'Long');
static const ratio = FieldType(5, 8, 'R', 'Ratio');
static const signedByte =
FieldType(6, 1, 'SB', 'Signed Byte', isSigned: true);
static const undefined = FieldType(7, 1, 'U', 'Undefined');
static const signedShort =
FieldType(8, 2, 'SS', 'Signed Short', isSigned: true);
static const signedLong =
FieldType(9, 4, 'SL', 'Signed Long', isSigned: true);
static const signedRatio =
FieldType(10, 8, 'SR', 'Signed Ratio', isSigned: true);
static const f32 =
FieldType(11, 4, 'F32', 'Single-Precision Floating Point (32-bit)');
static const f64 =
FieldType(12, 8, 'F64', 'Double-Precision Floating Point (64-bit)');
static const ifd = FieldType(13, 4, 'L', 'IFD');
}
// field type descriptions as (length, abbreviation, full name) tuples
const fieldTypes = [
FieldType.proprietary, // no such type
FieldType.byte,
FieldType.ascii,
FieldType.short,
FieldType.long,
FieldType.ratio,
FieldType.signedByte,
FieldType.undefined,
FieldType.signedShort,
FieldType.signedLong,
FieldType.signedRatio,
FieldType.f32,
FieldType.f64,
FieldType.ifd,
];

View file

@ -0,0 +1,61 @@
import 'dart:async';
import 'package:exif/src/file_interface_generic.dart'
if (dart.library.html) "package:exif/src/file_interface_html.dart"
if (dart.library.io) 'package:exif/src/file_interface_io.dart';
abstract class FileReader {
static Future<FileReader> fromFile(dynamic file) async {
return createFileReaderFromFile(file);
}
factory FileReader.fromBytes(List<int> bytes) {
return _BytesReader(bytes);
}
int readByteSync();
List<int> readSync(int bytes);
int positionSync();
void setPositionSync(int position);
}
class _BytesReader implements FileReader {
List<int> bytes;
int readPos = 0;
_BytesReader(this.bytes);
@override
int positionSync() {
return readPos;
}
@override
int readByteSync() {
return bytes[readPos++];
}
@override
List<int> readSync(int n) {
final start = readPos;
if (start >= bytes.length) {
return [];
}
var end = readPos + n;
if (end > bytes.length) {
end = bytes.length;
}
final r = bytes.sublist(start, end);
readPos += end - start;
return r;
}
@override
void setPositionSync(int position) {
readPos = position;
}
}

View file

@ -0,0 +1,10 @@
import 'dart:async';
import 'package:exif/src/file_interface.dart';
Future<FileReader> createFileReaderFromFile(dynamic file) async {
if (file is List<int>) {
return FileReader.fromBytes(file);
}
throw UnsupportedError("Can't read file of type: ${file.runtimeType}");
}

View file

@ -0,0 +1,20 @@
import 'dart:async';
import 'dart:html' as dart_html;
import 'dart:typed_data';
import 'package:exif/src/file_interface.dart';
Future<FileReader> createFileReaderFromFile(dynamic file) async {
if (file is dart_html.File) {
final fileReader = dart_html.FileReader();
fileReader.readAsArrayBuffer(file);
await fileReader.onLoad.first;
final data = fileReader.result;
if (data is Uint8List) {
return FileReader.fromBytes(data);
}
} else if (file is List<int>) {
return FileReader.fromBytes(file);
}
throw UnsupportedError("Can't read file of type: ${file.runtimeType}");
}

View file

@ -0,0 +1,42 @@
import 'dart:async';
import 'dart:io';
import 'package:exif/src/file_interface.dart';
class _FileReader implements FileReader {
final RandomAccessFile file;
_FileReader(this.file);
@override
int positionSync() {
return file.positionSync();
}
@override
int readByteSync() {
return file.readByteSync();
}
@override
List<int> readSync(int bytes) {
return file.readSync(bytes).toList(growable: false);
}
@override
void setPositionSync(int position) {
file.setPositionSync(position);
}
}
Future<FileReader> createFileReaderFromFile(dynamic file) async {
if (file is RandomAccessFile) {
return _FileReader(file);
} else if (file is File) {
final data = await file.readAsBytes();
return FileReader.fromBytes(data);
} else if (file is List<int>) {
return FileReader.fromBytes(file);
}
throw UnsupportedError("Can't read file of type: ${file.runtimeType}");
}

276
exif/lib/src/heic.dart Normal file
View file

@ -0,0 +1,276 @@
import 'dart:typed_data';
import 'package:exif/src/file_interface.dart';
import 'package:exif/src/util.dart';
class HeicBox {
final String name;
int version = 0;
int minorVersion = 0;
int itemCount = 0;
int size = 0;
int after = 0;
int pos = 0;
List compat = [];
// this is full of boxes, but not in a predictable order.
Map<String, HeicBox> subs = {};
Map<int, List<List<int>>> locs = {};
HeicBox? exifInfe;
int itemId = 0;
Uint8List? itemType;
Uint8List? itemName;
int itemProtectionIndex = 0;
Uint8List? majorBrand;
int flags = 0;
HeicBox(this.name);
void setFull(int vflags) {
/**
ISO boxes come in 'old' and 'full' variants.
The 'full' variant contains version and flags information.
*/
version = vflags >> 24;
flags = vflags & 0x00ffffff;
}
}
class HEICExifFinder {
final FileReader fileReader;
const HEICExifFinder(this.fileReader);
Uint8List getBytes(int nbytes) {
final bytes = fileReader.readSync(nbytes);
if (bytes.length != nbytes) {
throw Exception("Bad size");
}
return Uint8List.fromList(bytes);
}
int getInt(int size) {
// some fields have variant-sized data.
if (size == 2) {
return ByteData.view(getBytes(2).buffer).getInt16(0);
}
if (size == 4) {
return ByteData.view(getBytes(4).buffer).getInt32(0);
}
if (size == 8) {
return ByteData.view(getBytes(8).buffer).getInt64(0);
}
if (size == 0) {
return 0;
}
throw Exception("Bad size");
}
Uint8List getString() {
final List<Uint8List> read = [];
while (true) {
final char = getBytes(1);
if (listEqual(char, Uint8List.fromList('\x00'.codeUnits))) {
break;
}
read.add(char);
}
return Uint8List.fromList(read.expand((x) => x).toList());
}
List<int> getInt4x2() {
final num = getBytes(1).single;
final num0 = num >> 4;
final num1 = num & 0xf;
return [num0, num1];
}
HeicBox nextBox() {
final pos = fileReader.positionSync();
int size = ByteData.view(getBytes(4).buffer).getInt32(0);
final kind = String.fromCharCodes(getBytes(4));
final box = HeicBox(kind);
if (size == 0) {
// signifies 'to the end of the file', we shouldn't see this.
throw Exception("Unknown error");
}
if (size == 1) {
// 64-bit size follows type.
size = ByteData.view(getBytes(8).buffer).getInt64(0);
box.size = size - 16;
box.after = pos + size;
} else {
box.size = size - 8;
box.after = pos + size;
}
box.pos = fileReader.positionSync();
return box;
}
void _parseFtyp(HeicBox box) {
box.majorBrand = getBytes(4);
box.minorVersion = ByteData.view(getBytes(4).buffer).getInt32(0);
box.compat = [];
int size = box.size - 8;
while (size > 0) {
box.compat.add(getBytes(4));
size -= 4;
}
}
void _parseMeta(HeicBox meta) {
meta.setFull(ByteData.view(getBytes(4).buffer).getInt32(0));
while (fileReader.positionSync() < meta.after) {
final box = nextBox();
final psub = getParser(box);
if (psub != null) {
psub(box);
meta.subs[box.name] = box;
}
// skip any unparsed data
fileReader.setPositionSync(box.after);
}
}
void _parseInfe(HeicBox box) {
box.setFull(ByteData.view(getBytes(4).buffer).getInt32(0));
if (box.version >= 2) {
if (box.version == 2) {
box.itemId = ByteData.view(getBytes(2).buffer).getInt16(0);
} else if (box.version == 3) {
box.itemId = ByteData.view(getBytes(4).buffer).getInt32(0);
}
box.itemProtectionIndex = ByteData.view(getBytes(2).buffer).getInt16(0);
box.itemType = getBytes(4);
box.itemName = getString();
// ignore the rest
}
}
void _parseIinf(HeicBox box) {
box.setFull(ByteData.view(getBytes(4).buffer).getInt32(0));
final count = ByteData.view(getBytes(2).buffer).getInt16(0);
box.exifInfe = null;
for (var i = 0; i < count; i += 1) {
final infe = expectParse('infe');
if (listEqual(infe.itemType, Uint8List.fromList('Exif'.codeUnits))) {
box.exifInfe = infe;
break;
}
}
}
void _parseIloc(HeicBox box) {
box.setFull(ByteData.view(getBytes(4).buffer).getInt32(0));
final size = getInt4x2();
final size2 = getInt4x2();
final offsetSize = size[0];
final lengthSize = size[1];
final baseOffsetSize = size2[0];
final indexSize = size2[1];
if (box.version < 2) {
box.itemCount = ByteData.view(getBytes(2).buffer).getInt16(0);
} else if (box.version == 2) {
box.itemCount = ByteData.view(getBytes(4).buffer).getInt32(0);
} else {
throw Exception("Box version 2, ${box.version}");
}
box.locs = {};
for (var i = 0; i < box.itemCount; i += 1) {
int itemId;
if (box.version < 2) {
itemId = ByteData.view(getBytes(2).buffer).getInt16(0);
} else if (box.version == 2) {
itemId = ByteData.view(getBytes(4).buffer).getInt32(0);
} else {
throw Exception("Box version 2, ${box.version}");
}
if (box.version == 1 || box.version == 2) {
// ignore construction_method
ByteData.view(getBytes(2).buffer).getInt16(0);
}
// ignore data_reference_index
ByteData.view(getBytes(2).buffer).getInt16(0);
final baseOffset = getInt(baseOffsetSize);
final extentCount = ByteData.view(getBytes(2).buffer).getInt16(0);
final List<List<int>> extent = [];
for (var i = 0; i < extentCount; i += 1) {
if ((box.version == 1 || box.version == 2) && indexSize > 0) {
getInt(indexSize);
}
final extentOffset = getInt(offsetSize);
final extentLength = getInt(lengthSize);
extent.add([baseOffset + extentOffset, extentLength]);
}
box.locs[itemId] = extent;
}
}
void Function(HeicBox)? getParser(HeicBox box) {
final defs = {
'ftyp': _parseFtyp,
'meta': _parseMeta,
'infe': _parseInfe,
'iinf': _parseIinf,
'iloc': _parseIloc,
};
return defs[box.name];
}
HeicBox parseBox(HeicBox box) {
final probe = getParser(box);
if (probe == null) {
throw Exception('Unhandled box');
}
probe(box);
// in case anything is left unread
fileReader.setPositionSync(box.after);
return box;
}
HeicBox expectParse(String name) {
while (true) {
final box = nextBox();
if (box.name == name) {
return parseBox(box);
}
fileReader.setPositionSync(box.after);
}
}
List<int> findExif() {
final ftyp = expectParse('ftyp');
assert(listEqual(ftyp.majorBrand, Uint8List.fromList('heic'.codeUnits)) ||
listEqual(ftyp.majorBrand, Uint8List.fromList('avif'.codeUnits)));
assert(ftyp.minorVersion == 0);
final meta = expectParse('meta');
final itemId = meta.subs['iinf']?.exifInfe?.itemId;
if (itemId == null) {
return [];
}
final extents = meta.subs['iloc']?.locs[itemId];
// we expect the Exif data to be in one piece.
if (extents == null || extents.length != 1) {
return [];
}
final int pos = extents[0][0];
// looks like there's a kind of pseudo-box here.
fileReader.setPositionSync(pos);
// the payload of "Exif" item may be start with either
// b'\xFF\xE1\xSS\xSSExif\x00\x00' (with APP1 marker, e.g. Android Q)
// or
// b'Exif\x00\x00' (without APP1 marker, e.g. iOS)
// according to "ISO/IEC 23008-12, 2017-12", both of them are legal
final exifTiffHeaderOffset = ByteData.view(getBytes(4).buffer).getInt32(0);
assert(exifTiffHeaderOffset >= 6);
getBytes(exifTiffHeaderOffset);
// assert self.get(exif_tiff_header_offset)[-6:] == b'Exif\x00\x00'
final offset = fileReader.positionSync();
final endian = fileReader.readSync(1)[0];
return [offset, endian];
}
}

View file

@ -0,0 +1,57 @@
import 'dart:convert';
import 'package:exif/src/file_interface.dart';
class LineReader {
FileReader file;
final List<int> _buffer = [];
bool _endOfFile = false;
LineReader(this.file);
String popString(int n) {
String s;
if (n < _buffer.length) {
s = utf8.decode(_buffer.sublist(0, n));
_buffer.removeRange(0, n);
} else {
s = utf8.decode(_buffer);
_buffer.clear();
}
return s;
}
String readLine() {
int endOfLine = _buffer.indexOf(10);
if (endOfLine >= 0) {
return popString(endOfLine + 1);
}
if (_endOfFile) {
return popString(_buffer.length);
}
while (true) {
final r = file.readSync(1024 * 10);
if (r.isEmpty) {
_endOfFile = true;
endOfLine = -1;
} else {
endOfLine = r.indexOf(10);
_buffer.addAll(r);
if (endOfLine >= 0) {
endOfLine += _buffer.length;
}
}
if (endOfLine >= 0) {
return popString(endOfLine + 1);
} else if (_endOfFile) {
return popString(_buffer.length);
}
}
}
}

View file

@ -0,0 +1,18 @@
import 'package:exif/src/tags_info.dart' show MakerTag, TagsBase;
// Makernote (proprietary) tag definitions for Apple iOS
// Based on version 1.01 of ExifTool -> Image/ExifTool/Apple.pm
// http://owl.phy.queensu.ca/~phil/exiftool/
class MakerNoteApple extends TagsBase {
//static MakerTag _make(String name) => MakerTag.make(name);
static MakerTag _withMap(String name, Map<int, String> map) =>
MakerTag.makeWithMap(name, map);
static final tags = {
0x000a: _withMap('HDRImageType', {
3: 'HDR Image',
4: 'Original Image',
}),
};
}

View file

@ -0,0 +1,676 @@
import 'package:exif/src/tags_info.dart' show MakerTag, TagsBase;
import 'package:sprintf/sprintf.dart' show sprintf;
// Makernote (proprietary) tag definitions for Canon.
// http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html
class MakerNoteCanon extends TagsBase {
static MakerTag _make(String name) => MakerTag.make(name);
static MakerTag _withMap(String name, Map<int, String> map) =>
MakerTag.makeWithMap(name, map);
static final tags = {
0x0003: _make('FlashInfo'),
0x0006: _make('ImageType'),
0x0007: _make('FirmwareVersion'),
0x0008: _make('ImageNumber'),
0x0009: _make('OwnerName'),
0x000c: _make('SerialNumber'),
0x000e: _make('FileLength'),
0x0010: _withMap('ModelID', {
0x1010000: 'PowerShot A30',
0x1040000: 'PowerShot S300 / Digital IXUS 300 / IXY Digital 300',
0x1060000: 'PowerShot A20',
0x1080000: 'PowerShot A10',
0x1090000: 'PowerShot S110 / Digital IXUS v / IXY Digital 200',
0x1100000: 'PowerShot G2',
0x1110000: 'PowerShot S40',
0x1120000: 'PowerShot S30',
0x1130000: 'PowerShot A40',
0x1140000: 'EOS D30',
0x1150000: 'PowerShot A100',
0x1160000: 'PowerShot S200 / Digital IXUS v2 / IXY Digital 200a',
0x1170000: 'PowerShot A200',
0x1180000: 'PowerShot S330 / Digital IXUS 330 / IXY Digital 300a',
0x1190000: 'PowerShot G3',
0x1210000: 'PowerShot S45',
0x1230000: 'PowerShot SD100 / Digital IXUS II / IXY Digital 30',
0x1240000: 'PowerShot S230 / Digital IXUS v3 / IXY Digital 320',
0x1250000: 'PowerShot A70',
0x1260000: 'PowerShot A60',
0x1270000: 'PowerShot S400 / Digital IXUS 400 / IXY Digital 400',
0x1290000: 'PowerShot G5',
0x1300000: 'PowerShot A300',
0x1310000: 'PowerShot S50',
0x1340000: 'PowerShot A80',
0x1350000: 'PowerShot SD10 / Digital IXUS i / IXY Digital L',
0x1360000: 'PowerShot S1 IS',
0x1370000: 'PowerShot Pro1',
0x1380000: 'PowerShot S70',
0x1390000: 'PowerShot S60',
0x1400000: 'PowerShot G6',
0x1410000: 'PowerShot S500 / Digital IXUS 500 / IXY Digital 500',
0x1420000: 'PowerShot A75',
0x1440000: 'PowerShot SD110 / Digital IXUS IIs / IXY Digital 30a',
0x1450000: 'PowerShot A400',
0x1470000: 'PowerShot A310',
0x1490000: 'PowerShot A85',
0x1520000: 'PowerShot S410 / Digital IXUS 430 / IXY Digital 450',
0x1530000: 'PowerShot A95',
0x1540000: 'PowerShot SD300 / Digital IXUS 40 / IXY Digital 50',
0x1550000: 'PowerShot SD200 / Digital IXUS 30 / IXY Digital 40',
0x1560000: 'PowerShot A520',
0x1570000: 'PowerShot A510',
0x1590000: 'PowerShot SD20 / Digital IXUS i5 / IXY Digital L2',
0x1640000: 'PowerShot S2 IS',
0x1650000:
'PowerShot SD430 / Digital IXUS Wireless / IXY Digital Wireless',
0x1660000: 'PowerShot SD500 / Digital IXUS 700 / IXY Digital 600',
0x1668000: 'EOS D60',
0x1700000: 'PowerShot SD30 / Digital IXUS i Zoom / IXY Digital L3',
0x1740000: 'PowerShot A430',
0x1750000: 'PowerShot A410',
0x1760000: 'PowerShot S80',
0x1780000: 'PowerShot A620',
0x1790000: 'PowerShot A610',
0x1800000: 'PowerShot SD630 / Digital IXUS 65 / IXY Digital 80',
0x1810000: 'PowerShot SD450 / Digital IXUS 55 / IXY Digital 60',
0x1820000: 'PowerShot TX1',
0x1870000: 'PowerShot SD400 / Digital IXUS 50 / IXY Digital 55',
0x1880000: 'PowerShot A420',
0x1890000: 'PowerShot SD900 / Digital IXUS 900 Ti / IXY Digital 1000',
0x1900000: 'PowerShot SD550 / Digital IXUS 750 / IXY Digital 700',
0x1920000: 'PowerShot A700',
0x1940000:
'PowerShot SD700 IS / Digital IXUS 800 IS / IXY Digital 800 IS',
0x1950000: 'PowerShot S3 IS',
0x1960000: 'PowerShot A540',
0x1970000: 'PowerShot SD600 / Digital IXUS 60 / IXY Digital 70',
0x1980000: 'PowerShot G7',
0x1990000: 'PowerShot A530',
0x2000000:
'PowerShot SD800 IS / Digital IXUS 850 IS / IXY Digital 900 IS',
0x2010000: 'PowerShot SD40 / Digital IXUS i7 / IXY Digital L4',
0x2020000: 'PowerShot A710 IS',
0x2030000: 'PowerShot A640',
0x2040000: 'PowerShot A630',
0x2090000: 'PowerShot S5 IS',
0x2100000: 'PowerShot A460',
0x2120000:
'PowerShot SD850 IS / Digital IXUS 950 IS / IXY Digital 810 IS',
0x2130000: 'PowerShot A570 IS',
0x2140000: 'PowerShot A560',
0x2150000: 'PowerShot SD750 / Digital IXUS 75 / IXY Digital 90',
0x2160000: 'PowerShot SD1000 / Digital IXUS 70 / IXY Digital 10',
0x2180000: 'PowerShot A550',
0x2190000: 'PowerShot A450',
0x2230000: 'PowerShot G9',
0x2240000: 'PowerShot A650 IS',
0x2260000: 'PowerShot A720 IS',
0x2290000: 'PowerShot SX100 IS',
0x2300000:
'PowerShot SD950 IS / Digital IXUS 960 IS / IXY Digital 2000 IS',
0x2310000:
'PowerShot SD870 IS / Digital IXUS 860 IS / IXY Digital 910 IS',
0x2320000:
'PowerShot SD890 IS / Digital IXUS 970 IS / IXY Digital 820 IS',
0x2360000: 'PowerShot SD790 IS / Digital IXUS 90 IS / IXY Digital 95 IS',
0x2370000: 'PowerShot SD770 IS / Digital IXUS 85 IS / IXY Digital 25 IS',
0x2380000: 'PowerShot A590 IS',
0x2390000: 'PowerShot A580',
0x2420000: 'PowerShot A470',
0x2430000: 'PowerShot SD1100 IS / Digital IXUS 80 IS / IXY Digital 20 IS',
0x2460000: 'PowerShot SX1 IS',
0x2470000: 'PowerShot SX10 IS',
0x2480000: 'PowerShot A1000 IS',
0x2490000: 'PowerShot G10',
0x2510000: 'PowerShot A2000 IS',
0x2520000: 'PowerShot SX110 IS',
0x2530000:
'PowerShot SD990 IS / Digital IXUS 980 IS / IXY Digital 3000 IS',
0x2540000:
'PowerShot SD880 IS / Digital IXUS 870 IS / IXY Digital 920 IS',
0x2550000: 'PowerShot E1',
0x2560000: 'PowerShot D10',
0x2570000:
'PowerShot SD960 IS / Digital IXUS 110 IS / IXY Digital 510 IS',
0x2580000: 'PowerShot A2100 IS',
0x2590000: 'PowerShot A480',
0x2600000: 'PowerShot SX200 IS',
0x2610000:
'PowerShot SD970 IS / Digital IXUS 990 IS / IXY Digital 830 IS',
0x2620000:
'PowerShot SD780 IS / Digital IXUS 100 IS / IXY Digital 210 IS',
0x2630000: 'PowerShot A1100 IS',
0x2640000:
'PowerShot SD1200 IS / Digital IXUS 95 IS / IXY Digital 110 IS',
0x2700000: 'PowerShot G11',
0x2710000: 'PowerShot SX120 IS',
0x2720000: 'PowerShot S90',
0x2750000: 'PowerShot SX20 IS',
0x2760000:
'PowerShot SD980 IS / Digital IXUS 200 IS / IXY Digital 930 IS',
0x2770000:
'PowerShot SD940 IS / Digital IXUS 120 IS / IXY Digital 220 IS',
0x2800000: 'PowerShot A495',
0x2810000: 'PowerShot A490',
0x2820000: 'PowerShot A3100 IS / A3150 IS',
0x2830000: 'PowerShot A3000 IS',
0x2840000: 'PowerShot SD1400 IS / IXUS 130 / IXY 400F',
0x2850000: 'PowerShot SD1300 IS / IXUS 105 / IXY 200F',
0x2860000: 'PowerShot SD3500 IS / IXUS 210 / IXY 10S',
0x2870000: 'PowerShot SX210 IS',
0x2880000: 'PowerShot SD4000 IS / IXUS 300 HS / IXY 30S',
0x2890000: 'PowerShot SD4500 IS / IXUS 1000 HS / IXY 50S',
0x2920000: 'PowerShot G12',
0x2930000: 'PowerShot SX30 IS',
0x2940000: 'PowerShot SX130 IS',
0x2950000: 'PowerShot S95',
0x2980000: 'PowerShot A3300 IS',
0x2990000: 'PowerShot A3200 IS',
0x3000000: 'PowerShot ELPH 500 HS / IXUS 310 HS / IXY 31S',
0x3010000: 'PowerShot Pro90 IS',
0x3010001: 'PowerShot A800',
0x3020000: 'PowerShot ELPH 100 HS / IXUS 115 HS / IXY 210F',
0x3030000: 'PowerShot SX230 HS',
0x3040000: 'PowerShot ELPH 300 HS / IXUS 220 HS / IXY 410F',
0x3050000: 'PowerShot A2200',
0x3060000: 'PowerShot A1200',
0x3070000: 'PowerShot SX220 HS',
0x3080000: 'PowerShot G1 X',
0x3090000: 'PowerShot SX150 IS',
0x3100000: 'PowerShot ELPH 510 HS / IXUS 1100 HS / IXY 51S',
0x3110000: 'PowerShot S100 (new)',
0x3130000: 'PowerShot SX40 HS',
0x3120000: 'PowerShot ELPH 310 HS / IXUS 230 HS / IXY 600F',
0x3160000: 'PowerShot A1300',
0x3170000: 'PowerShot A810',
0x3180000: 'PowerShot ELPH 320 HS / IXUS 240 HS / IXY 420F',
0x3190000: 'PowerShot ELPH 110 HS / IXUS 125 HS / IXY 220F',
0x3200000: 'PowerShot D20',
0x3210000: 'PowerShot A4000 IS',
0x3220000: 'PowerShot SX260 HS',
0x3230000: 'PowerShot SX240 HS',
0x3240000: 'PowerShot ELPH 530 HS / IXUS 510 HS / IXY 1',
0x3250000: 'PowerShot ELPH 520 HS / IXUS 500 HS / IXY 3',
0x3260000: 'PowerShot A3400 IS',
0x3270000: 'PowerShot A2400 IS',
0x3280000: 'PowerShot A2300',
0x3330000: 'PowerShot G15',
0x3340000: 'PowerShot SX50',
0x3350000: 'PowerShot SX160 IS',
0x3360000: 'PowerShot S110 (new)',
0x3370000: 'PowerShot SX500 IS',
0x3380000: 'PowerShot N',
0x3390000: 'IXUS 245 HS / IXY 430F',
0x3400000: 'PowerShot SX280 HS',
0x3410000: 'PowerShot SX270 HS',
0x3420000: 'PowerShot A3500 IS',
0x3430000: 'PowerShot A2600',
0x3450000: 'PowerShot A1400',
0x3460000: 'PowerShot ELPH 130 IS / IXUS 140 / IXY 110F',
0x3470000: 'PowerShot ELPH 115/120 IS / IXUS 132/135 / IXY 90F/100F',
0x3490000: 'PowerShot ELPH 330 HS / IXUS 255 HS / IXY 610F',
0x3510000: 'PowerShot A2500',
0x3540000: 'PowerShot G16',
0x3550000: 'PowerShot S120',
0x3560000: 'PowerShot SX170 IS',
0x3580000: 'PowerShot SX510 HS',
0x3590000: 'PowerShot S200 (new)',
0x3600000: 'IXY 620F',
0x3610000: 'PowerShot N100',
0x3640000: 'PowerShot G1 X Mark II',
0x3650000: 'PowerShot D30',
0x3660000: 'PowerShot SX700 HS',
0x3670000: 'PowerShot SX600 HS',
0x3680000: 'PowerShot ELPH 140 IS / IXUS 150 / IXY 130',
0x3690000: 'PowerShot ELPH 135 / IXUS 145 / IXY 120',
0x3700000: 'PowerShot ELPH 340 HS / IXUS 265 HS / IXY 630',
0x3710000: 'PowerShot ELPH 150 IS / IXUS 155 / IXY 140',
0x3740000: 'EOS M3',
0x3750000: 'PowerShot SX60 HS',
0x3760000: 'PowerShot SX520 HS',
0x3770000: 'PowerShot SX400 IS',
0x3780000: 'PowerShot G7 X',
0x3790000: 'PowerShot N2',
0x3800000: 'PowerShot SX530 HS',
0x3820000: 'PowerShot SX710 HS',
0x3830000: 'PowerShot SX610 HS',
0x3870000: 'PowerShot ELPH 160 / IXUS 160',
0x3890000: 'PowerShot ELPH 170 IS / IXUS 170',
0x3910000: 'PowerShot SX410 IS',
0x4040000: 'PowerShot G1',
0x6040000: 'PowerShot S100 / Digital IXUS / IXY Digital',
0x4007d673: 'DC19/DC21/DC22',
0x4007d674: 'XH A1',
0x4007d675: 'HV10',
0x4007d676: 'MD130/MD140/MD150/MD160/ZR850',
0x4007d777: 'DC50',
0x4007d778: 'HV20',
0x4007d779: 'DC211',
0x4007d77a: 'HG10',
0x4007d77b: 'HR10',
0x4007d77d: 'MD255/ZR950',
0x4007d81c: 'HF11',
0x4007d878: 'HV30',
0x4007d87c: 'XH A1S',
0x4007d87e: 'DC301/DC310/DC311/DC320/DC330',
0x4007d87f: 'FS100',
0x4007d880: 'HF10',
0x4007d882: 'HG20/HG21',
0x4007d925: 'HF21',
0x4007d926: 'HF S11',
0x4007d978: 'HV40',
0x4007d987: 'DC410/DC411/DC420',
0x4007d988: 'FS19/FS20/FS21/FS22/FS200',
0x4007d989: 'HF20/HF200',
0x4007d98a: 'HF S10/S100',
0x4007da8e: 'HF R10/R16/R17/R18/R100/R106',
0x4007da8f: 'HF M30/M31/M36/M300/M306',
0x4007da90: 'HF S20/S21/S200',
0x4007da92: 'FS31/FS36/FS37/FS300/FS305/FS306/FS307',
0x4007dda9: 'HF G25',
0x80000001: 'EOS-1D',
0x80000167: 'EOS-1DS',
0x80000168: 'EOS 10D',
0x80000169: 'EOS-1D Mark III',
0x80000170: 'EOS Digital Rebel / 300D / Kiss Digital',
0x80000174: 'EOS-1D Mark II',
0x80000175: 'EOS 20D',
0x80000176: 'EOS Digital Rebel XSi / 450D / Kiss X2',
0x80000188: 'EOS-1Ds Mark II',
0x80000189: 'EOS Digital Rebel XT / 350D / Kiss Digital N',
0x80000190: 'EOS 40D',
0x80000213: 'EOS 5D',
0x80000215: 'EOS-1Ds Mark III',
0x80000218: 'EOS 5D Mark II',
0x80000219: 'WFT-E1',
0x80000232: 'EOS-1D Mark II N',
0x80000234: 'EOS 30D',
0x80000236: 'EOS Digital Rebel XTi / 400D / Kiss Digital X',
0x80000241: 'WFT-E2',
0x80000246: 'WFT-E3',
0x80000250: 'EOS 7D',
0x80000252: 'EOS Rebel T1i / 500D / Kiss X3',
0x80000254: 'EOS Rebel XS / 1000D / Kiss F',
0x80000261: 'EOS 50D',
0x80000269: 'EOS-1D X',
0x80000270: 'EOS Rebel T2i / 550D / Kiss X4',
0x80000271: 'WFT-E4',
0x80000273: 'WFT-E5',
0x80000281: 'EOS-1D Mark IV',
0x80000285: 'EOS 5D Mark III',
0x80000286: 'EOS Rebel T3i / 600D / Kiss X5',
0x80000287: 'EOS 60D',
0x80000288: 'EOS Rebel T3 / 1100D / Kiss X50',
0x80000289: 'EOS 7D Mark II',
0x80000297: 'WFT-E2 II',
0x80000298: 'WFT-E4 II',
0x80000301: 'EOS Rebel T4i / 650D / Kiss X6i',
0x80000302: 'EOS 6D',
0x80000324: 'EOS-1D C',
0x80000325: 'EOS 70D',
0x80000326: 'EOS Rebel T5i / 700D / Kiss X7i',
0x80000327: 'EOS Rebel T5 / 1200D / Kiss X70',
0x80000331: 'EOS M',
0x80000355: 'EOS M2',
0x80000346: 'EOS Rebel SL1 / 100D / Kiss X7',
0x80000347: 'EOS Rebel T6s / 760D / 8000D',
0x80000382: 'EOS 5DS',
0x80000393: 'EOS Rebel T6i / 750D / Kiss X8i',
0x80000401: 'EOS 5DS R',
}),
0x0013: _make('ThumbnailImageValidArea'),
0x0015: _withMap(
'SerialNumberFormat', {0x90000000: 'Format 1', 0xA0000000: 'Format 2'}),
0x001a:
_withMap('SuperMacro', {0: 'Off', 1: 'On const ()', 2: 'On const ()'}),
0x001c: _withMap('DateStampMode', {
0: 'Off',
1: 'Date',
2: 'Date & Time',
}),
0x001e: _make('FirmwareRevision'),
0x0028: _make('ImageUniqueID'),
0x0095: _make('LensModel'),
0x0096: _make('InternalSerialNumber '),
0x0097: _make('DustRemovalData '),
0x0098: _make('CropInfo '),
0x009a: _make('AspectInfo'),
0x00b4: _withMap('ColorSpace', {1: 'sRGB', 2: 'Adobe RGB'}),
};
static final tagsXxx = {
'MakerNote Tag 0x0001': cameraSettings,
'MakerNote Tag 0x0002': focalLength,
'MakerNote Tag 0x0004': shotInfo,
'MakerNote Tag 0x0026': afInfo2,
'MakerNote Tag 0x0093': fileInfo,
};
// this is in element offset, name, optional value dictionary format
// 0x0001
static Map<int, MakerTag> cameraSettings = {
1: _withMap('Macromode', {1: 'Macro', 2: 'Normal'}),
2: _make('SelfTimer'),
3: _withMap(
'Quality', {1: 'Economy', 2: 'Normal', 3: 'Fine', 5: 'Superfine'}),
4: _withMap('FlashMode', {
0: 'Flash Not Fired',
1: 'Auto',
2: 'On',
3: 'Red-Eye Reduction',
4: 'Slow Synchro',
5: 'Auto + Red-Eye Reduction',
6: 'On + Red-Eye Reduction',
16: 'external flash'
}),
5: _withMap('ContinuousDriveMode', {
0: 'Single Or Timer',
1: 'Continuous',
2: 'Movie',
}),
7: _withMap('FocusMode', {
0: 'One-Shot',
1: 'AI Servo',
2: 'AI Focus',
3: 'MF',
4: 'Single',
5: 'Continuous',
6: 'MF'
}),
9: _withMap('RecordMode', {
1: 'JPEG',
2: 'CRW+THM',
3: 'AVI+THM',
4: 'TIF',
5: 'TIF+JPEG',
6: 'CR2',
7: 'CR2+JPEG',
9: 'Video'
}),
10: _withMap('ImageSize', {0: 'Large', 1: 'Medium', 2: 'Small'}),
11: _withMap('EasyShootingMode', {
0: 'Full Auto',
1: 'Manual',
2: 'Landscape',
3: 'Fast Shutter',
4: 'Slow Shutter',
5: 'Night',
6: 'B&W',
7: 'Sepia',
8: 'Portrait',
9: 'Sports',
10: 'Macro/Close-Up',
11: 'Pan Focus',
51: 'High Dynamic Range',
}),
12: _withMap('DigitalZoom', {0: 'None', 1: '2x', 2: '4x', 3: 'Other'}),
13: _withMap('Contrast', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}),
14: _withMap('Saturation', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}),
15: _withMap('Sharpness', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}),
16: _withMap('ISO', {
0: 'See ISOSpeedRatings Tag',
15: 'Auto',
16: '50',
17: '100',
18: '200',
19: '400'
}),
17: _withMap('MeteringMode', {
0: 'Default',
1: 'Spot',
2: 'Average',
3: 'Evaluative',
4: 'Partial',
5: 'Center-weighted'
}),
18: _withMap('FocusType', {
0: 'Manual',
1: 'Auto',
3: 'Close-Up (Macro)',
8: 'Locked (Pan Mode)'
}),
19: _withMap('AFPointSelected', {
0x3000: 'None (MF)',
0x3001: 'Auto-Selected',
0x3002: 'Right',
0x3003: 'Center',
0x3004: 'Left'
}),
20: _withMap('ExposureMode', {
0: 'Easy Shooting',
1: 'Program',
2: 'Tv-priority',
3: 'Av-priority',
4: 'Manual',
5: 'A-DEP'
}),
22: _make('LensType'),
23: _make('LongFocalLengthOfLensInFocalUnits'),
24: _make('ShortFocalLengthOfLensInFocalUnits'),
25: _make('FocalUnitsPerMM'),
28: _withMap('FlashActivity', {0: 'Did Not Fire', 1: 'Fired'}),
29: _withMap('FlashDetails', {
0: 'Manual',
1: 'TTL',
2: 'A-TTL',
3: 'E-TTL',
4: 'FP Sync Enabled',
7: '2nd("Rear")-Curtain Sync Used',
11: 'FP Sync Used',
13: 'Internal Flash',
14: 'External E-TTL'
}),
32: _withMap('FocusMode', {0: 'Single', 1: 'Continuous', 8: 'Manual'}),
33: _withMap('AESetting', {
0: 'Normal AE',
1: 'Exposure Compensation',
2: 'AE Lock',
3: 'AE Lock + Exposure Comp.',
4: 'No AE'
}),
34: _withMap('ImageStabilization', {
0: 'Off',
1: 'On',
2: 'Shoot Only',
3: 'Panning',
4: 'Dynamic',
256: 'Off',
257: 'On',
258: 'Shoot Only',
259: 'Panning',
260: 'Dynamic'
}),
39: _withMap('SpotMeteringMode', {0: 'Center', 1: 'AF Point'}),
41: _withMap('ManualFlashOutput', {
0x0: 'n/a',
0x500: 'Full',
0x502: 'Medium',
0x504: 'Low',
0x7fff: 'n/a'
}),
};
// 0x0002
static Map<int, MakerTag> focalLength = {
1: _withMap('FocalType', {
1: 'Fixed',
2: 'Zoom',
}),
2: _make('FocalLength'),
};
// 0x0004
static Map<int, MakerTag> shotInfo = {
7: _withMap('WhiteBalance', {
0: 'Auto',
1: 'Sunny',
2: 'Cloudy',
3: 'Tungsten',
4: 'Fluorescent',
5: 'Flash',
6: 'Custom'
}),
8: _withMap('SlowShutter',
{-1: 'n/a', 0: 'Off', 1: 'Night Scene', 2: 'On', 3: 'None'}),
9: _make('SequenceNumber'),
14: _make('AFPointUsed'),
15: _withMap('FlashBias', {
0xFFC0: '-2 EV',
0xFFCC: '-1.67 EV',
0xFFD0: '-1.50 EV',
0xFFD4: '-1.33 EV',
0xFFE0: '-1 EV',
0xFFEC: '-0.67 EV',
0xFFF0: '-0.50 EV',
0xFFF4: '-0.33 EV',
0x0000: '0 EV',
0x000c: '0.33 EV',
0x0010: '0.50 EV',
0x0014: '0.67 EV',
0x0020: '1 EV',
0x002c: '1.33 EV',
0x0030: '1.50 EV',
0x0034: '1.67 EV',
0x0040: '2 EV'
}),
19: _make('SubjectDistance'),
};
// 0x0026
static Map<int, MakerTag> afInfo2 = {
2: _withMap('AFAreaMode', {
0: 'Off (Manual Focus)',
2: 'Single-point AF',
4: 'Multi-point AF or AI AF',
5: 'Face Detect AF',
6: 'Face + Tracking',
7: 'Zone AF',
8: 'AF Point Expansion',
9: 'Spot AF',
11: 'Flexizone Multi',
13: 'Flexizone Single',
}),
3: _make('NumAFPoints'),
4: _make('ValidAFPoints'),
5: _make('CanonImageWidth'),
};
// 0x0093
static Map<int, MakerTag> fileInfo = {
1: _make('FileNumber'),
3: _withMap('BracketMode', {
0: 'Off',
1: 'AEB',
2: 'FEB',
3: 'ISO',
4: 'WB',
}),
4: _make('BracketValue'),
5: _make('BracketShotNumber'),
6: _withMap('RawJpgQuality', {
0xFFFF: 'n/a',
1: 'Economy',
2: 'Normal',
3: 'Fine',
4: 'RAW',
5: 'Superfine',
130: 'Normal Movie'
}),
7: _withMap('RawJpgSize', {
0: 'Large',
1: 'Medium',
2: 'Small',
5: 'Medium 1',
6: 'Medium 2',
7: 'Medium 3',
8: 'Postcard',
9: 'Widescreen',
10: 'Medium Widescreen',
14: 'Small 1',
15: 'Small 2',
16: 'Small 3',
128: '640x480 Movie',
129: 'Medium Movie',
130: 'Small Movie',
137: '1280x720 Movie',
142: '1920x1080 Movie',
}),
8: _withMap('LongExposureNoiseReduction2',
{0: 'Off', 1: 'On (1D)', 2: 'On', 3: 'Auto'}),
9: _withMap(
'WBBracketMode', {0: 'Off', 1: 'On (shift AB)', 2: 'On (shift GM)'}),
12: _make('WBBracketValueAB'),
13: _make('WBBracketValueGM'),
14: _withMap('FilterEffect',
{0: 'None', 1: 'Yellow', 2: 'Orange', 3: 'Red', 4: 'Green'}),
15: _withMap('ToningEffect', {
0: 'None',
1: 'Sepia',
2: 'Blue',
3: 'Purple',
4: 'Green',
}),
16: _make('MacroMagnification'),
19: _withMap('LiveViewShooting', {0: 'Off', 1: 'On'}),
25: _withMap('FlashExposureLock', {0: 'Off', 1: 'On'})
};
static String addOneFunc(int value) {
return "${value + 1}";
}
static String subtractOneFunc(int value) {
return "${value - 1}";
}
static String convertTempFunc(int value) {
return sprintf('%d C', [value - 128]);
}
// CameraInfo data structures have variable sized members. Each entry here is:
// byte offset: (item name, data item type, decoding map).
// Note that the data item type is fed directly to struct.unpack at the
// specified offset.
static const cameraInfoTagName = 'MakerNote Tag 0x000D';
// A map of regular expressions on 'Image Model' to the CameraInfo spec
static Map<String, Map<int, CameraInfo>> cameraInfoModelMap = {
r'EOS 5D$': {
23: const CameraInfo('CameraTemperature', 1, convertTempFunc),
204: const CameraInfo('DirectoryIndex', 4, subtractOneFunc),
208: const CameraInfo('FileIndex', 2, addOneFunc),
},
r'EOS 5D Mark II$': {
25: const CameraInfo('CameraTemperature', 1, convertTempFunc),
443: const CameraInfo('FileIndex', 4, addOneFunc),
455: const CameraInfo('DirectoryIndex', 4, subtractOneFunc),
},
r'EOS 5D Mark III$': {
27: const CameraInfo('CameraTemperature', 1, convertTempFunc),
652: const CameraInfo('FileIndex', 4, addOneFunc),
656: const CameraInfo('FileIndex2', 4, addOneFunc),
664: const CameraInfo('DirectoryIndex', 4, subtractOneFunc),
668: const CameraInfo('DirectoryIndex2', 4, subtractOneFunc),
},
r'\b(600D|REBEL T3i|Kiss X5)\b': {
25: const CameraInfo('CameraTemperature', 1, convertTempFunc),
475: const CameraInfo('FileIndex', 4, addOneFunc),
487: const CameraInfo('DirectoryIndex', 4, subtractOneFunc),
},
};
}
class CameraInfo {
final String tagName;
final int tagSize;
final String Function(int) function;
const CameraInfo(
this.tagName,
this.tagSize,
this.function,
);
}

View file

@ -0,0 +1,63 @@
import 'package:exif/src/tags_info.dart' show MakerTag, TagsBase;
// Makernote (proprietary) tag definitions for casio.
class MakerNoteCasio extends TagsBase {
static MakerTag _make(String name) => MakerTag.make(name);
static MakerTag _withMap(String name, Map<int, String> map) =>
MakerTag.makeWithMap(name, map);
static Map<int, MakerTag> tags = {
0x0001: _withMap('RecordingMode', {
1: 'Single Shutter',
2: 'Panorama',
3: 'Night Scene',
4: 'Portrait',
5: 'Landscape',
}),
0x0002: _withMap('Quality', {1: 'Economy', 2: 'Normal', 3: 'Fine'}),
0x0003: _withMap('FocusingMode',
{2: 'Macro', 3: 'Auto Focus', 4: 'Manual Focus', 5: 'Infinity'}),
0x0004: _withMap('FlashMode', {
1: 'Auto',
2: 'On',
3: 'Off',
4: 'Red Eye Reduction',
}),
0x0005:
_withMap('FlashIntensity', {11: 'Weak', 13: 'Normal', 15: 'Strong'}),
0x0006: _make('Object Distance'),
0x0007: _withMap('WhiteBalance', {
1: 'Auto',
2: 'Tungsten',
3: 'Daylight',
4: 'Fluorescent',
5: 'Shade',
129: 'Manual'
}),
0x000B: _withMap('Sharpness', {
0: 'Normal',
1: 'Soft',
2: 'Hard',
}),
0x000C: _withMap('Contrast', {
0: 'Normal',
1: 'Low',
2: 'High',
}),
0x000D: _withMap('Saturation', {
0: 'Normal',
1: 'Low',
2: 'High',
}),
0x0014: _withMap('CCDSpeed', {
64: 'Normal',
80: 'Normal',
100: 'High',
125: '+1.0',
244: '+3.0',
250: '+2.0'
}),
};
}

View file

@ -0,0 +1,101 @@
import 'package:exif/src/tags_info.dart' show MakerTag, MakerTagFunc, TagsBase;
import 'package:exif/src/util.dart';
// Makernote (proprietary) tag definitions for FujiFilm.
// http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/FujiFilm.html
class MakerNoteFujifilm extends TagsBase {
static MakerTag _make(String name) => MakerTag.make(name);
static MakerTag _withMap(String name, Map<int, String> map) =>
MakerTag.makeWithMap(name, map);
static MakerTag _withFunc(String name, MakerTagFunc func) =>
MakerTag.makeWithFunc(name, func);
static final tags = {
0x0000: _withFunc('NoteVersion', makeString),
0x0010: _make('InternalSerialNumber'),
0x1000: _make('Quality'),
0x1001: _withMap('Sharpness', {
0x1: 'Soft',
0x2: 'Soft',
0x3: 'Normal',
0x4: 'Hard',
0x5: 'Hard2',
0x82: 'Medium Soft',
0x84: 'Medium Hard',
0x8000: 'Film Simulation'
}),
0x1002: _withMap('WhiteBalance', {
0x0: 'Auto',
0x100: 'Daylight',
0x200: 'Cloudy',
0x300: 'Daylight Fluorescent',
0x301: 'Day White Fluorescent',
0x302: 'White Fluorescent',
0x303: 'Warm White Fluorescent',
0x304: 'Living Room Warm White Fluorescent',
0x400: 'Incandescent',
0x500: 'Flash',
0x600: 'Underwater',
0xf00: 'Custom',
0xf01: 'Custom2',
0xf02: 'Custom3',
0xf03: 'Custom4',
0xf04: 'Custom5',
0xff0: 'Kelvin'
}),
0x1003: _withMap('Saturation', {
0x0: 'Normal',
0x80: 'Medium High',
0x100: 'High',
0x180: 'Medium Low',
0x200: 'Low',
0x300: 'None (B&W)',
0x301: 'B&W Red Filter',
0x302: 'B&W Yellow Filter',
0x303: 'B&W Green Filter',
0x310: 'B&W Sepia',
0x400: 'Low 2',
0x8000: 'Film Simulation'
}),
0x1004: _withMap('Contrast', {
0x0: 'Normal',
0x80: 'Medium High',
0x100: 'High',
0x180: 'Medium Low',
0x200: 'Low',
0x8000: 'Film Simulation'
}),
0x1005: _make('ColorTemperature'),
0x1006: _withMap('Contrast', {0x0: 'Normal', 0x100: 'High', 0x300: 'Low'}),
0x100a: _make('WhiteBalanceFineTune'),
0x1010: _withMap(
'FlashMode', {0: 'Auto', 1: 'On', 2: 'Off', 3: 'Red Eye Reduction'}),
0x1011: _make('FlashStrength'),
0x1020: _withMap('Macro', {0: 'Off', 1: 'On'}),
0x1021: _withMap('FocusMode', {0: 'Auto', 1: 'Manual'}),
0x1022: _withMap('AFPointSet', {0: 'Yes', 1: 'No'}),
0x1023: _make('FocusPixel'),
0x1030: _withMap('SlowSync', {0: 'Off', 1: 'On'}),
0x1031: _withMap('PictureMode', {
0: 'Auto',
1: 'Portrait',
2: 'Landscape',
4: 'Sports',
5: 'Night',
6: 'Program AE',
256: 'Aperture Priority AE',
512: 'Shutter Priority AE',
768: 'Manual Exposure'
}),
0x1032: _make('ExposureCount'),
0x1100: _withMap('MotorOrBracket', {0: 'Off', 1: 'On'}),
0x1210:
_withMap('ColorMode', {0x0: 'Standard', 0x10: 'Chrome', 0x30: 'B & W'}),
0x1300: _withMap('BlurWarning', {0: 'Off', 1: 'On'}),
0x1301: _withMap('FocusWarning', {0: 'Off', 1: 'On'}),
0x1302: _withMap('ExposureWarning', {0: 'Off', 1: 'On'}),
};
}

View file

@ -0,0 +1,286 @@
import 'package:exif/src/exif_types.dart';
import 'package:exif/src/tags_info.dart' show MakerTag, MakerTagFunc, TagsBase;
import 'package:exif/src/util.dart';
import 'package:sprintf/sprintf.dart' show sprintf;
// Makernote (proprietary) tag definitions for Nikon.
class MakerNoteNikon extends TagsBase {
static Map<int, MakerTag> tagsNew = _buildTagsNew();
static Map<int, MakerTag> tagsOld = _buildTagsOld();
static MakerTag _make(String name) => MakerTag.make(name);
static MakerTag _withMap(String name, Map<int, String> map) =>
MakerTag.makeWithMap(name, map);
static MakerTag _withFunc(String name, MakerTagFunc func) =>
MakerTag.makeWithFunc(name, func);
// First digit seems to be in steps of 1/6 EV.
// Does the third value mean the step size? It is usually 6,
// but it is 12 for the ExposureDifference.
// Check for an error condition that could cause a crash.
// This only happens if something has gone really wrong in
// reading the Nikon MakerNote.
// http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp
static String _evBias(List<int> seq) {
if (seq.length < 4) {
return '';
}
if (listEqual(seq, [252, 1, 6, 0])) {
return '-2/3 EV';
}
if (listEqual(seq, [253, 1, 6, 0])) {
return '-1/2 EV';
}
if (listEqual(seq, [254, 1, 6, 0])) {
return '-1/3 EV';
}
if (listEqual(seq, [0, 1, 6, 0])) {
return '0 EV';
}
if (listEqual(seq, [2, 1, 6, 0])) {
return '+1/3 EV';
}
if (listEqual(seq, [3, 1, 6, 0])) {
return '+1/2 EV';
}
if (listEqual(seq, [4, 1, 6, 0])) {
return '+2/3 EV';
}
// Handle combinations not in the table.
int a = seq[0];
String? retStr;
// Causes headaches for the +/- logic, so special case it.
if (a == 0) {
return '0 EV';
}
if (a > 127) {
a = 256 - a;
retStr = '-';
} else {
retStr = '+';
}
final step = seq[2]; // Assume third value means the step size
final whole = a ~/ step;
a = a % step;
if (whole != 0) {
retStr = sprintf('%s%s ', [retStr, whole.toString()]);
}
if (a == 0) {
retStr += 'EV';
} else {
final r = Ratio(a, step);
retStr = '$retStr$r EV';
}
return retStr;
}
// Nikon E99x MakerNote Tags
static Map<int, MakerTag> _buildTagsNew() {
return {
0x0001: _withFunc('MakernoteVersion', makeString), // Sometimes binary
0x0002: _make('ISOSetting'),
0x0003: _make('ColorMode'),
0x0004: _make('Quality'),
0x0005: _make('Whitebalance'),
0x0006: _make('ImageSharpening'),
0x0007: _make('FocusMode'),
0x0008: _make('FlashSetting'),
0x0009: _make('AutoFlashMode'),
0x000B: _make('WhiteBalanceBias'),
0x000C: _make('WhiteBalanceRBCoeff'),
0x000D: _withFunc('ProgramShift', _evBias),
// Nearly the same as the other EV vals, but step size is 1/12 EV []
0x000E: _withFunc('ExposureDifference', _evBias),
0x000F: _make('ISOSelection'),
0x0010: _make('DataDump'),
0x0011: _make('NikonPreview'),
0x0012: _withFunc('FlashCompensation', _evBias),
0x0013: _make('ISOSpeedRequested'),
0x0016: _make('PhotoCornerCoordinates'),
0x0017: _withFunc('ExternalFlashExposureComp', _evBias),
0x0018: _withFunc('FlashBracketCompensationApplied', _evBias),
0x0019: _make('AEBracketCompensationApplied'),
0x001A: _make('ImageProcessing'),
0x001B: _make('CropHiSpeed'),
0x001C: _make('ExposureTuning'),
0x001D: _make('SerialNumber'), // Conflict with 0x00A0 ?
0x001E: _make('ColorSpace'),
0x001F: _make('VRInfo'),
0x0020: _make('ImageAuthentication'),
0x0022: _make('ActiveDLighting'),
0x0023: _make('PictureControl'),
0x0024: _make('WorldTime'),
0x0025: _make('ISOInfo'),
0x0080: _make('ImageAdjustment'),
0x0081: _make('ToneCompensation'),
0x0082: _make('AuxiliaryLens'),
0x0083: _make('LensType'),
0x0084: _make('LensMinMaxFocalMaxAperture'),
0x0085: _make('ManualFocusDistance'),
0x0086: _make('DigitalZoomFactor'),
0x0087: _withMap('FlashMode', {
0x00: 'Did Not Fire',
0x01: 'Fired, Manual',
0x07: 'Fired, External',
0x08: 'Fired, Commander Mode ',
0x09: 'Fired, TTL Mode',
}),
0x0088: _withMap('AFFocusPosition', {
0x0000: 'Center',
0x0100: 'Top',
0x0200: 'Bottom',
0x0300: 'Left',
0x0400: 'Right',
}),
0x0089: _withMap('BracketingMode', {
0x00: 'Single frame, no bracketing',
0x01: 'Continuous, no bracketing',
0x02: 'Timer, no bracketing',
0x10: 'Single frame, exposure bracketing',
0x11: 'Continuous, exposure bracketing',
0x12: 'Timer, exposure bracketing',
0x40: 'Single frame, white balance bracketing',
0x41: 'Continuous, white balance bracketing',
0x42: 'Timer, white balance bracketing'
}),
0x008A: _make('AutoBracketRelease'),
0x008B: _make('LensFStops'),
0x008C: _make('NEFCurve1'), // ExifTool calls this 'ContrastCurve'
0x008D: _make('ColorMode'),
0x008F: _make('SceneMode'),
0x0090: _make('LightingType'),
0x0091: _make('ShotInfo'), // First 4 bytes are a version number in ASCII
0x0092: _make('HueAdjustment'),
// ExifTool calls this 'NEFCompression', should be 1-4
0x0093: _make('Compression'),
0x0094: _withMap('Saturation', {
-3: 'B&W',
-2: '-2',
-1: '-1',
0: '0',
1: '1',
2: '2',
}),
0x0095: _make('NoiseReduction'),
0x0096: _make('NEFCurve2'), // ExifTool calls this 'LinearizationTable'
0x0097:
_make('ColorBalance'), // First 4 bytes are a version number in ASCII
0x0098: _make('LensData'), // First 4 bytes are a version number in ASCII
0x0099: _make('RawImageCenter'),
0x009A: _make('SensorPixelSize'),
0x009C: _make('Scene Assist'),
0x009E: _make('RetouchHistory'),
0x00A0: _make('SerialNumber'),
0x00A2: _make('ImageDataSize'),
// 00A3: unknown - a single byte 0
// 00A4: In NEF, looks like a 4 byte ASCII version number ('0200')
0x00A5: _make('ImageCount'),
0x00A6: _make('DeletedImageCount'),
0x00A7: _make('TotalShutterReleases'),
// First 4 bytes are a version number in ASCII, with version specific
// info to follow. Its hard to treat it as a string due to embedded nulls.
0x00A8: _make('FlashInfo'),
0x00A9: _make('ImageOptimization'),
0x00AA: _make('Saturation'),
0x00AB: _make('DigitalVariProgram'),
0x00AC: _make('ImageStabilization'),
0x00AD: _make('AFResponse'),
0x00B0: _make('MultiExposure'),
0x00B1: _make('HighISONoiseReduction'),
0x00B6: _make('PowerUpTime'),
0x00B7: _make('AFInfo2'),
0x00B8: _make('FileInfo'),
0x00B9: _make('AFTune'),
0x0100: _make('DigitalICE'),
0x0103: _withMap('PreviewCompression', {
1: 'Uncompressed',
2: 'CCITT 1D',
3: 'T4/Group 3 Fax',
4: 'T6/Group 4 Fax',
5: 'LZW',
6: 'JPEG (old-style)',
7: 'JPEG',
8: 'Adobe Deflate',
9: 'JBIG B&W',
10: 'JBIG Color',
32766: 'Next',
32769: 'Epson ERF Compressed',
32771: 'CCIRLEW',
32773: 'PackBits',
32809: 'Thunderscan',
32895: 'IT8CTPAD',
32896: 'IT8LW',
32897: 'IT8MP',
32898: 'IT8BL',
32908: 'PixarFilm',
32909: 'PixarLog',
32946: 'Deflate',
32947: 'DCS',
34661: 'JBIG',
34676: 'SGILog',
34677: 'SGILog24',
34712: 'JPEG 2000',
34713: 'Nikon NEF Compressed',
65000: 'Kodak DCR Compressed',
65535: 'Pentax PEF Compressed',
}),
0x0201: _make('PreviewImageStart'),
0x0202: _make('PreviewImageLength'),
0x0213: _withMap('PreviewYCbCrPositioning', {
1: 'Centered',
2: 'Co-sited',
}),
0x0E09: _make('NikonCaptureVersion'),
0x0E0E: _make('NikonCaptureOffsets'),
0x0E10: _make('NikonScan'),
0x0E22: _make('NEFBitDepth'),
};
}
static Map<int, MakerTag> _buildTagsOld() {
return {
0x0003: _withMap('Quality', {
1: 'VGA Basic',
2: 'VGA Normal',
3: 'VGA Fine',
4: 'SXGA Basic',
5: 'SXGA Normal',
6: 'SXGA Fine',
}),
0x0004: _withMap('ColorMode', {
1: 'Color',
2: 'Monochrome',
}),
0x0005: _withMap('ImageAdjustment', {
0: 'Normal',
1: 'Bright+',
2: 'Bright-',
3: 'Contrast+',
4: 'Contrast-',
}),
0x0006: _withMap('CCDSpeed', {
0: 'ISO 80',
2: 'ISO 160',
4: 'ISO 320',
5: 'ISO 100',
}),
0x0007: _withMap('WhiteBalance', {
0: 'Auto',
1: 'Preset',
2: 'Daylight',
3: 'Incandescent',
4: 'Fluorescent',
5: 'Cloudy',
6: 'Speed Light',
}),
};
}
}

View file

@ -0,0 +1,287 @@
import 'package:exif/src/tags_info.dart' show MakerTag, MakerTagFunc, TagsBase;
import 'package:exif/src/util.dart';
import 'package:sprintf/sprintf.dart' show sprintf;
// Makernote (proprietary) tag definitions for olympus.
class MakerNoteOlympus extends TagsBase {
static Map<int, MakerTag> tags = _buildTags();
static MakerTag _make(String name) => MakerTag.make(name);
static MakerTag _withMap(String name, Map<int, String> map) =>
MakerTag.makeWithMap(name, map);
static MakerTag _withFunc(String name, MakerTagFunc func) =>
MakerTag.makeWithFunc(name, func);
// decode Olympus SpecialMode tag in MakerNote
static String _specialMode(List<int> v) {
final Map<int, String> mode1 = {
0: 'Normal',
1: 'Unknown',
2: 'Fast',
3: 'Panorama',
};
final Map<int, String> mode2 = {
0: 'Non-panoramic',
1: 'Left to right',
2: 'Right to left',
3: 'Bottom to top',
4: 'Top to bottom',
};
if (v.isEmpty) {
return '';
}
if (v.length < 3 ||
(!mode1.containsKey(v[0]) || !mode2.containsKey(v[2]))) {
return v.toString();
}
return sprintf('%s - sequence %d - %s', [mode1[v[0]], v[1], mode2[v[2]]]);
}
static Map<int, MakerTag> _buildTags() {
return {
// ah HAH! those sneeeeeaky bastids! this is how they get past the fact
// that a JPEG thumbnail is not allowed in an uncompressed TIFF file
0x0100: _make('JPEGThumbnail'),
0x0200: _withFunc('SpecialMode', _specialMode),
0x0201: _withMap('JPEGQual', {
1: 'SQ',
2: 'HQ',
3: 'SHQ',
}),
0x0202: _withMap('Macro', {0: 'Normal', 1: 'Macro', 2: 'SuperMacro'}),
0x0203: _withMap('BWMode', {0: 'Off', 1: 'On'}),
0x0204: _make('DigitalZoom'),
0x0205: _make('FocalPlaneDiagonal'),
0x0206: _make('LensDistortionParams'),
0x0207: _make('SoftwareRelease'),
0x0208: _make('PictureInfo'),
0x0209: _withFunc('CameraID', makeString),
// print as string
0x0F00: _make('DataDump'),
0x0300: _make('PreCaptureFrames'),
0x0404: _make('SerialNumber'),
0x1000: _make('ShutterSpeedValue'),
0x1001: _make('ISOValue'),
0x1002: _make('ApertureValue'),
0x1003: _make('BrightnessValue'),
0x1004: _withMap('FlashMode', {2: 'On', 3: 'Off'}),
0x1005: _withMap('FlashDevice',
{0: 'None', 1: 'Internal', 4: 'External', 5: 'Internal + External'}),
0x1006: _make('ExposureCompensation'),
0x1007: _make('SensorTemperature'),
0x1008: _make('LensTemperature'),
0x100b: _withMap('FocusMode', {0: 'Auto', 1: 'Manual'}),
0x1017: _make('RedBalance'),
0x1018: _make('BlueBalance'),
0x101a: _make('SerialNumber'),
0x1023: _make('FlashExposureComp'),
0x1026: _withMap('ExternalFlashBounce', {0: 'No', 1: 'Yes'}),
0x1027: _make('ExternalFlashZoom'),
0x1028: _make('ExternalFlashMode'),
0x1029: _withMap('Contrast int16u', {0: 'High', 1: 'Normal', 2: 'Low'}),
0x102a: _make('SharpnessFactor'),
0x102b: _make('ColorControl'),
0x102c: _make('ValidBits'),
0x102d: _make('CoringFilter'),
0x102e: _make('OlympusImageWidth'),
0x102f: _make('OlympusImageHeight'),
0x1034: _make('CompressionRatio'),
0x1035: _withMap('PreviewImageValid', {0: 'No', 1: 'Yes'}),
0x1036: _make('PreviewImageStart'),
0x1037: _make('PreviewImageLength'),
0x1039: _withMap('CCDScanMode', {0: 'Interlaced', 1: 'Progressive'}),
0x103a: _withMap('NoiseReduction', {0: 'Off', 1: 'On'}),
0x103b: _make('InfinityLensStep'),
0x103c: _make('NearLensStep'),
// TODO - these need extra definitions
// http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html
0x2010: _make('Equipment'),
0x2020: _make('CameraSettings'),
0x2030: _make('RawDevelopment'),
0x2040: _make('ImageProcessing'),
0x2050: _make('FocusInfo'),
0x3000: _make('RawInfo '),
};
}
}
/*
// 0x2020 CameraSettings
static Map<int,List> TAG_0x2020 = {
0x0100: ['PreviewImageValid', {
0: 'No',
1: 'Yes'
}],
0x0101: ['PreviewImageStart', ],
0x0102: ['PreviewImageLength', ],
0x0200: ['ExposureMode', {
1: 'Manual',
2: 'Program',
3: 'Aperture-priority AE',
4: 'Shutter speed priority AE',
5: 'Program-shift'
}],
0x0201: ['AELock', {
0: 'Off',
1: 'On'
}],
0x0202: ['MeteringMode', {
2: 'Center Weighted',
3: 'Spot',
5: 'ESP',
261: 'Pattern+AF',
515: 'Spot+Highlight control',
1027: 'Spot+Shadow control'
}],
0x0300: ['MacroMode', {
0: 'Off',
1: 'On'
}],
0x0301: ['FocusMode', {
0: 'Single AF',
1: 'Sequential shooting AF',
2: 'Continuous AF',
3: 'Multi AF',
10: 'MF'
}],
0x0302: ['FocusProcess', {
0: 'AF Not Used',
1: 'AF Used'
}],
0x0303: ['AFSearch', {
0: 'Not Ready',
1: 'Ready'
}],
0x0304: ['AFAreas', ],
0x0401: ['FlashExposureCompensation', ],
0x0500: ['WhiteBalance2', {
0: 'Auto',
16: '7500K (Fine Weather with Shade)',
17: '6000K (Cloudy)',
18: '5300K (Fine Weather)',
20: '3000K (Tungsten light)',
21: '3600K (Tungsten light-like)',
33: '6600K (Daylight fluorescent)',
34: '4500K (Neutral white fluorescent)',
35: '4000K (Cool white fluorescent)',
48: '3600K (Tungsten light-like)',
256: 'Custom WB 1',
257: 'Custom WB 2',
258: 'Custom WB 3',
259: 'Custom WB 4',
512: 'Custom WB 5400K',
513: 'Custom WB 2900K',
514: 'Custom WB 8000K',
}],
0x0501: ['WhiteBalanceTemperature', ],
0x0502: ['WhiteBalanceBracket', ],
0x0503: ['CustomSaturation', ], // (3 numbers: 1. CS Value, 2. Min, 3. Max)
0x0504: ['ModifiedSaturation', {
0: 'Off',
1: 'CM1 (Red Enhance)',
2: 'CM2 (Green Enhance)',
3: 'CM3 (Blue Enhance)',
4: 'CM4 (Skin Tones)',
}],
0x0505: ['ContrastSetting', ], // (3 numbers: 1. Contrast, 2. Min, 3. Max)
0x0506: ['SharpnessSetting', ], // (3 numbers: 1. Sharpness, 2. Min, 3. Max)
0x0507: ['ColorSpace', {
0: 'sRGB',
1: 'Adobe RGB',
2: 'Pro Photo RGB'
}],
0x0509: ['SceneMode', {
0: 'Standard',
6: 'Auto',
7: 'Sport',
8: 'Portrait',
9: 'Landscape+Portrait',
10: 'Landscape',
11: 'Night scene',
13: 'Panorama',
16: 'Landscape+Portrait',
17: 'Night+Portrait',
19: 'Fireworks',
20: 'Sunset',
22: 'Macro',
25: 'Documents',
26: 'Museum',
28: 'Beach&Snow',
30: 'Candle',
35: 'Underwater Wide1',
36: 'Underwater Macro',
39: 'High Key',
40: 'Digital Image Stabilization',
44: 'Underwater Wide2',
45: 'Low Key',
46: 'Children',
48: 'Nature Macro',
}],
0x050a: ['NoiseReduction', {
0: 'Off',
1: 'Noise Reduction',
2: 'Noise Filter',
3: 'Noise Reduction + Noise Filter',
4: 'Noise Filter (ISO Boost)',
5: 'Noise Reduction + Noise Filter (ISO Boost)'
}],
0x050b: ['DistortionCorrection', {
0: 'Off',
1: 'On'
}],
0x050c: ['ShadingCompensation', {
0: 'Off',
1: 'On'
}],
0x050d: ['CompressionFactor', ],
0x050f: ['Gradation', {
'-1 -1 1': 'Low Key',
'0 -1 1': 'Normal',
'1 -1 1': 'High Key'
}],
0x0520: ['PictureMode', {
1: 'Vivid',
2: 'Natural',
3: 'Muted',
256: 'Monotone',
512: 'Sepia'
}],
0x0521: ['PictureModeSaturation', ],
0x0522: ['PictureModeHue?', ],
0x0523: ['PictureModeContrast', ],
0x0524: ['PictureModeSharpness', ],
0x0525: ['PictureModeBWFilter', {
0: 'n/a',
1: 'Neutral',
2: 'Yellow',
3: 'Orange',
4: 'Red',
5: 'Green'
}],
0x0526: ['PictureModeTone', {
0: 'n/a',
1: 'Neutral',
2: 'Sepia',
3: 'Blue',
4: 'Purple',
5: 'Green'
}],
0x0600: ['Sequence', ], // 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits
0x0601: ['PanoramaMode', ], // (2 numbers: 1. Mode, 2. Shot number)
0x0603: ['ImageQuality2', {
1: 'SQ',
2: 'HQ',
3: 'SHQ',
4: 'RAW',
}],
0x0901: ['ManometerReading', ],
};
*/

View file

@ -0,0 +1,38 @@
import 'package:exif/src/file_interface.dart';
import 'package:exif/src/read_exif.dart';
Future<String> printExifOfBytes(List<int> bytes,
{String? stopTag,
bool details = true,
bool strict = false,
bool debug = false}) async {
final data =
readExifFromFileReader(FileReader.fromBytes(bytes), stopTag: stopTag);
if (data.tags.isEmpty) {
return "No EXIF information found";
}
final prints = [];
// prints.addAll(data.warnings);
if (data.tags.containsKey('JPEGThumbnail')) {
prints.add('File has JPEG thumbnail');
data.tags.remove('JPEGThumbnail');
}
if (data.tags.containsKey('TIFFThumbnail')) {
prints.add('File has TIFF thumbnail');
data.tags.remove('TIFFThumbnail');
}
final tagKeys = data.tags.keys.toList();
tagKeys.sort();
for (final key in tagKeys) {
final tag = data.tags[key];
prints.add("$key (${tag!.tagType}): $tag");
}
return prints.join("\n");
}

459
exif/lib/src/read_exif.dart Normal file
View file

@ -0,0 +1,459 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:exif/src/exif_decode_makernote.dart';
import 'package:exif/src/exif_types.dart';
import 'package:exif/src/exifheader.dart';
import 'package:exif/src/file_interface.dart';
import 'package:exif/src/heic.dart';
import 'package:exif/src/linereader.dart';
import 'package:exif/src/reader.dart';
import 'package:exif/src/util.dart';
int _incrementBase(List<int> data, int base) {
return (data[base + 2]) * 256 + (data[base + 3]) + 2;
}
/// Process an image file data.
/// This is the function that has to deal with all the arbitrary nasty bits
/// of the EXIF standard.
Future<Map<String, IfdTag>> readExifFromBytes(List<int> bytes,
{String? stopTag,
bool details = true,
bool strict = false,
bool debug = false,
bool truncateTags = true}) async {
return readExifFromFileReader(FileReader.fromBytes(bytes),
stopTag: stopTag,
details: details,
strict: strict,
debug: debug,
truncateTags: truncateTags)
.tags;
}
/// Streaming version of [readExifFromBytes].
Future<Map<String, IfdTag>> readExifFromFile(File file,
{String? stopTag,
bool details = true,
bool strict = false,
bool debug = false,
bool truncateTags = true}) async {
final randomAccessFile = file.openSync();
final fileReader = await FileReader.fromFile(randomAccessFile);
final r = readExifFromFileReader(fileReader,
stopTag: stopTag,
details: details,
strict: strict,
debug: debug,
truncateTags: truncateTags);
randomAccessFile.closeSync();
return r.tags;
}
/// Process an image file (expects an open file object).
/// This is the function that has to deal with all the arbitrary nasty bits
/// of the EXIF standard.
ExifData readExifFromFileReader(FileReader f,
{String? stopTag,
bool details = true,
bool strict = false,
bool debug = false,
bool truncateTags = true}) {
ReadParams readParams;
// determine whether it's a JPEG or TIFF
final header = f.readSync(12);
if (_isTiff(header)) {
readParams = _tiffReadParams(f);
} else if (_isHeic(header) || _isAvif(header)) {
readParams = _heicReadParams(f);
} else if (_isJpeg(header)) {
readParams = _jpegReadParams(f);
} else if (_isPng(header)) {
readParams = _pngReadParams(f);
} else if (_isWebp(header)) {
readParams = _webpReadParams(f);
} else {
return ExifData.withWarning("File format not recognized.");
}
if (readParams.error != "") {
return ExifData.withWarning(readParams.error);
}
final file = IfdReader(Reader(f, readParams.offset, readParams.endian),
fakeExif: readParams.fakeExif);
final hdr = ExifHeader(
file: file,
strict: strict,
debug: debug,
detailed: details,
truncateTags: truncateTags);
final ifdList = file.listIfd();
ifdList.asMap().forEach((ifdIndex, ifd) {
hdr.dumpIfd(ifd, _ifdNameOfIndex(ifdIndex), stopTag: stopTag);
});
// EXIF IFD
final exifOff = hdr.tags['Image ExifOffset'];
if (exifOff != null && exifOff.tag.values is IfdInts) {
hdr.dumpIfd(exifOff.tag.values.firstAsInt(), 'EXIF', stopTag: stopTag);
}
if (details) {
DecodeMakerNote(hdr.tags, hdr.file, hdr.dumpIfd).decode();
}
if (details && ifdList.length >= 2) {
hdr.extractTiffThumbnail(ifdList[1]);
hdr.extractJpegThumbnail();
}
// parse XMP tags (experimental)
if (debug && details) {
_parseXmpTags(f, hdr);
}
return ExifData(
hdr.tags.map((key, value) => MapEntry(key, value.tag)), hdr.warnings);
}
String _ifdNameOfIndex(int index) {
if (index == 0) {
return 'Image';
} else if (index == 1) {
return 'Thumbnail';
} else {
return 'IFD $index';
}
}
void _parseXmpTags(FileReader f, ExifHeader hdr) {
String xmpString = '';
// Easy we already have them
final imageApplicationNotes = hdr.tags['Image ApplicationNotes'];
if (imageApplicationNotes != null) {
// XMP present in Exif
final tag = imageApplicationNotes.tag;
xmpString =
(tag is IfdInts) ? makeString((tag as IfdInts).ints) : tag.toString();
// We need to look in the entire file for the XML
} else {
// XMP not in Exif, searching file for XMP info...
bool xmlStarted = false;
bool xmlFinished = false;
final reader = LineReader(f);
while (true) {
String line = reader.readLine();
if (line.isEmpty) break;
final openTag = line.indexOf('<x:xmpmeta');
final closeTag = line.indexOf('</x:xmpmeta>');
if (openTag != -1) {
xmlStarted = true;
line = line.substring(openTag);
// printf('** XMP found opening tag at line position %s', [open_tag]);
}
if (closeTag != -1) {
// printf('** XMP found closing tag at line position %s', [close_tag]);
int lineOffset = 0;
if (openTag != -1) {
lineOffset = openTag;
}
line = line.substring(0, (closeTag - lineOffset) + 12);
xmlFinished = true;
}
if (xmlStarted) {
xmpString += line;
}
if (xmlFinished) {
break;
}
}
// print('** XMP Finished searching for info');
if (xmpString.isNotEmpty) {
hdr.parseXmp(xmpString);
}
}
}
bool _isTiff(List<int> header) =>
header.length >= 4 &&
listContainedIn(
header.sublist(0, 4), ['II*\x00'.codeUnits, 'MM\x00*'.codeUnits]);
bool _isHeic(List<int> header) =>
listRangeEqual(header, 4, 12, 'ftypheic'.codeUnits);
bool _isAvif(List<int> header) =>
listRangeEqual(header, 4, 12, 'ftypavif'.codeUnits);
bool _isJpeg(List<int> header) =>
listRangeEqual(header, 0, 2, '\xFF\xD8'.codeUnits);
bool _isPng(List<int> header) =>
listRangeEqual(header, 0, 8, '\x89PNG\r\n\x1a\n'.codeUnits);
bool _isWebp(List<int> header) =>
listRangeEqual(header, 0, 4, 'RIFF'.codeUnits) &&
listRangeEqual(header, 8, 12, 'WEBP'.codeUnits);
ReadParams _heicReadParams(FileReader f) {
f.setPositionSync(0);
final heic = HEICExifFinder(f);
final res = heic.findExif();
if (res.length != 2) {
return ReadParams.error("Possibly corrupted heic data");
}
final int offset = res[0];
final Endian endian = Reader.endianOfByte(res[1]);
return ReadParams(endian: endian, offset: offset);
}
ReadParams _jpegReadParams(FileReader f) {
// by default do not fake an EXIF beginning
var fakeExif = false;
int offset;
Endian endian;
f.setPositionSync(0);
const headerLength = 12;
var data = f.readSync(headerLength);
if (data.length != headerLength) {
return ReadParams.error("File format not recognized.");
}
var base = 2;
while (data[2] == 0xFF &&
listContainedIn(data.sublist(6, 10), [
'JFIF'.codeUnits,
'JFXX'.codeUnits,
'OLYM'.codeUnits,
'Phot'.codeUnits
])) {
final length = data[4] * 256 + data[5];
// printf("** Length offset is %d", [length]);
f.readSync(length - 8);
// fake an EXIF beginning of file
// I don't think this is used. --gd
data = [0xFF, 0x00];
data.addAll(f.readSync(10));
fakeExif = true;
if (base > 2) {
// print("** Added to base");
base = base + length + 4 - 2;
} else {
// print("** Added to zero");
base = length + 4;
}
// printf("** Set segment base to 0x%X", [base]);
}
// Big ugly patch to deal with APP2 (or other) data coming before APP1
f.setPositionSync(0);
// in theory, this could be insufficient since 64K is the maximum size--gd
// print('** f.position=${f.positionSync()}, base=$base');
data = f.readSync(base + 4000);
// print('** data.length=${data.length}');
// base = 2
while (true) {
// print('** base=$base');
// if (data.length == 4020) {
// print("** data.length=${data.length}, base=$base");
// }
if (listRangeEqual(data, base, base + 2, [0xFF, 0xE1])) {
// APP1
// print("** APP1 at base $base");
// print("** Length: (${data[base + 2]}, ${data[base + 3]})");
// print("** Code: ${new String.fromCharCodes(data.sublist(base + 4,base + 8))}");
if (listRangeEqual(data, base + 4, base + 8, "Exif".codeUnits)) {
// print("** Decrement base by 2 to get to pre-segment header (for compatibility with later code)");
base -= 2;
break;
}
base += _incrementBase(data, base);
} else if (listRangeEqual(data, base, base + 2, [0xFF, 0xE0])) {
// APP0
// print("** APP0 at base $base");
// printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]);
// printf("** Code: %s", [data.sublist(base + 4, base + 8)]);
base += _incrementBase(data, base);
} else if (listRangeEqual(data, base, base + 2, [0xFF, 0xE2])) {
// APP2
// printf("** APP2 at base 0x%X", [base]);
// printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]);
// printf("** Code: %s", [data.sublist(base + 4,base + 8)]);
base += _incrementBase(data, base);
} else if (listRangeEqual(data, base, base + 2, [0xFF, 0xEE])) {
// APP14
// printf("** APP14 Adobe segment at base 0x%X", [base]);
// printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]);
// printf("** Code: %s", [data.sublist(base + 4,base + 8)]);
base += _incrementBase(data, base);
// print("** There is useful EXIF-like data here, but we have no parser for it.");
} else if (listRangeEqual(data, base, base + 2, [0xFF, 0xDB])) {
// printf("** JPEG image data at base 0x%X No more segments are expected.", [base]);
break;
} else if (listRangeEqual(data, base, base + 2, [0xFF, 0xD8])) {
// APP12
// printf("** FFD8 segment at base 0x%X", [base]);
// printf("** Got 0x%X 0x%X and %s instead", [data[base], data[base + 1], data.sublist(4 + base,10 + base)]);
// printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]);
// printf("** Code: %s", [data.sublist(base + 4,base + 8)]);
base += _incrementBase(data, base);
} else if (listRangeEqual(data, base, base + 2, [0xFF, 0xEC])) {
// APP12
// printf("** APP12 XMP (Ducky) or Pictureinfo segment at base 0x%X", [base]);
// printf("** Got 0x%X and 0x%X instead", [data[base], data[base + 1]]);
// printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]);
// printf("** Code: %s", [data.sublist(base + 4,base + 8)]);
base += _incrementBase(data, base);
// print("** There is useful EXIF-like data here (quality, comment, copyright), but we have no parser for it.");
} else {
try {
base += _incrementBase(data, base);
} on RangeError {
return ReadParams.error(
"Unexpected/unhandled segment type or file content.");
}
}
}
f.setPositionSync(base + 12);
if (data[2 + base] == 0xFF &&
listRangeEqual(data, 6 + base, 10 + base, 'Exif'.codeUnits)) {
// detected EXIF header
offset = f.positionSync();
endian = Reader.endianOfByte(f.readByteSync());
//HACK TEST: endian = 'M'
} else if (data[2 + base] == 0xFF &&
listRangeEqual(data, 6 + base, 10 + base + 1, 'Ducky'.codeUnits)) {
// detected Ducky header.
// printf("** EXIF-like header (normally 0xFF and code): 0x%X and %s",
// [data[2 + base], data.sublist(6 + base,10 + base + 1)]);
offset = f.positionSync();
endian = Reader.endianOfByte(f.readByteSync());
} else if (data[2 + base] == 0xFF &&
listRangeEqual(data, 6 + base, 10 + base + 1, 'Adobe'.codeUnits)) {
// detected APP14 (Adobe);
// printf("** EXIF-like header (normally 0xFF and code): 0x%X and %s",
// [data[2 + base], data.sublist(6 + base,10 + base + 1)]);
offset = f.positionSync();
endian = Reader.endianOfByte(f.readByteSync());
} else {
// print("** No EXIF header expected data[2+base]==0xFF and data[6+base:10+base]===Exif (or Duck)");
// printf("** Did get 0x%X and %s",
// [data[2 + base], data.sublist(6 + base,10 + base + 1)]);
return ReadParams.error("No EXIF information found");
}
return ReadParams(endian: endian, offset: offset, fakeExif: fakeExif);
}
ReadParams _pngReadParams(FileReader f) {
f.setPositionSync(8);
while (true) {
final data = f.readSync(8);
final chunk = String.fromCharCodes(data.sublist(4, 8));
if (chunk.isEmpty || chunk == "IEND") {
break;
}
if (chunk == "eXIf") {
final offset = f.positionSync();
final endian = Reader.endianOfByte(f.readByteSync());
return ReadParams(endian: endian, offset: offset);
}
final chunkSize =
Int8List.fromList(data.sublist(0, 4)).buffer.asByteData().getInt32(0);
f.setPositionSync(f.positionSync() + chunkSize + 4);
}
return ReadParams.error("No EXIF information found");
}
ReadParams _webpReadParams(FileReader f) {
// Each RIFF box is a 4-byte ASCII tag, followed by a little-endian uint32
// length, and finally that number of bytes of data. The file starts with an
// outer box with the tag 'RIFF', whose content is the file format ('WEBP')
// followed by a series of inner boxes. We need the inner 'EXIF' box.
//
// The outer box encapsulates the entire file, so we can safely skip forward
// to the first inner box.
f.setPositionSync(12);
while (true) {
final header = f.readSync(8);
if (header.isEmpty) {
return ReadParams.error("No EXIF information found");
} else if (header.length < 8) {
return ReadParams.error("Invalid RIFF encoding");
}
final tag = String.fromCharCodes(header.sublist(0, 4));
final length = Int8List.fromList(header.sublist(4, 8))
.buffer
.asByteData()
.getInt32(0, Endian.little);
// According to exiftool's RIFF documentation, WebP uses "EXIF" as tag
// name while other RIFF-based files tend to use "Exif".
if (tag == "EXIF") {
// Look for Exif\x00\x00, and skip it if present. The WebP implementation
// in Exiv2 also handles a \xFF\x01\xFF\xE1\x00\x00 prefix, but with no
// explanation or test file present, so we ignore that for now.
final exifHeader = f.readSync(6);
if (!listEqual(
exifHeader, Uint8List.fromList('Exif\x00\x00'.codeUnits))) {
// There was no Exif\x00\x00 marker, rewind
f.setPositionSync(f.positionSync() - exifHeader.length);
}
final offset = f.positionSync();
final endian = Reader.endianOfByte(f.readByteSync());
return ReadParams(endian: endian, offset: offset);
}
// Skip forward to the next box.
f.setPositionSync(f.positionSync() + length);
}
}
ReadParams _tiffReadParams(FileReader f) {
f.setPositionSync(0);
final endian = Reader.endianOfByte(f.readByteSync());
f.readSync(1);
return ReadParams(endian: endian, offset: 0);
}
class ReadParams {
final bool fakeExif;
final Endian endian;
final int offset;
final String error;
ReadParams({
required this.endian,
required this.offset,
// by default do not fake an EXIF beginning
this.fakeExif = false,
}) : error = "";
ReadParams.error(this.error)
: endian = Endian.little,
offset = 0,
fakeExif = false;
}

251
exif/lib/src/reader.dart Normal file
View file

@ -0,0 +1,251 @@
import 'dart:typed_data';
import 'package:exif/src/exif_types.dart';
import 'package:exif/src/field_types.dart';
import 'package:exif/src/file_interface.dart';
import 'package:exif/src/makernote_canon.dart';
import 'package:exif/src/util.dart';
class Reader {
FileReader file;
int baseOffset;
Endian endian;
Reader(this.file, this.baseOffset, this.endian);
List<int> readSlice(int relativePos, int length) {
file.setPositionSync(baseOffset + relativePos);
return file.readSync(length);
}
// Convert slice to integer, based on sign and endian flags.
// Usually this offset is assumed to be relative to the beginning of the
// start of the EXIF information.
// For some cameras that use relative tags, this offset may be relative
// to some other starting point.
int readInt(int offset, int length, {bool signed = false}) {
final sliced = readSlice(offset, length);
int val;
if (endian == Endian.little) {
val = s2nLittleEndian(sliced, signed: signed);
} else {
val = s2nBigEndian(sliced, signed: signed);
}
return val;
}
Ratio readRatio(int offset, {required bool signed}) {
final n = readInt(offset, 4, signed: signed);
final d = readInt(offset + 4, 4, signed: signed);
return Ratio(n, d);
}
// Convert offset to string.
List<int> offsetToBytes(int readOffset, int length) {
final List<int> s = [];
for (int dummy = 0; dummy < length; dummy++) {
if (endian == Endian.little) {
s.add(readOffset & 0xFF);
} else {
s.insert(0, readOffset & 0xFF);
}
readOffset = readOffset >> 8;
}
return s;
}
static Endian endianOfByte(int b) {
if (b == 'I'.codeUnitAt(0)) {
return Endian.little;
}
return Endian.big;
}
}
class IfdReader {
Reader file;
final bool fakeExif;
IfdReader(this.file, {required this.fakeExif});
// Return first IFD.
int _firstIfd() => file.readInt(4, 4);
// Return the pointer to next IFD.
int _nextIfd(int ifd) {
final entries = file.readInt(ifd, 2);
final nextIfd = file.readInt(ifd + 2 + 12 * entries, 4);
if (nextIfd == ifd) {
return 0;
} else {
return nextIfd;
}
}
// Return the list of IFDs in the header.
List<int> listIfd() {
int i = _firstIfd();
final List<int> ifds = [];
while (i > 0) {
ifds.add(i);
i = _nextIfd(i);
}
return ifds;
}
List<IfdEntry> readIfdEntries(int ifd, {required bool relative}) {
final numEntries = file.readInt(ifd, 2);
return List<IfdEntry>.generate(numEntries, (i) {
// entry is index of start of this IFD in the file
final offset = ifd + 2 + 12 * i;
final tag = file.readInt(offset, 2);
final fieldType = FieldType.ofValue(file.readInt(offset + 2, 2));
final count = file.readInt(offset + 4, 4);
final typeLength = fieldType.length;
// Adjust for tag id/type/count (2+2+4 bytes)
// Now we point at either the data or the 2nd level offset
int fieldOffset = offset + 8;
// If the value fits in 4 bytes, it is inlined, else we
// need to jump ahead again.
if (count * typeLength > 4) {
// offset is not the value; it's a pointer to the value
// if relative we set things up so s2n will seek to the right
// place when it adds this.offset. Note that this 'relative'
// is for the Nikon type 3 makernote. Other cameras may use
// other relative offsets, which would have to be computed here
// slightly differently.
if (relative) {
fieldOffset = file.readInt(fieldOffset, 4) + ifd - 8;
if (fakeExif) {
fieldOffset += 18;
}
} else {
fieldOffset = file.readInt(fieldOffset, 4);
}
}
return IfdEntry(
fieldOffset: fieldOffset,
tag: tag,
fieldType: fieldType,
count: count);
});
}
Endian get endian => file.endian;
set endian(Endian e) {
file.endian = e;
}
int get baseOffset => file.baseOffset;
set baseOffset(int v) {
file.baseOffset = v;
}
int readInt(int offset, int length, {bool signed = false}) {
return file.readInt(offset, length, signed: signed);
}
List<int> readSlice(int relativePos, int length) {
return file.readSlice(relativePos, length);
}
IfdRatios _readIfdRatios(IfdEntry entry) {
final List<Ratio> values = [];
var pos = entry.fieldOffset;
for (int dummy = 0; dummy < entry.count; dummy++) {
values.add(file.readRatio(pos, signed: entry.fieldType.isSigned));
pos += entry.fieldType.length;
}
return IfdRatios(values);
}
IfdInts _readIfdInts(IfdEntry entry) {
final List<int> values = [];
var pos = entry.fieldOffset;
for (int dummy = 0; dummy < entry.count; dummy++) {
values.add(file.readInt(pos, entry.fieldType.length,
signed: entry.fieldType.isSigned));
pos += entry.fieldType.length;
}
return IfdInts(values);
}
IfdBytes _readAscii(IfdEntry entry) {
var count = entry.count;
// special case: null-terminated ASCII string
// XXX investigate
// sometimes gets too big to fit in int value
if (count <= 0) {
return IfdBytes.empty();
}
if (count > 1024 * 1024) {
count = 1024 * 1024;
}
try {
// and count < (2**31)) // 2E31 is hardware dependant. --gd
var values = file.readSlice(entry.fieldOffset, count);
// Drop any garbage after a null.
final i = values.indexOf(0);
if (i >= 0) {
values = values.sublist(0, i);
}
return IfdBytes(Uint8List.fromList(values));
} catch (e) {
// warnings.add("exception($e) at position: $filePosition, length: $count");
return IfdBytes.empty();
}
}
IfdValues readField(IfdEntry entry, {required String tagName}) {
if (entry.fieldType == FieldType.ascii) {
return _readAscii(entry);
}
// XXX investigate
// some entries get too big to handle could be malformed
// file or problem with this.s2n
if (entry.count < 1000) {
if (entry.fieldType == FieldType.ratio ||
entry.fieldType == FieldType.signedRatio) {
return _readIfdRatios(entry);
} else {
return _readIfdInts(entry);
}
// The test above causes problems with tags that are
// supposed to have long values! Fix up one important case.
} else if (tagName == 'MakerNote' ||
tagName == MakerNoteCanon.cameraInfoTagName) {
return _readIfdInts(entry);
}
return const IfdNone();
}
List<int> offsetToBytes(int readOffset, int length) {
return file.offsetToBytes(readOffset, length);
}
}
class IfdEntry {
final int fieldOffset;
final int tag;
final FieldType fieldType;
final int count;
IfdEntry({
required this.fieldOffset,
required this.tag,
required this.fieldType,
required this.count,
});
}

413
exif/lib/src/tags.dart Normal file
View file

@ -0,0 +1,413 @@
import 'package:exif/src/tags_info.dart'
show MakerTag, MakerTagFunc, TagsBase, MakerTagsWithName;
import 'package:exif/src/util.dart';
// Standard tag definitions.
class StandardTags extends TagsBase {
static MakerTag _make(String name) => MakerTag.make(name);
static MakerTag _withMap(String name, Map<int, String> map) =>
MakerTag.makeWithMap(name, map);
static MakerTag _withFunc(String name, MakerTagFunc func) =>
MakerTag.makeWithFunc(name, func);
static MakerTag _withTags(String name, MakerTagsWithName tags) =>
MakerTag.makeWithTags(name, tags);
// Interoperability tags
static final Map<int, MakerTag> _interopTags = {
0x0001: _make('InteroperabilityIndex'),
0x0002: _make('InteroperabilityVersion'),
0x1000: _make('RelatedImageFileFormat'),
0x1001: _make('RelatedImageWidth'),
0x1002: _make('RelatedImageLength'),
};
static final MakerTagsWithName _interopInfo =
MakerTagsWithName(name: 'Interoperability', tags: _interopTags);
// GPS tags
static final Map<int, MakerTag> _gpsTags = {
0x0000: _make('GPSVersionID'),
0x0001: _make('GPSLatitudeRef'),
0x0002: _make('GPSLatitude'),
0x0003: _make('GPSLongitudeRef'),
0x0004: _make('GPSLongitude'),
0x0005: _make('GPSAltitudeRef'),
0x0006: _make('GPSAltitude'),
0x0007: _make('GPSTimeStamp'),
0x0008: _make('GPSSatellites'),
0x0009: _make('GPSStatus'),
0x000A: _make('GPSMeasureMode'),
0x000B: _make('GPSDOP'),
0x000C: _make('GPSSpeedRef'),
0x000D: _make('GPSSpeed'),
0x000E: _make('GPSTrackRef'),
0x000F: _make('GPSTrack'),
0x0010: _make('GPSImgDirectionRef'),
0x0011: _make('GPSImgDirection'),
0x0012: _make('GPSMapDatum'),
0x0013: _make('GPSDestLatitudeRef'),
0x0014: _make('GPSDestLatitude'),
0x0015: _make('GPSDestLongitudeRef'),
0x0016: _make('GPSDestLongitude'),
0x0017: _make('GPSDestBearingRef'),
0x0018: _make('GPSDestBearing'),
0x0019: _make('GPSDestDistanceRef'),
0x001A: _make('GPSDestDistance'),
0x001B: _make('GPSProcessingMethod'),
0x001C: _make('GPSAreaInformation'),
0x001D: _make('GPSDate'),
0x001E: _make('GPSDifferential'),
};
static final MakerTagsWithName _gpsInfo =
MakerTagsWithName(name: 'GPS', tags: _gpsTags);
// Main Exif tag names
static final Map<int, MakerTag> tags = {
0x00FE: _withMap('SubfileType', {
0x0: 'Full-resolution Image',
0x1: 'Reduced-resolution image',
0x2: 'Single page of multi-page image',
0x3: 'Single page of multi-page reduced-resolution image',
0x4: 'Transparency mask',
0x5: 'Transparency mask of reduced-resolution image',
0x6: 'Transparency mask of multi-page image',
0x7: 'Transparency mask of reduced-resolution multi-page image',
0x10001: 'Alternate reduced-resolution image',
0xffffffff: 'invalid ',
}),
0x00FF: _withMap('OldSubfileType', {
1: 'Full-resolution image',
2: 'Reduced-resolution image',
3: 'Single page of multi-page image',
}),
0x0100: _make('ImageWidth'),
0x0101: _make('ImageLength'),
0x0102: _make('BitsPerSample'),
0x0103: _withMap('Compression', const {
1: 'Uncompressed',
2: 'CCITT 1D',
3: 'T4/Group 3 Fax',
4: 'T6/Group 4 Fax',
5: 'LZW',
6: 'JPEG (old-style)',
7: 'JPEG',
8: 'Adobe Deflate',
9: 'JBIG B&W',
10: 'JBIG Color',
32766: 'Next',
32769: 'Epson ERF Compressed',
32771: 'CCIRLEW',
32773: 'PackBits',
32809: 'Thunderscan',
32895: 'IT8CTPAD',
32896: 'IT8LW',
32897: 'IT8MP',
32898: 'IT8BL',
32908: 'PixarFilm',
32909: 'PixarLog',
32946: 'Deflate',
32947: 'DCS',
34661: 'JBIG',
34676: 'SGILog',
34677: 'SGILog24',
34712: 'JPEG 2000',
34713: 'Nikon NEF Compressed',
65000: 'Kodak DCR Compressed',
65535: 'Pentax PEF Compressed'
}),
0x0106: _make('PhotometricInterpretation'),
0x0107: _make('Thresholding'),
0x0108: _make('CellWidth'),
0x0109: _make('CellLength'),
0x010A: _make('FillOrder'),
0x010D: _make('DocumentName'),
0x010E: _make('ImageDescription'),
0x010F: _make('Make'),
0x0110: _make('Model'),
0x0111: _make('StripOffsets'),
0x0112: _withMap('Orientation', const {
1: 'Horizontal (normal)',
2: 'Mirrored horizontal',
3: 'Rotated 180',
4: 'Mirrored vertical',
5: 'Mirrored horizontal then rotated 90 CCW',
6: 'Rotated 90 CW',
7: 'Mirrored horizontal then rotated 90 CW',
8: 'Rotated 90 CCW'
}),
0x0115: _make('SamplesPerPixel'),
0x0116: _make('RowsPerStrip'),
0x0117: _make('StripByteCounts'),
0x0118: _make('MinSampleValue'),
0x0119: _make('MaxSampleValue'),
0x011A: _make('XResolution'),
0x011B: _make('YResolution'),
0x011C: _make('PlanarConfiguration'),
0x011D: _withFunc('PageName', makeString),
0x011E: _make('XPosition'),
0x011F: _make('YPosition'),
0x0122: _withMap('GrayResponseUnit', const {
1: '0.1',
2: '0.001',
3: '0.0001',
4: '1e-05',
5: '1e-06',
}),
0x0123: _make('GrayResponseCurve'),
0x0124: _make('T4Options'),
0x0125: _make('T6Options'),
0x0128: _withMap('ResolutionUnit',
const {1: 'Not Absolute', 2: 'Pixels/Inch', 3: 'Pixels/Centimeter'}),
0x0129: _make('PageNumber'),
0x012C: _make('ColorResponseUnit'),
0x012D: _make('TransferFunction'),
0x0131: _make('Software'),
0x0132: _make('DateTime'),
0x013B: _make('Artist'),
0x013C: _make('HostComputer'),
0x013D:
_withMap('Predictor', const {1: 'None', 2: 'Horizontal differencing'}),
0x013E: _make('WhitePoint'),
0x013F: _make('PrimaryChromaticities'),
0x0140: _make('ColorMap'),
0x0141: _make('HalftoneHints'),
0x0142: _make('TileWidth'),
0x0143: _make('TileLength'),
0x0144: _make('TileOffsets'),
0x0145: _make('TileByteCounts'),
0x0146: _make('BadFaxLines'),
0x0147: _withMap(
'CleanFaxData', const {0: 'Clean', 1: 'Regenerated', 2: 'Unclean'}),
0x0148: _make('ConsecutiveBadFaxLines'),
0x014C: _withMap('InkSet', const {1: 'CMYK', 2: 'Not CMYK'}),
0x014D: _make('InkNames'),
0x014E: _make('NumberofInks'),
0x0150: _make('DotRange'),
0x0151: _make('TargetPrinter'),
0x0152: _withMap('ExtraSamples', const {
0: 'Unspecified',
1: 'Associated Alpha',
2: 'Unassociated Alpha'
}),
0x0153: _withMap('SampleFormat', const {
1: 'Unsigned',
2: 'Signed',
3: 'Float',
4: 'Undefined',
5: 'Complex int',
6: 'Complex float'
}),
0x0154: _make('SMinSampleValue'),
0x0155: _make('SMaxSampleValue'),
0x0156: _make('TransferRange'),
0x0157: _make('ClipPath'),
0x0200: _make('JPEGProc'),
0x0201: _make('JPEGInterchangeFormat'),
0x0202: _make('JPEGInterchangeFormatLength'),
0x0211: _make('YCbCrCoefficients'),
0x0212: _make('YCbCrSubSampling'),
0x0213: _withMap('YCbCrPositioning', const {1: 'Centered', 2: 'Co-sited'}),
0x0214: _make('ReferenceBlackWhite'),
0x02BC: _make('ApplicationNotes'), // XPM Info
0x4746: _make('Rating'),
0x828D: _make('CFARepeatPatternDim'),
0x828E: _make('CFAPattern'),
0x828F: _make('BatteryLevel'),
0x8298: _make('Copyright'),
0x829A: _make('ExposureTime'),
0x829D: _make('FNumber'),
0x83BB: _make('IPTC/NAA'),
0x8769: _make('ExifOffset'), // Exif Tags
0x8773: _make('InterColorProfile'),
0x8822: _withMap('ExposureProgram', const {
0: 'Unidentified',
1: 'Manual',
2: 'Program Normal',
3: 'Aperture Priority',
4: 'Shutter Priority',
5: 'Program Creative',
6: 'Program Action',
7: 'Portrait Mode',
8: 'Landscape Mode'
}),
0x8824: _make('SpectralSensitivity'),
0x8825: _withTags('GPSInfo', _gpsInfo), // GPS tags
0x8827: _make('ISOSpeedRatings'),
0x8828: _make('OECF'),
0x8830: _withMap('SensitivityType', const {
0: 'Unknown',
1: 'Standard Output Sensitivity',
2: 'Recommended Exposure Index',
3: 'ISO Speed',
4: 'Standard Output Sensitivity and Recommended Exposure Index',
5: 'Standard Output Sensitivity and ISO Speed',
6: 'Recommended Exposure Index and ISO Speed',
7: 'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed'
}),
0x8832: _make('RecommendedExposureIndex'),
0x8833: _make('ISOSpeed'),
0x9000: _withFunc('ExifVersion', makeString),
0x9003: _make('DateTimeOriginal'),
0x9004: _make('DateTimeDigitized'),
0x9010: _make('OffsetTime'),
0x9011: _make('OffsetTimeOriginal'),
0x9012: _make('OffsetTimeDigitized'),
0x9101: _withMap('ComponentsConfiguration', const {
0: '',
1: 'Y',
2: 'Cb',
3: 'Cr',
4: 'Red',
5: 'Green',
6: 'Blue'
}),
0x9102: _make('CompressedBitsPerPixel'),
0x9201: _make('ShutterSpeedValue'),
0x9202: _make('ApertureValue'),
0x9203: _make('BrightnessValue'),
0x9204: _make('ExposureBiasValue'),
0x9205: _make('MaxApertureValue'),
0x9206: _make('SubjectDistance'),
0x9207: _withMap('MeteringMode', const {
0: 'Unidentified',
1: 'Average',
2: 'CenterWeightedAverage',
3: 'Spot',
4: 'MultiSpot',
5: 'Pattern',
6: 'Partial',
255: 'other'
}),
0x9208: _withMap('LightSource', const {
0: 'Unknown',
1: 'Daylight',
2: 'Fluorescent',
3: 'Tungsten (incandescent light)',
4: 'Flash',
9: 'Fine weather',
10: 'Cloudy weather',
11: 'Shade',
12: 'Daylight fluorescent (D 5700 - 7100K)',
13: 'Day white fluorescent (N 4600 - 5400K)',
14: 'Cool white fluorescent (W 3900 - 4500K)',
15: 'White fluorescent (WW 3200 - 3700K)',
17: 'Standard light A',
18: 'Standard light B',
19: 'Standard light C',
20: 'D55',
21: 'D65',
22: 'D75',
23: 'D50',
24: 'ISO studio tungsten',
255: 'other light source'
}),
0x9209: _withMap('Flash', const {
0: 'Flash did not fire',
1: 'Flash fired',
5: 'Strobe return light not detected',
7: 'Strobe return light detected',
9: 'Flash fired, compulsory flash mode',
13: 'Flash fired, compulsory flash mode, return light not detected',
15: 'Flash fired, compulsory flash mode, return light detected',
16: 'Flash did not fire, compulsory flash mode',
24: 'Flash did not fire, auto mode',
25: 'Flash fired, auto mode',
29: 'Flash fired, auto mode, return light not detected',
31: 'Flash fired, auto mode, return light detected',
32: 'No flash function',
65: 'Flash fired, red-eye reduction mode',
69: 'Flash fired, red-eye reduction mode, return light not detected',
71: 'Flash fired, red-eye reduction mode, return light detected',
73: 'Flash fired, compulsory flash mode, red-eye reduction mode',
77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
89: 'Flash fired, auto mode, red-eye reduction mode',
93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
95: 'Flash fired, auto mode, return light detected, red-eye reduction mode'
}),
0x920A: _make('FocalLength'),
0x9214: _make('SubjectArea'),
0x927C: _make('MakerNote'),
0x9286: _withFunc('UserComment', makeStringUc),
0x9290: _make('SubSecTime'),
0x9291: _make('SubSecTimeOriginal'),
0x9292: _make('SubSecTimeDigitized'),
// used by Windows Explorer
0x9C9B: _make('XPTitle'),
0x9C9C: _make('XPComment'),
0x9C9D: _withFunc('XPAuthor',
makeString), // const [gnored by Windows Explorer if Artist exists]
0x9C9E: _make('XPKeywords'),
0x9C9F: _make('XPSubject'),
0xA000: _withFunc('FlashPixVersion', makeString),
0xA001: _withMap(
'ColorSpace', const {1: 'sRGB', 2: 'Adobe RGB', 65535: 'Uncalibrated'}),
0xA002: _make('ExifImageWidth'),
0xA003: _make('ExifImageLength'),
0xA004: _make('RelatedSoundFile'),
0xA005: _withTags('InteroperabilityOffset', _interopInfo),
0xA20B: _make('FlashEnergy'), // 0x920B in TIFF/EP
0xA20C: _make('SpatialFrequencyResponse'), // 0x920C
0xA20E: _make('FocalPlaneXResolution'), // 0x920E
0xA20F: _make('FocalPlaneYResolution'), // 0x920F
0xA210: _make('FocalPlaneResolutionUnit'), // 0x9210
0xA214: _make('SubjectLocation'), // 0x9214
0xA215: _make('ExposureIndex'), // 0x9215
0xA217: _withMap('SensingMethod', const {
// 0x9217
1: 'Not defined',
2: 'One-chip color area',
3: 'Two-chip color area',
4: 'Three-chip color area',
5: 'Color sequential area',
7: 'Trilinear',
8: 'Color sequential linear'
}),
0xA300: _withMap('FileSource', const {
1: 'Film Scanner',
2: 'Reflection Print Scanner',
3: 'Digital Camera'
}),
0xA301: _withMap('SceneType', const {1: 'Directly Photographed'}),
0xA302: _make('CVAPattern'),
0xA401: _withMap('CustomRendered', const {0: 'Normal', 1: 'Custom'}),
0xA402: _withMap('ExposureMode',
const {0: 'Auto Exposure', 1: 'Manual Exposure', 2: 'Auto Bracket'}),
0xA403: _withMap('WhiteBalance', const {0: 'Auto', 1: 'Manual'}),
0xA404: _make('DigitalZoomRatio'),
0xA405: _make('FocalLengthIn35mmFilm'),
0xA406: _withMap('SceneCaptureType',
const {0: 'Standard', 1: 'Landscape', 2: 'Portrait', 3: 'Night]'}),
0xA407: _withMap('GainControl', const {
0: 'None',
1: 'Low gain up',
2: 'High gain up',
3: 'Low gain down',
4: 'High gain down'
}),
0xA408: _withMap('Contrast', const {0: 'Normal', 1: 'Soft', 2: 'Hard'}),
0xA409: _withMap('Saturation', const {0: 'Normal', 1: 'Soft', 2: 'Hard'}),
0xA40A: _withMap('Sharpness', const {0: 'Normal', 1: 'Soft', 2: 'Hard'}),
0xA40B: _make('DeviceSettingDescription'),
0xA40C: _make('SubjectDistanceRange'),
0xA420: _make('ImageUniqueID'),
0xA430: _make('CameraOwnerName'),
0xA431: _make('BodySerialNumber'),
0xA432: _make('LensSpecification'),
0xA433: _make('LensMake'),
0xA434: _make('LensModel'),
0xA435: _make('LensSerialNumber'),
0xA500: _make('Gamma'),
0xC4A5: _make('PrintIM'),
0xEA1C: _make('Padding'),
0xEA1D: _make('OffsetSchema'),
0xFDE8: _make('OwnerName'),
0xFDE9: _make('SerialNumber'),
};
}

View file

@ -0,0 +1,25 @@
typedef MakerTagFunc = String Function(List<int> list);
class MakerTag {
String name;
Map<int, String>? map;
MakerTagFunc? func;
MakerTagsWithName? tags;
MakerTag.make(this.name);
MakerTag.makeWithMap(this.name, this.map);
MakerTag.makeWithFunc(this.name, this.func);
MakerTag.makeWithTags(this.name, this.tags);
}
class MakerTagsWithName {
String name;
Map<int, MakerTag> tags;
MakerTagsWithName({this.name = "", this.tags = const {}});
}
class TagsBase {}

109
exif/lib/src/util.dart Normal file
View file

@ -0,0 +1,109 @@
import 'dart:math';
import 'package:collection/collection.dart' show ListEquality;
import 'package:sprintf/sprintf.dart' show sprintf;
bool listRangeEqual(List list1, int begin, int end, List list2) {
var beginIndex = begin >= 0 ? begin : 0;
beginIndex = beginIndex < list1.length ? beginIndex : list1.length;
var endIndex = end >= begin ? end : begin;
endIndex = endIndex < list1.length ? endIndex : list1.length;
return listEqual(list1.sublist(beginIndex, endIndex), list2);
}
final listEqual = const ListEquality().equals;
bool listHasPrefix(List list, List prefix, {int start = 0}) {
if (prefix.isEmpty) {
return true;
}
if (list.length - start < prefix.length) {
return false;
}
return listEqual(list.sublist(start, start + prefix.length), prefix);
}
bool listContainedIn<T>(List<T> a, List<List<T>> b) =>
b.any((i) => listEqual(i, a));
void printf(String a, List b) => print(sprintf(a, b));
// Don't throw an exception when given an out of range character.
String makeString(List<int> seq) {
String s = String.fromCharCodes(seq.where((c) => 32 <= c && c < 256));
if (s.isEmpty) {
if (seq.isEmpty || seq.reduce(max) == 0) {
return "";
}
s = seq.map((e) => e.toString()).join();
}
return s.trim();
}
// Special version to deal with the code in the first 8 bytes of a user comment.
// First 8 bytes gives coding system e.g. ASCII vs. JIS vs Unicode.
String makeStringUc(List<int> seq) {
if (seq.length <= 8) {
return "";
}
// Remove code from sequence only if it is valid
if ({'ASCII', 'UNICODE', 'JIS', ''}
.contains(makeString(seq.sublist(0, 8)).toUpperCase())) {
seq = seq.sublist(8);
}
// Of course, this is only correct if ASCII, and the standard explicitly
// allows JIS and Unicode.
return makeString(seq);
}
// Extract multi-byte integer in little endian.
int s2nBigEndian(List<int> s, {bool signed = false}) {
if (s.isEmpty) {
return 0;
}
int xor = 0;
if (signed && s[0] >= 128) {
xor = 0xff;
}
int x = 0;
for (final c in s) {
x = (x << 8) | (c ^ xor);
}
if (xor != 0) {
x = -(x + 1);
}
return x;
}
// Extract multi-byte integer in little endian.
int s2nLittleEndian(List<int> s, {bool signed = false}) {
if (s.isEmpty) {
return 0;
}
int xor = 0;
if (signed && s.last >= 128) {
xor = 0xff;
}
int x = 0;
int y = 0;
for (final int c in s) {
x = x | ((c ^ xor) << y);
y += 8;
}
if (xor != 0) {
x = -(x + 1);
}
return x;
}

View file

@ -0,0 +1,84 @@
import 'dart:convert';
import 'package:exif/src/exif_types.dart';
import 'package:exif/src/field_types.dart';
import 'package:exif/src/reader.dart';
import 'package:exif/src/tags_info.dart';
class ValuesToPrintable {
final String value;
final bool malformed;
const ValuesToPrintable(this.value) : malformed = false;
const ValuesToPrintable.malformed(this.value) : malformed = true;
factory ValuesToPrintable.convert(IfdValues values, IfdEntry entry,
{required MakerTag? tagEntry, required bool truncateTags}) {
// compute printable version of values
if (tagEntry != null) {
// optional 2nd tag element is present
if (tagEntry.func != null) {
// call mapping function
final printable =
tagEntry.func!(values.toList().whereType<int>().toList());
return ValuesToPrintable(printable);
} else if (tagEntry.map != null) {
final sb = StringBuffer();
for (final i in values.toList()) {
// use lookup table for this tag
if (i is int) {
sb.write(tagEntry.map![i] ?? i);
} else {
sb.write(i);
}
}
return ValuesToPrintable(sb.toString());
}
}
if (entry.fieldType == FieldType.ascii && values is IfdBytes) {
final bytes = values.bytes;
try {
return ValuesToPrintable(utf8.decode(bytes));
} on FormatException {
if (truncateTags && bytes.length > 20) {
return ValuesToPrintable.malformed(
'b"${bytesToStringRepr(bytes.sublist(0, 20))}, ... ]');
}
return ValuesToPrintable.malformed("b'${bytesToStringRepr(bytes)}'");
}
} else if (entry.count == 1) {
return ValuesToPrintable(values.toList()[0].toString());
}
if (entry.count > 50 && values.length > 20) {
if (truncateTags) {
final s = values.toList().sublist(0, 20).toString();
return ValuesToPrintable("${s.substring(0, s.length - 1)}, ... ]");
}
}
return ValuesToPrintable(values.toString());
}
static String bytesToStringRepr(List<int> bytes) => bytes.map((e) {
switch (e) {
case 9:
return r'\t';
case 10:
return r'\n';
case 13:
return r'\r';
case 92:
return r'\\';
}
if (e < 32 || e >= 128) {
final hex = e.toRadixString(16).padLeft(2, '0');
return "\\x$hex";
}
return String.fromCharCode(e);
}).join();
}

25
exif/pubspec.yaml Normal file
View file

@ -0,0 +1,25 @@
name: exif
version: 3.3.0
description: >-
Decode Exif metadata from digital image files.
Supported formats: TIFF, JPEG, HEIC, PNG, WebP
homepage: https://www.github.com/bigflood/dartexif
environment:
sdk: '>=2.12.0 <4.0.0'
dependencies:
args: ^2.0.0
collection: ^1.15.0
convert: ^3.0.0
json_annotation: ^4.3.0
sprintf: ^7.0.0
dev_dependencies:
archive: ^3.1.2
build_runner: ^2.1.4
http: '>=1.0.0 <2.0.0'
json_serializable: ^6.0.0
lints: ^2.0.1
path: ^1.8.0
stream_channel: ^2.1.0
test: ^1.16.8
executables:
print_exif:

Binary file not shown.

View file

@ -0,0 +1,59 @@
EXIF ApertureValue (Ratio): 4845/1918
EXIF BrightnessValue (Signed Ratio): 7187/850
EXIF ColorSpace (Short): Uncalibrated
EXIF ComponentsConfiguration (Undefined): YCbCr
EXIF CustomRendered (Short): 2
EXIF DateTimeDigitized (ASCII): 2018:03:30 12:14:19
EXIF DateTimeOriginal (ASCII): 2018:03:30 12:14:19
EXIF ExifImageLength (Long): 180
EXIF ExifImageWidth (Long): 240
EXIF ExifVersion (Undefined): 0221
EXIF ExposureBiasValue (Signed Ratio): 0
EXIF ExposureMode (Short): Auto Exposure
EXIF ExposureProgram (Short): Program Normal
EXIF ExposureTime (Ratio): 1/209
EXIF FNumber (Ratio): 12/5
EXIF Flash (Short): Flash did not fire, compulsory flash mode
EXIF FlashPixVersion (Undefined): 0100
EXIF FocalLength (Ratio): 6
EXIF FocalLengthIn35mmFilm (Short): 52
EXIF ISOSpeedRatings (Short): 16
EXIF LensMake (ASCII): Apple
EXIF LensModel (ASCII): iPhone X back dual camera 6mm f/2.4
EXIF LensSpecification (Ratio): [4, 6, 9/5, 12/5]
EXIF MakerNote (Undefined): []
EXIF MeteringMode (Short): Pattern
EXIF SceneCaptureType (Short): Standard
EXIF SceneType (Undefined): Directly Photographed
EXIF SensingMethod (Short): One-chip color area
EXIF ShutterSpeedValue (Signed Ratio): 7789/1011
EXIF SubSecTimeDigitized (ASCII): 365
EXIF SubSecTimeOriginal (ASCII): 365
EXIF SubjectArea (Short): [2007, 1503, 2209, 1327]
EXIF WhiteBalance (Short): Auto
GPS GPSAltitude (Ratio): 6568/1433
GPS GPSAltitudeRef (Byte): 0
GPS GPSDate (ASCII): 2018:03:30
GPS GPSDestBearing (Ratio): 21278/339
GPS GPSDestBearingRef (ASCII): M
GPS GPSImgDirection (Ratio): 21278/339
GPS GPSImgDirectionRef (ASCII): M
GPS GPSLatitude (Ratio): [37, 45, 907/25]
GPS GPSLatitudeRef (ASCII): N
GPS GPSLongitude (Ratio): [122, 30, 861/25]
GPS GPSLongitudeRef (ASCII): W
GPS GPSSpeed (Ratio): 5709/10354
GPS GPSSpeedRef (ASCII): K
GPS GPSTimeStamp (Ratio): [19, 14, 1813/100]
GPS Tag 0x001F (Ratio): 6619/1103
Image DateTime (ASCII): 2018:03:30 12:14:19
Image ExifOffset (Long): 204
Image GPSInfo (Long): 784
Image Make (ASCII): Apple
Image Model (ASCII): iPhone X
Image Orientation (Short): Horizontal (normal)
Image ResolutionUnit (Short): Pixels/Inch
Image Software (ASCII): 12.0
Image XResolution (Ratio): 127/5
Image YCbCrPositioning (Short): Centered
Image YResolution (Ratio): 127/5

Binary file not shown.

View file

@ -0,0 +1,80 @@
EXIF ApertureValue (Ratio): 4845/1918
EXIF BrightnessValue (Signed Ratio): 7187/850
EXIF ColorSpace (Short): Uncalibrated
EXIF ComponentsConfiguration (Undefined): YCbCr
EXIF CustomRendered (Short): 2
EXIF DateTimeDigitized (ASCII): 2018:03:30 12:14:19
EXIF DateTimeOriginal (ASCII): 2018:03:30 12:14:19
EXIF ExifImageLength (Long): 3024
EXIF ExifImageWidth (Long): 4032
EXIF ExifVersion (Undefined): 0221
EXIF ExposureBiasValue (Signed Ratio): 0
EXIF ExposureMode (Short): Auto Exposure
EXIF ExposureProgram (Short): Program Normal
EXIF ExposureTime (Ratio): 1/209
EXIF FNumber (Ratio): 12/5
EXIF Flash (Short): Flash did not fire, compulsory flash mode
EXIF FlashPixVersion (Undefined): 0100
EXIF FocalLength (Ratio): 6
EXIF FocalLengthIn35mmFilm (Short): 52
EXIF ISOSpeedRatings (Short): 16
EXIF LensMake (ASCII): Apple
EXIF LensModel (ASCII): iPhone X back dual camera 6mm f/2.4
EXIF LensSpecification (Ratio): [4, 6, 9/5, 12/5]
EXIF MakerNote (Undefined): [65, 112, 112, 108, 101, 32, 105, 79, 83, 0, 0, 1, 77, 77, 0, 21, 0, 1, 0, 9, ... ]
EXIF MeteringMode (Short): Pattern
EXIF SceneCaptureType (Short): Standard
EXIF SceneType (Undefined): Directly Photographed
EXIF SensingMethod (Short): One-chip color area
EXIF ShutterSpeedValue (Signed Ratio): 7789/1011
EXIF SubSecTimeDigitized (ASCII): 365
EXIF SubSecTimeOriginal (ASCII): 365
EXIF SubjectArea (Short): [2007, 1503, 2209, 1327]
EXIF WhiteBalance (Short): Auto
GPS GPSAltitude (Ratio): 6568/1433
GPS GPSAltitudeRef (Byte): 0
GPS GPSDate (ASCII): 2018:03:30
GPS GPSDestBearing (Ratio): 21278/339
GPS GPSDestBearingRef (ASCII): M
GPS GPSImgDirection (Ratio): 21278/339
GPS GPSImgDirectionRef (ASCII): M
GPS GPSLatitude (Ratio): [37, 45, 907/25]
GPS GPSLatitudeRef (ASCII): N
GPS GPSLongitude (Ratio): [122, 30, 861/25]
GPS GPSLongitudeRef (ASCII): W
GPS GPSSpeed (Ratio): 5709/10354
GPS GPSSpeedRef (ASCII): K
GPS GPSTimeStamp (Ratio): [19, 14, 1813/100]
GPS Tag 0x001F (Ratio): 6619/1103
Image DateTime (ASCII): 2018:03:30 12:14:19
Image ExifOffset (Long): 204
Image GPSInfo (Long): 1818
Image Make (ASCII): Apple
Image Model (ASCII): iPhone X
Image Orientation (Short): Rotated 180
Image ResolutionUnit (Short): Pixels/Inch
Image Software (ASCII): 12.0
Image XResolution (Ratio): 72
Image YCbCrPositioning (Short): Centered
Image YResolution (Ratio): 72
MakerNote HDRImageType (Signed Long): 2
MakerNote Tag 0x0001 (Signed Long): 10
MakerNote Tag 0x0002 (Undefined): [2, 1, 122, 0, 107, 0, 120, 0, 67, 0, 69, 0, 59, 0, 57, 0, 28, 1, 85, 2, ... ]
MakerNote Tag 0x0003 (Undefined): [6, 7, 8, 85, 102, 108, 97, 103, 115, 85, 118, 97, 108, 117, 101, 89, 116, 105, 109, 101, ... ]
MakerNote Tag 0x0004 (Signed Long): 1
MakerNote Tag 0x0005 (Signed Long): 173
MakerNote Tag 0x0006 (Signed Long): 170
MakerNote Tag 0x0007 (Signed Long): 1
MakerNote Tag 0x0008 (Signed Ratio): [-69926911/22675456, 22145/723, 256/7305]
MakerNote Tag 0x000C (Signed Ratio): [2100775/105295456, 1/200]
MakerNote Tag 0x000D (Signed Long): 25
MakerNote Tag 0x000E (Signed Long): 4
MakerNote Tag 0x0010 (Signed Long): 1
MakerNote Tag 0x0014 (Signed Long): 3
MakerNote Tag 0x0017 (Signed Long): 2048
MakerNote Tag 0x0019 (Signed Long): 34
MakerNote Tag 0x001A (ASCII): C55AB7
MakerNote Tag 0x001D (Signed Ratio): 218813911/175351561
MakerNote Tag 0x001F (Signed Long): 1
MakerNote Tag 0x0020 (ASCII): 48FC-9F8C-9CCF675F4C62
MakerNote Tag 0x0021 (Signed Ratio): 1/6

BIN
exif/test/data/png-test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View file

@ -0,0 +1,13 @@
EXIF ComponentsConfiguration (Undefined): YCbCr
EXIF ExifVersion (Undefined): 0232
EXIF FlashPixVersion (Undefined): 0100
EXIF UserComment (Undefined): exif test
GPS GPSLatitude (Ratio): [44, 29, 76078/1397]
GPS GPSLatitudeRef (ASCII): N
GPS GPSLongitude (Ratio): [11, 19, 48947/1171]
GPS GPSLongitudeRef (ASCII): E
Image ExifOffset (Long): 88
Image GPSInfo (Long): 160
Image ImageDescription (ASCII): Flutter Dash
Image ResolutionUnit (Short): Pixels/Inch
Image YCbCrPositioning (Short): Centered

0
exif/test/data/test-data Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,59 @@
EXIF ApertureValue (Ratio): 4845/1918
EXIF BrightnessValue (Signed Ratio): 7187/850
EXIF ColorSpace (Short): Uncalibrated
EXIF ComponentsConfiguration (Undefined): YCbCr
EXIF CustomRendered (Short): 2
EXIF DateTimeDigitized (ASCII): 2018:03:30 12:14:19
EXIF DateTimeOriginal (ASCII): 2018:03:30 12:14:19
EXIF ExifImageLength (Long): 180
EXIF ExifImageWidth (Long): 240
EXIF ExifVersion (Undefined): 0221
EXIF ExposureBiasValue (Signed Ratio): 0
EXIF ExposureMode (Short): Auto Exposure
EXIF ExposureProgram (Short): Program Normal
EXIF ExposureTime (Ratio): 1/209
EXIF FNumber (Ratio): 12/5
EXIF Flash (Short): Flash did not fire, compulsory flash mode
EXIF FlashPixVersion (Undefined): 0100
EXIF FocalLength (Ratio): 6
EXIF FocalLengthIn35mmFilm (Short): 52
EXIF ISOSpeedRatings (Short): 16
EXIF LensMake (ASCII): Apple
EXIF LensModel (ASCII): iPhone X back dual camera 6mm f/2.4
EXIF LensSpecification (Ratio): [4, 6, 9/5, 12/5]
EXIF MakerNote (Undefined): []
EXIF MeteringMode (Short): Pattern
EXIF SceneCaptureType (Short): Standard
EXIF SceneType (Undefined): Directly Photographed
EXIF SensingMethod (Short): One-chip color area
EXIF ShutterSpeedValue (Signed Ratio): 7789/1011
EXIF SubSecTimeDigitized (ASCII): 365
EXIF SubSecTimeOriginal (ASCII): 365
EXIF SubjectArea (Short): [2007, 1503, 2209, 1327]
EXIF WhiteBalance (Short): Auto
GPS GPSAltitude (Ratio): 6568/1433
GPS GPSAltitudeRef (Byte): 0
GPS GPSDate (ASCII): 2018:03:30
GPS GPSDestBearing (Ratio): 21278/339
GPS GPSDestBearingRef (ASCII): M
GPS GPSImgDirection (Ratio): 21278/339
GPS GPSImgDirectionRef (ASCII): M
GPS GPSLatitude (Ratio): [37, 45, 907/25]
GPS GPSLatitudeRef (ASCII): N
GPS GPSLongitude (Ratio): [122, 30, 861/25]
GPS GPSLongitudeRef (ASCII): W
GPS GPSSpeed (Ratio): 5709/10354
GPS GPSSpeedRef (ASCII): K
GPS GPSTimeStamp (Ratio): [19, 14, 1813/100]
GPS Tag 0x001F (Ratio): 6619/1103
Image DateTime (ASCII): 2018:03:30 12:14:19
Image ExifOffset (Long): 204
Image GPSInfo (Long): 784
Image Make (ASCII): Apple
Image Model (ASCII): iPhone X
Image Orientation (Short): Horizontal (normal)
Image ResolutionUnit (Short): Pixels/Inch
Image Software (ASCII): 12.0
Image XResolution (Ratio): 127/5
Image YCbCrPositioning (Short): Centered
Image YResolution (Ratio): 127/5

View file

@ -0,0 +1,52 @@
@TestOn("vm")
import "dart:io" as io;
import 'package:exif/exif.dart';
import "package:test/test.dart";
void main() {
test("read heic file test", () async {
const filename = "test/data/heic-test.heic";
final file = io.File(filename);
final output = tagsToString(await readExifFromFile(file));
final expected = await io.File("$filename.dump").readAsString();
expect(output, equals(expected.trim()));
});
test("read png file test", () async {
const filename = "test/data/png-test.png";
final file = io.File(filename);
final output = tagsToString(await readExifFromFile(file));
final expected = await io.File("$filename.dump").readAsString();
expect(output, equals(expected.trim()));
});
test("read avif file test", () async {
const filename = "test/data/avif-test.avif";
final file = io.File(filename);
final output = tagsToString(await readExifFromFile(file));
final expected = await io.File("$filename.dump").readAsString();
expect(output, equals(expected.trim()));
});
test("read webp file test", () async {
const filename = "test/data/webp-test.webp";
final file = io.File(filename);
final output = tagsToString(await readExifFromFile(file));
final expected = await io.File("$filename.dump").readAsString();
expect(output, equals(expected.trim()));
});
}
String tagsToString(Map<String, IfdTag> tags) {
final tagKeys = tags.keys.toList();
tagKeys.sort();
final prints = [];
for (final key in tagKeys) {
final tag = tags[key];
prints.add("$key (${tag!.tagType}): $tag");
}
return prints.join("\n");
}

View file

@ -0,0 +1,94 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
import 'sample_file.dart';
Stream<SampleFile> readSamples() async* {
yield* readIanareSamples();
yield await readSampleFile("test/data/heic-test.heic");
}
Future<SampleFile> readSampleFile(String filename) async {
final fileBytes = await io.File(filename).readAsBytes();
final dump = await io.File("$filename.dump").readAsString();
return SampleFile(
name: filename,
content: fileBytes,
dump: dump.trim(),
);
}
Stream<SampleFile> readIanareSamples() async* {
const commit = "2a62d69683c154ffe03b4502bdfa3248d8a1b05c";
final filenamePrefix = p.join("test", "data", "$commit-");
final dumpFile = await downloadUrl(
filenamePrefix,
"https://raw.githubusercontent.com/ianare/exif-samples/$commit/dump",
);
final nameToDumps = readDumpFile(dumpFile);
final path = await downloadUrl(
filenamePrefix,
"https://github.com/ianare/exif-samples/archive/$commit.tar.gz",
);
final data = io.File(path).readAsBytesSync();
final ar = TarDecoder().decodeBytes(GZipDecoder().decodeBytes(data));
for (final file in ar) {
file.name =
file.name.replaceAll("exif-samples-$commit", "exif-samples-master");
if (!file.name.endsWith('.jpg') && !file.name.endsWith('.tiff')) {
continue;
}
if (!nameToDumps.containsKey(file.name)) {
file.name = utf8.decode(file.name.codeUnits);
}
yield SampleFile(
name: file.name,
content: file.content as Uint8List,
dump: nameToDumps[file.name],
);
}
}
Map<String, String> readDumpFile(String dumpFile) {
final fileDumps = io.File(dumpFile).readAsStringSync().trim().split("\n\n");
final nameAndDumps = fileDumps.map((e) => e.split("\n")).map((e) => MapEntry(
e[0].split("Opening: ")[1],
e
.sublist(1)
.where((e) =>
!e.startsWith("Possibly corrupted ") &&
!e.startsWith("No values found for "))
.join("\n")));
return Map.fromEntries(nameAndDumps);
}
Future<String> downloadUrl(String filenamePrefix, String url) async {
final filename = filenamePrefix + Uri.parse(url).pathSegments.last;
if (!await io.File(filename).exists()) {
print('downloading $filename ..');
final res = await http.get(Uri.parse(url));
await io.File(filename).writeAsBytes(res.bodyBytes);
}
return filename;
}

View file

@ -0,0 +1,23 @@
import 'package:exif/exif.dart';
import "package:test/test.dart";
void main() {
test("range error", () async {
final data = [
'',
'\xFF',
'\xFF\xD8',
'\xFF\xD8abc',
'II',
'II*\x00',
'II*\x00ftypheic',
'MM',
'MM\x00*',
];
for (final x in data) {
final exifDump = await printExifOfBytes(x.codeUnits);
expect(exifDump, equals("No EXIF information found"), reason: x);
}
});
}

View file

@ -0,0 +1,25 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:json_annotation/json_annotation.dart';
part 'sample_file.g.dart';
@JsonSerializable()
class SampleFile {
String name;
String encodedContent = "";
String? dump;
List<int> getContent() => base64.decode(encodedContent);
SampleFile({this.name = "", this.dump = "", Uint8List? content}) {
if (content != null) {
encodedContent = base64.encode(content);
}
}
factory SampleFile.fromJson(Map<String, dynamic> json) =>
_$SampleFileFromJson(json);
Map<String, dynamic> toJson() => _$SampleFileToJson(this);
}

View file

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sample_file.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SampleFile _$SampleFileFromJson(Map<String, dynamic> json) => SampleFile(
name: json['name'] as String? ?? "",
dump: json['dump'] as String? ?? "",
)..encodedContent = json['encodedContent'] as String;
Map<String, dynamic> _$SampleFileToJson(SampleFile instance) =>
<String, dynamic>{
'name': instance.name,
'encodedContent': instance.encodedContent,
'dump': instance.dump,
};

View file

@ -0,0 +1,14 @@
@TestOn("vm")
import 'package:exif/exif.dart';
import 'package:test/test.dart';
import 'read_samples.dart';
Future main() async {
await for (final file in readSamples()) {
test(file.name, () async {
final exifDump = await printExifOfBytes(file.getContent());
expect(exifDump, equals(file.dump));
});
}
}

11
exif/test/util_test.dart Normal file
View file

@ -0,0 +1,11 @@
import 'package:exif/src/util.dart';
import "package:test/test.dart";
void main() {
test("make_string_uc", () {
expect(makeStringUc([]), equals(""));
expect(makeStringUc([1, 2, 3, 4, 5, 6, 7]), equals(""));
expect(makeStringUc([1, 2, 3, 4, 5, 6, 7, 8, 97, 98, 99]), equals("abc"));
expect(makeString([0, 2, 0, 0]), equals("0200"));
});
}

View file

@ -0,0 +1,13 @@
import 'dart:convert';
import "package:stream_channel/stream_channel.dart";
import 'read_samples.dart';
Future hybridMain(StreamChannel channel) async {
await for (final file in readSamples()) {
channel.sink.add(const JsonEncoder().convert(file));
}
channel.sink.close();
}

21
exif/test/web_test.dart Normal file
View file

@ -0,0 +1,21 @@
@TestOn("browser")
import "dart:convert";
import 'package:exif/exif.dart';
import "package:test/test.dart";
import "sample_file.dart";
void main() {
test("run hybrid main", () async {
final channel = spawnHybridUri("web_hybrid_main.dart");
await for (final msg in channel.stream) {
final file = SampleFile.fromJson(
json.decode(msg as String) as Map<String, dynamic>);
print(file.name);
expect(await printExifOfBytes(file.getContent()), equals(file.dump),
reason: "file=${file.name}");
}
}, timeout: Timeout.parse("60s"));
}

View file

@ -1,41 +1,5 @@
dependency_overrides: dependency_overrides:
adaptive_number: exif:
path: ./dependencies/adaptive_number path: ./dependencies/exif
dots_indicator: sprintf:
path: ./dependencies/dots_indicator path: ./dependencies/sprintf
ed25519_edwards:
path: ./dependencies/ed25519_edwards
flutter_markdown_plus:
path: ./dependencies/flutter_markdown_plus
flutter_sharing_intent:
path: ./dependencies/flutter_sharing_intent
hand_signature:
path: ./dependencies/hand_signature
hashlib:
path: ./dependencies/hashlib
hashlib_codecs:
path: ./dependencies/hashlib_codecs
introduction_screen:
path: ./dependencies/introduction_screen
libsignal_protocol_dart:
path: ./dependencies/libsignal_protocol_dart
lottie:
path: ./dependencies/lottie
mutex:
path: ./dependencies/mutex
optional:
path: ./dependencies/optional
photo_view:
path: ./dependencies/photo_view
pointycastle:
path: ./dependencies/pointycastle
qr:
path: ./dependencies/qr
qr_flutter:
path: ./dependencies/qr_flutter
restart_app:
path: ./dependencies/restart_app
screen_protector:
path: ./dependencies/screen_protector
x25519:
path: ./dependencies/x25519

22
sprintf/LICENSE Normal file
View file

@ -0,0 +1,22 @@
Copyright (c) 2012, Richard Eames
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

13
sprintf/lib/sprintf.dart Normal file
View file

@ -0,0 +1,13 @@
library sprintf;
import 'dart:math';
part 'src/formatters/Formatter.dart';
part 'src/formatters/int_formatter.dart';
part 'src/formatters/float_formatter.dart';
part 'src/formatters/string_formatter.dart';
part 'src/sprintf_impl.dart';
//typedef SPrintF = String Function(String fmt, args);
var sprintf = PrintFormat();

View file

@ -0,0 +1,25 @@
part of sprintf;
abstract class Formatter {
var fmt_type;
var options;
Formatter(this.fmt_type, this.options);
static String get_padding(int count, String pad) {
var padding_piece = pad;
var padding = StringBuffer();
while (count > 0) {
if ((count & 1) == 1) {
padding.write(padding_piece);
}
count >>= 1;
padding_piece = '${padding_piece}${padding_piece}';
}
return padding.toString();
}
String asString();
}

View file

@ -0,0 +1,302 @@
part of sprintf;
class FloatFormatter extends Formatter {
// ignore: todo
// TODO: can't rely on '.' being the decimal separator
static final _number_rx = RegExp(r'^[\-\+]?(\d+)\.(\d+)$');
static final _expo_rx = RegExp(r'^[\-\+]?(\d)\.(\d+)e([\-\+]?\d+)$');
static final _leading_zeroes_rx = RegExp(r'^(0*)[1-9]+');
double _arg;
final List<int> _digits = [];
int _exponent = 0;
int _decimal = 0;
bool _is_negative = false;
bool _has_init = false;
String? _output;
FloatFormatter(this._arg, var fmt_type, var options)
: super(fmt_type, options) {
if (_arg.isNaN) {
_has_init = true;
return;
}
if (_arg.isInfinite) {
_is_negative = _arg.isNegative;
_has_init = true;
return;
}
_arg = _arg.toDouble();
if (_arg < 0) {
_is_negative = true;
_arg = -_arg;
}
var arg_str =
_arg == _arg.truncate() ? _arg.toStringAsFixed(1) : _arg.toString();
var m1 = _number_rx.firstMatch(arg_str);
if (m1 != null) {
var int_part = m1.group(1)!;
var fraction = m1.group(2)!;
/*
* Cases:
* 1.2345 = 1.2345e0 -> [12345] e+0 d1 l5
* 123.45 = 1.2345e2 -> [12345] e+2 d3 l5
* 0.12345 = 1.2345e-1 -> [012345] e-1 d1 l6
* 0.0012345 = 1.2345e-3 -> [00012345] e-3 d1 l8
*/
_decimal = int_part.length;
_digits.addAll(int_part.split('').map(int.parse));
_digits.addAll(fraction.split('').map(int.parse));
if (int_part.length == 1) {
if (int_part == '0') {
var leading_zeroes_match = _leading_zeroes_rx.firstMatch(fraction);
if (leading_zeroes_match != null) {
var zeroes_count = leading_zeroes_match.group(1)!.length;
// print("zeroes_count=${zeroes_count}");
_exponent =
zeroes_count > 0 ? -(zeroes_count + 1) : zeroes_count - 1;
} else {
_exponent = 0;
}
} // else int_part != 0
else {
_exponent = 0;
}
} else {
_exponent = int_part.length - 1;
}
} else {
var m2 = _expo_rx.firstMatch(arg_str);
if (m2 != null) {
var int_part = m2.group(1)!;
var fraction = m2.group(2)!;
_exponent = int.parse(m2.group(3)!);
if (_exponent > 0) {
var diff = _exponent - fraction.length + 1;
_decimal = _exponent + 1;
_digits.addAll(int_part.split('').map(int.parse));
_digits.addAll(fraction.split('').map(int.parse));
_digits.addAll(
Formatter.get_padding(diff, '0').split('').map(int.parse));
} else {
var diff = int_part.length - _exponent - 1;
_decimal = int_part.length;
_digits.addAll(
Formatter.get_padding(diff, '0').split('').map(int.parse));
_digits.addAll(int_part.split('').map(int.parse));
_digits.addAll(fraction.split('').map(int.parse));
}
} // else something wrong
}
_has_init = true;
//print("arg_str=${arg_str}");
//print("decimal=${_decimal}, exp=${_exponent}, digits=${_digits}");
}
@override
String asString() {
var ret = '';
if (!_has_init) {
return ret;
}
if (_output != null) {
return _output!;
}
if (options['add_space'] && options['sign'] == '' && _arg >= 0) {
options['sign'] = ' ';
}
if (_arg.isInfinite) {
if (_arg.isNegative) {
options['sign'] = '-';
}
ret = 'inf';
options['padding_char'] = ' ';
}
if (_arg.isNaN) {
ret = 'nan';
options['padding_char'] = ' ';
}
if (options['precision'] == -1) {
// ignore: todo
options['precision'] = 6; // TODO: make this configurable
} else if (fmt_type == 'g' && options['precision'] == 0) {
options['precision'] = 1;
}
if (_is_negative) {
options['sign'] = '-';
}
if (!(_arg.isInfinite || _arg.isNaN)) {
if (fmt_type == 'e') {
ret = asExponential(options['precision'], remove_trailing_zeros: false);
} else if (fmt_type == 'f') {
ret = asFixed(options['precision'], remove_trailing_zeros: false);
} else {
// type == g
var _exp = _exponent;
var sig_digs = options['precision'];
// print("${_exp} ${sig_digs}");
if (-4 <= _exp && _exp < options['precision']) {
sig_digs -= _decimal;
var precision = max<num>(options['precision'] - 1 - _exp, sig_digs);
ret = asFixed(precision.toInt(),
remove_trailing_zeros: !options['alternate_form']);
} else {
ret = asExponential(options['precision'] - 1,
remove_trailing_zeros: !options['alternate_form']);
}
}
}
var min_chars = options['width'];
var str_len = ret.length + options['sign'].length;
var padding = '';
if (min_chars > str_len) {
if (options['padding_char'] == '0' && !options['left_align']) {
padding = Formatter.get_padding(min_chars - str_len, '0');
} else {
padding = Formatter.get_padding(min_chars - str_len, ' ');
}
}
if (options['left_align']) {
ret = "${options['sign']}${ret}${padding}";
} else if (options['padding_char'] == '0') {
ret = "${options['sign']}${padding}${ret}";
} else {
ret = "${padding}${options['sign']}${ret}";
}
if (options['is_upper']) {
ret = ret.toUpperCase();
}
return (_output = ret);
}
String asFixed(int precision, {bool remove_trailing_zeros = true}) {
// precision is the number of decimal places after the decimal point to keep
var offset = _decimal + precision - 1;
var extra_zeroes = precision - (_digits.length - offset);
if (extra_zeroes > 0) {
_digits.addAll(
Formatter.get_padding(extra_zeroes, '0').split('').map(int.parse));
}
_round(offset + 1, offset);
var ret = _digits.sublist(0, _decimal).fold('', (i, e) => '${i}${e}');
var trailing_digits = _digits.sublist(_decimal, _decimal + precision);
if (remove_trailing_zeros) {
trailing_digits = _remove_trailing_zeros(trailing_digits);
}
var trailing_zeroes = trailing_digits.fold('', (i, e) => '${i}${e}');
if (trailing_zeroes.isEmpty) {
return ret;
}
ret = '${ret}.${trailing_zeroes}';
return ret;
}
String asExponential(int precision, {bool remove_trailing_zeros = true}) {
var offset = _decimal - _exponent;
var extra_zeroes = precision - (_digits.length - offset) + 1;
if (extra_zeroes > 0) {
_digits.addAll(
Formatter.get_padding(extra_zeroes, '0').split('').map(int.parse));
}
_round(offset + precision, offset);
var ret = _digits[offset - 1].toString();
//print ("(${offset}, ${precision})${_digits}");
var trailing_digits = _digits.sublist(offset, offset + precision);
// print ("trailing_digits=${trailing_digits}");
var _exp_str = _exponent.abs().toString();
if (_exponent < 10 && _exponent > -10) {
_exp_str = '0${_exp_str}';
}
_exp_str = (_exponent < 0) ? 'e-${_exp_str}' : 'e+${_exp_str}';
if (remove_trailing_zeros) {
trailing_digits = _remove_trailing_zeros(trailing_digits);
}
if (trailing_digits.isNotEmpty) {
ret += '.';
}
ret = trailing_digits.fold(ret, (i, e) => '${i}${e}');
ret = '${ret}${_exp_str}';
return ret;
}
List<int> _remove_trailing_zeros(List<int> trailing_digits) {
var nzeroes = 0;
for (var i = trailing_digits.length - 1; i >= 0; i--) {
if (trailing_digits[i] == 0) {
nzeroes++;
} else {
break;
}
}
return trailing_digits.sublist(0, trailing_digits.length - nzeroes);
}
/*
rounding_offset: Where to start rounding from
offset: where to end rounding
*/
void _round(var rounding_offset, var offset) {
var carry = 0;
if (rounding_offset >= _digits.length) {
return;
}
// Round the digit after the precision
var d = _digits[rounding_offset];
carry = d >= 5 ? 1 : 0;
_digits[rounding_offset] = d % 10;
rounding_offset -= 1;
//propagate the carry
while (carry > 0) {
d = _digits[rounding_offset] + carry;
if (rounding_offset == 0 && d > 9) {
_digits.insert(0, 0);
_decimal += 1;
rounding_offset += 1;
}
carry = d < 10 ? 0 : 1;
_digits[rounding_offset] = d % 10;
rounding_offset -= 1;
}
}
}

View file

@ -0,0 +1,92 @@
part of sprintf;
class IntFormatter extends Formatter {
int _arg;
static const int MAX_INT = 0x1FFFFFFFFFFFFF; // javascript 53bit
IntFormatter(this._arg, var fmt_type, var options) : super(fmt_type, options);
@override
String asString() {
var ret = '';
var prefix = '';
var radix = fmt_type == 'x' ? 16 : (fmt_type == 'o' ? 8 : 10);
if (_arg < 0) {
if (radix == 10) {
_arg = _arg.abs();
options['sign'] = '-';
} else {
// sort of reverse twos complement
_arg = (MAX_INT - (~_arg) & MAX_INT);
}
}
ret = _arg.toRadixString(radix);
if (options['alternate_form']) {
if (radix == 16 && _arg != 0) {
prefix = '0x';
} else if (radix == 8 && _arg != 0) {
prefix = '0';
}
if (options['sign'] == '+' && radix != 10) {
options['sign'] = '';
}
}
// space "prefixes non-negative signed numbers with a space"
if ((options['add_space'] &&
options['sign'] == '' &&
_arg > -1 &&
radix == 10)) {
options['sign'] = ' ';
}
if (radix != 10) {
options['sign'] = '';
}
var padding = '';
var min_digits = options['precision'];
var min_chars = options['width'];
var num_length = ret.length;
var sign_length = options['sign'].length;
num str_len = 0;
if (radix == 8 && min_chars <= min_digits) {
num_length += prefix.length;
}
if (min_digits > num_length) {
padding = Formatter.get_padding(min_digits - num_length, '0');
ret = '${padding}${ret}';
num_length = ret.length;
padding = '';
}
str_len = num_length + sign_length + prefix.length;
if (min_chars > str_len) {
if (options['padding_char'] == '0' && !options['left_align']) {
padding = Formatter.get_padding(min_chars - str_len, '0');
} else {
padding = Formatter.get_padding(min_chars - str_len, ' ');
}
}
if (options['left_align']) {
ret = "${options['sign']}${prefix}${ret}${padding}";
} else if (options['padding_char'] == '0') {
ret = "${options['sign']}${prefix}${padding}${ret}";
} else {
ret = "${padding}${options['sign']}${prefix}${ret}";
}
if (options['is_upper']) {
ret = ret.toUpperCase();
}
return ret;
}
}

View file

@ -0,0 +1,32 @@
part of sprintf;
class StringFormatter extends Formatter {
var _arg;
StringFormatter(this._arg, var fmt_type, var options)
: super(fmt_type, options) {
options['padding_char'] = ' ';
}
@override
String asString() {
var ret = _arg.toString();
if (options['precision'] > -1 && options['precision'] <= ret.length) {
ret = ret.substring(0, options['precision']);
}
if (options['width'] > -1) {
int diff = (options['width'] - ret.length);
if (diff > 0) {
var padding = Formatter.get_padding(diff, options['padding_char']);
if (!options['left_align']) {
ret = '${padding}${ret}';
} else {
ret = '${ret}${padding}';
}
}
}
return ret;
}
}

View file

@ -0,0 +1,120 @@
part of sprintf;
typedef PrintFormatFormatter = Formatter Function(dynamic arg, dynamic options);
//typedef Formatter PrintFormatFormatter(arg, options);
class PrintFormat {
static final RegExp specifier = RegExp(
r'%(?:(\d+)\$)?([\+\-\#0 ]*)(\d+|\*)?(?:\.(\d+|\*))?([a-z%])',
caseSensitive: false);
static final RegExp uppercase_rx = RegExp(r'[A-Z]', caseSensitive: true);
final Map<String, PrintFormatFormatter> _formatters = {
'i': (arg, options) => IntFormatter(arg, 'i', options),
'd': (arg, options) => IntFormatter(arg, 'd', options),
'x': (arg, options) => IntFormatter(arg, 'x', options),
'X': (arg, options) => IntFormatter(arg, 'x', options),
'o': (arg, options) => IntFormatter(arg, 'o', options),
'O': (arg, options) => IntFormatter(arg, 'o', options),
'e': (arg, options) => FloatFormatter(arg, 'e', options),
'E': (arg, options) => FloatFormatter(arg, 'e', options),
'f': (arg, options) => FloatFormatter(arg, 'f', options),
'F': (arg, options) => FloatFormatter(arg, 'f', options),
'g': (arg, options) => FloatFormatter(arg, 'g', options),
'G': (arg, options) => FloatFormatter(arg, 'g', options),
's': (arg, options) => StringFormatter(arg, 's', options),
};
String call(String fmt, var args) {
var ret = '';
var offset = 0;
var arg_offset = 0;
if (args is! List) {
throw ArgumentError('Expecting list as second argument');
}
for (var m in specifier.allMatches(fmt)) {
var _parameter = m[1];
var _flags = m[2]!;
var _width = m[3];
var _precision = m[4];
var _type = m[5]!;
var _arg_str = '';
var _options = {
'is_upper': false,
'width': -1,
'precision': -1,
'length': -1,
'radix': 10,
'sign': '',
'specifier_type': _type,
};
_parse_flags(_flags).forEach((var k, var v) {
_options[k] = v;
});
// The argument we want to deal with
var _arg = _parameter == null ? null : args[int.parse(_parameter) - 1];
// parse width
if (_width != null) {
_options['width'] =
(_width == '*' ? args[arg_offset++] : int.parse(_width));
}
// parse precision
if (_precision != null) {
_options['precision'] =
(_precision == '*' ? args[arg_offset++] : int.parse(_precision));
}
// grab the argument we'll be dealing with
if (_arg == null && _type != '%') {
_arg = args[arg_offset++];
}
_options['is_upper'] = uppercase_rx.hasMatch(_type);
if (_type == '%') {
if (_flags.isNotEmpty || _width != null || _precision != null) {
throw Exception('"%" does not take any flags');
}
_arg_str = '%';
} else if (_formatters.containsKey(_type)) {
_arg_str = _formatters[_type]!(_arg, _options).asString();
} else {
throw ArgumentError('Unknown format type ${_type}');
}
// Add the pre-format string to the return
ret += fmt.substring(offset, m.start);
offset = m.end;
ret += _arg_str;
}
return ret += fmt.substring(offset);
}
void register_specifier(String specifier, PrintFormatFormatter formatter) {
_formatters[specifier] = formatter;
}
void unregistier_specifier(String specifier) {
_formatters.remove(specifier);
}
Map _parse_flags(String flags) {
return {
'sign': flags.contains('+') ? '+' : '',
'padding_char': flags.contains('0') ? '0' : ' ',
'add_space': flags.contains(' '),
'left_align': flags.contains('-'),
'alternate_form': flags.contains('#'),
};
}
}

12
sprintf/pubspec.yaml Normal file
View file

@ -0,0 +1,12 @@
name: sprintf
version: 7.0.0
description: Dart implementation of sprintf. Provides simple printf like
formatting such as sprintf("hello %s", ["world"]);
homepage: https://github.com/Naddiseo/dart-sprintf
environment:
sdk: ">=2.12.0-0 <3.0.0"
dev_dependencies:
test: ^1.16.0-nullsafety.13
lints: ^1.0.1

View file

@ -0,0 +1,225 @@
library sprintf_test;
import 'package:test/test.dart';
import 'package:sprintf/sprintf.dart';
part 'testing_data.dart';
void test_testdata() {
expectedTestData.forEach((prefix, type_map) {
group('"%$prefix Tests, ', () {
type_map.forEach((type, expected_array) {
var fmt = '|%${prefix}${type}|';
var input_array = expectedTestInputData[type]!;
assert(input_array.length == expected_array.length);
for (var i = 0; i < input_array.length - 1; i++) {
var raw_input = input_array[i];
var expected = expected_array[i];
final input = raw_input is! List ? [raw_input] : raw_input;
if (expected == '"throwsA"') {
test('Expecting "${fmt}".format(${raw_input}) to throw',
() => expect(() => sprintf(fmt, input), throwsA(anything)));
} else {
test('"${fmt}".format(${raw_input}) == "${expected}"',
() => expect(sprintf(fmt, input), expected));
}
}
}); // type_map
}); // group
}); // _expected
}
void test_bug0001() {
test('|%x|%X| 255', () => expect(sprintf('|%x|%X|', [255, 255]), '|ff|FF|'));
}
void test_bug0006a() {
test('|%.0f| 5.466', () => expect(sprintf('|%.0f|', [5.466]), '|5|'));
test('|%.0g| 5.466', () => expect(sprintf('|%.0g|', [5.466]), '|5|'));
test('|%.0e| 5.466', () => expect(sprintf('|%.0e|', [5.466]), '|5e+00|'));
}
void test_bug0006b() {
test('|%.2f| 5.466', () => expect(sprintf('|%.2f|', [5.466]), '|5.47|'));
test('|%.2g| 5.466', () => expect(sprintf('|%.2g|', [5.466]), '|5.5|'));
test('|%.2e| 5.466', () => expect(sprintf('|%.2e|', [5.466]), '|5.47e+00|'));
}
void test_bug0009() {
test('|%.2f| 2.09846', () => expect(sprintf('|%.2f|', [2.09846]), '|2.10|'));
}
void test_bug0010() {
test('|%.1f| 5.34', () => expect(sprintf('|%.1f|', [5.34]), '|5.3|'));
test('|%.1f| 22.51', () => expect(sprintf('|%.1f|', [22.51]), '|22.5|'));
test('|%.0f| 22.5', () => expect(sprintf('|%.0f|', [22.5]), '|23|'));
test('|%.0f| 22.77', () => expect(sprintf('|%.0f|', [22.77]), '|23|'));
}
void test_javascript_decimal_limit() {
test(
'%d 9007199254740991',
() => expect(
sprintf('|%d|', [9007199254740991 + 0]), '|9007199254740991|'));
//test('%d 9007199254740992', () => expect(sprintf('|%d|', [9007199254740991+1]), '|0|'));
//test('%d 9007199254740993', () => expect(sprintf('|%d|', [9007199254740991+2]), '|1|'));
test(
'%x 9007199254740991',
() =>
expect(sprintf('|%x|', [9007199254740991 + 0]), '|1fffffffffffff|'));
//test('%x 9007199254740992', () => expect(sprintf('|%x|', [9007199254740991+1]), '|0|'));
//test('%x 9007199254740993', () => expect(sprintf('|%x|', [9007199254740991+2]), '|1|'));
test('%x -9007199254740991',
() => expect(sprintf('|%x|', [-9007199254740991 + 0]), '|1|'));
//test('%x -9007199254740992', () => expect(sprintf('|%x|', [-9007199254740991+1]), '|2|'));
//test('%x -9007199254740993', () => expect(sprintf('|%x|', [-9007199254740991+2]), '|3|'));
}
void test_unsigned_neg_to_53bits() {
test('|%x|%X| -0', () => expect(sprintf('|%x|%X|', [-0, -0]), '|0|0|'));
test(
'|%x|%X| -1',
() => expect(
sprintf('|%x|%X|', [-1, -1]), '|1fffffffffffff|1FFFFFFFFFFFFF|'));
test(
'|%x|%X| -2',
() => expect(
sprintf('|%x|%X|', [-2, -2]), '|1ffffffffffffe|1FFFFFFFFFFFFE|'));
}
void test_int_formatting() {
test('|%+d|% d| 2', () => expect(sprintf('|%+d|% d|', [2, 2]), '|+2| 2|'));
test('|%+d|% d| -2', () => expect(sprintf('|%+d|% d|', [-2, -2]), '|-2|-2|'));
test(
'|%+x|% X|%#x| -2',
() => expect(sprintf('|%+x|% X|%#x|', [-2, -2, -2]),
'|1ffffffffffffe|1FFFFFFFFFFFFE|0x1ffffffffffffe|'));
}
void test_large_exponents_e() {
test('|%e| 1.79e+308',
() => expect(sprintf('|%e|', [1.79e+308]), '|1.790000e+308|'));
test('|%e| 1.79e-308',
() => expect(sprintf('|%e|', [1.79e-308]), '|1.790000e-308|'));
test('|%e| -1.79e+308',
() => expect(sprintf('|%e|', [-1.79e+308]), '|-1.790000e+308|'));
test('|%e| -1.79e-308',
() => expect(sprintf('|%e|', [-1.79e-308]), '|-1.790000e-308|'));
}
void test_large_exponents_g() {
test('|%g| 1.79e+308',
() => expect(sprintf('|%g|', [1.79e+308]), '|1.79e+308|'));
test('|%g| 1.79e-308',
() => expect(sprintf('|%g|', [1.79e-308]), '|1.79e-308|'));
test('|%g| -1.79e+308',
() => expect(sprintf('|%g|', [-1.79e+308]), '|-1.79e+308|'));
test('|%g| -1.79e-308',
() => expect(sprintf('|%g|', [-1.79e-308]), '|-1.79e-308|'));
}
void test_large_exponents_f() {
// ignore: todo
// TODO: C's printf introduces errors after 20 decimal places
test('|%f| 1.79e+308',
() => expect(sprintf('|%.3f|', [1.79e+308]), '|1.79e+308|'));
test('|%f| 1.79e-308',
() => expect(sprintf('|%f|', [1.79e-308]), '|1.790000e-308|'));
test('|%f| -1.79e+308',
() => expect(sprintf('|%f|', [-1.79e+308]), '|-1.790000e+308|'));
test('|%f| -1.79e-308',
() => expect(sprintf('|%f|', [-1.79e-308]), '|-1.790000e-308|'));
}
void test_object_to_string() {
var list = ['foo', 'bar'];
test("|%s| ['foo', 'bar'].toString()",
() => expect(sprintf('%s', [list]), '[foo, bar]'));
}
void test_round_bug0015() {
var n = 1;
test('|%.0f| 1', () => expect(sprintf('|%.0f|', [n]), '|1|'));
test('|%.1f| 1', () => expect(sprintf('|%.1f|', [n]), '|1.0|'));
test('|%.2f| 1', () => expect(sprintf('|%.2f|', [n]), '|1.00|'));
test('|%.0f| 1.234', () => expect(sprintf('|%.0f|', [1.234]), '|1|'));
test('|%.1f| 1.234', () => expect(sprintf('|%.1f|', [1.234]), '|1.2|'));
test('|%.2f| 1.234', () => expect(sprintf('|%.2f|', [1.234]), '|1.23|'));
test('|%.0f| 1.235', () => expect(sprintf('|%.0f|', [1.235]), '|1|'));
test('|%.1f| 1.235', () => expect(sprintf('|%.1f|', [1.235]), '|1.2|'));
test('|%.2f| 1.235', () => expect(sprintf('|%.2f|', [1.235]), '|1.24|'));
}
void test_bug0018() {
test(
'|%10.4f| 1.0', () => expect(sprintf('|%10.4f|', [1.0]), '| 1.0000|'));
}
void test_bug0022() {
test('|%2\$d %2\$d %1\$d|',
() => expect(sprintf('|%2\$d %2\$d %1\$d|', [5, 10]), '|10 10 5|'));
// these next two are from the sprintf manual, and should print the same
test('|%*d|', () => expect(sprintf('|%*d|', [5, 10]), '| 10|'));
test('|%2\$*1\$d|', () => expect(sprintf('|%*d|', [5, 10]), '| 10|'));
}
void test_bug0033() {
var inf = 1.0 / 0.0;
var nan = 0.0 / 0.0;
test(
'|%g %G| Infinity',
() => expect(sprintf('|%g %g %G %G|', [inf, -inf, inf, -inf]),
'|inf -inf INF -INF|'));
test(
'|%g %G| NaN', () => expect(sprintf('|%g %G|', [nan, nan]), '|nan NAN|'));
test(
'|%f %F| Infinity',
() => expect(sprintf('|%f %f %F %F|', [inf, -inf, inf, -inf]),
'|inf -inf INF -INF|'));
test(
'|%f %F| NaN', () => expect(sprintf('|%f %F|', [nan, nan]), '|nan NAN|'));
}
void main() {
test_bug0022();
test('|%6.6g -1.79e+20',
() => expect(sprintf('|%6.6g|', [-1.79E+20]), '|-1.79e+20|'));
test('|%6.6G -1.79e+20',
() => expect(sprintf('|%6.6G|', [-1.79E+20]), '|-1.79E+20|'));
test_bug0018();
//test_bug0009();
//test_bug0010();
//test('|%f| 1.79E+308', () => expect(sprintf('|%f|', [1.79e+308]), '|1.79e+308|'));
test_unsigned_neg_to_53bits();
test_int_formatting();
test_javascript_decimal_limit();
if (true) {
test_testdata();
test_large_exponents_e();
test_large_exponents_g();
//test_large_exponents_f();
test_bug0001();
test_bug0006a();
test_bug0006b();
test_bug0009();
test_bug0010();
test_object_to_string();
}
test_bug0033();
}

12140
sprintf/test/testing_data.dart Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,18 @@
import yaml # pip install PyYAML import yaml # pip install PyYAML
import os import os
import shutil import shutil
import subprocess import subprocess
import argparse
# Set up command-line argument parsing
parser = argparse.ArgumentParser(description="Update specific or all repositories.")
parser.add_argument('repo_name', nargs='?', default=None, help="Name of the repository to update (optional)")
args = parser.parse_args()
with open("config.yaml", "r") as f: with open("config.yaml", "r") as f:
config = yaml.safe_load(f) config = yaml.safe_load(f)
config_lock = {} config_lock = {}
LOCK_FILE_NAME = "config.lock.yaml" LOCK_FILE_NAME = "config.lock.yaml"
if os.path.exists(LOCK_FILE_NAME): if os.path.exists(LOCK_FILE_NAME):
@ -43,10 +48,10 @@ def integrate_package(folder_name, data):
repo_url = data['git'] repo_url = data['git']
keep_list = ["lib", "test", "LICENSE", "pubspec.yaml", "android", "ios"] keep_list = ["lib", "test", "LICENSE", "pubspec.yaml", "android", "ios"]
if "keep" in data: if "keep" in data:
keep_list += [item.rstrip('/') for item in data['keep']] keep_list += [item.rstrip('/') for item in data['keep']]
print(f"Processing {folder_name}...") print(f"Processing {folder_name}...")
if os.path.exists(folder_name): if os.path.exists(folder_name):
@ -62,7 +67,6 @@ def integrate_package(folder_name, data):
subprocess.run(["git", "checkout", config_lock[folder_name]], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=folder_name) subprocess.run(["git", "checkout", config_lock[folder_name]], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=folder_name)
else: else:
config_lock[folder_name] = last_commit_hash config_lock[folder_name] = last_commit_hash
for item in os.listdir(folder_name): for item in os.listdir(folder_name):
if item not in keep_list: if item not in keep_list:
@ -77,21 +81,25 @@ pubspec = {
"dependency_overrides": {}, "dependency_overrides": {},
} }
for folder_name, data in config.items(): # Determine which repositories to integrate
repos_to_update = [args.repo_name] if args.repo_name else config.keys()
pubspec["dependency_overrides"][folder_name] = {}
pubspec["dependency_overrides"][folder_name]["path"] = f"./dependencies/{folder_name}"
integrate_package(folder_name, data)
if "dependencies" in data:
for folder_name, data in data["dependencies"].items():
integrate_package(folder_name, data)
pubspec["dependency_overrides"][folder_name] = {}
pubspec["dependency_overrides"][folder_name]["path"] = f"./dependencies/{folder_name}"
for folder_name in repos_to_update:
if folder_name in config:
data = config[folder_name]
pubspec["dependency_overrides"][folder_name] = {}
pubspec["dependency_overrides"][folder_name]["path"] = f"./dependencies/{folder_name}"
integrate_package(folder_name, data)
if "dependencies" in data:
for dep_name, dep_data in data["dependencies"].items():
integrate_package(dep_name, dep_data)
pubspec["dependency_overrides"][dep_name] = {}
pubspec["dependency_overrides"][dep_name]["path"] = f"./dependencies/{dep_name}"
with open(LOCK_FILE_NAME, "w") as f: with open(LOCK_FILE_NAME, "w") as f:
yaml.safe_dump(config_lock, f, sort_keys=True) yaml.safe_dump(config_lock, f, sort_keys=True)
with open("pubspec.yaml", "w") as f: with open("pubspec.yaml", "w") as f:
yaml.safe_dump(pubspec, f, sort_keys=True) yaml.safe_dump(pubspec, f, sort_keys=True)