add exif plugin
This commit is contained in:
parent
e0c6a9617a
commit
72d9bd6320
59 changed files with 17635 additions and 57 deletions
|
|
@ -1,6 +1,7 @@
|
|||
adaptive_number: ea9178fdd4d82ac45cf0ec966ac870dae661124f
|
||||
dots_indicator: 508f5883ac79bdbc10254092de3f28f571d261cd
|
||||
ed25519_edwards: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf
|
||||
exif: bf170d5639f0b6fcb0947060cf8bd7b623df9069
|
||||
flutter_markdown_plus: dc1185c933fbf9dba559ef6c91586ff1503be3ee
|
||||
flutter_sharing_intent: aa1672f547d6579585fa27df0b28ffa2a2544aaa
|
||||
hand_signature: 1beedb164d093643365b0832277c377353c7464f
|
||||
|
|
@ -18,4 +19,5 @@ qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
|
|||
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
|
||||
restart_app: 66897cb67e235bab85421647bfae036acb4438cb
|
||||
screen_protector: 019c04d622d7b610d2903d3a347edc3ba76a6ed0
|
||||
sprintf: f1e74f2f4c339d983f9d011b4ba1df4ec8b8857c
|
||||
x25519: ecb1d357714537bba6e276ef45f093846d4beaee
|
||||
|
|
|
|||
|
|
@ -59,4 +59,11 @@ screen_protector:
|
|||
|
||||
|
||||
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
21
exif/LICENSE
Normal 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
5
exif/lib/exif.dart
Normal 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;
|
||||
301
exif/lib/src/exif_decode_makernote.dart
Normal file
301
exif/lib/src/exif_decode_makernote.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
116
exif/lib/src/exif_thumbnail.dart
Normal file
116
exif/lib/src/exif_thumbnail.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
149
exif/lib/src/exif_types.dart
Normal file
149
exif/lib/src/exif_types.dart
Normal 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]);
|
||||
}
|
||||
173
exif/lib/src/exifheader.dart
Normal file
173
exif/lib/src/exifheader.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
65
exif/lib/src/field_types.dart
Normal file
65
exif/lib/src/field_types.dart
Normal 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,
|
||||
];
|
||||
61
exif/lib/src/file_interface.dart
Normal file
61
exif/lib/src/file_interface.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
10
exif/lib/src/file_interface_generic.dart
Normal file
10
exif/lib/src/file_interface_generic.dart
Normal 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}");
|
||||
}
|
||||
20
exif/lib/src/file_interface_html.dart
Normal file
20
exif/lib/src/file_interface_html.dart
Normal 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}");
|
||||
}
|
||||
42
exif/lib/src/file_interface_io.dart
Normal file
42
exif/lib/src/file_interface_io.dart
Normal 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
276
exif/lib/src/heic.dart
Normal 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];
|
||||
}
|
||||
}
|
||||
57
exif/lib/src/linereader.dart
Normal file
57
exif/lib/src/linereader.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
exif/lib/src/makernote_apple.dart
Normal file
18
exif/lib/src/makernote_apple.dart
Normal 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',
|
||||
}),
|
||||
};
|
||||
}
|
||||
676
exif/lib/src/makernote_canon.dart
Normal file
676
exif/lib/src/makernote_canon.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
63
exif/lib/src/makernote_casio.dart
Normal file
63
exif/lib/src/makernote_casio.dart
Normal 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'
|
||||
}),
|
||||
};
|
||||
}
|
||||
101
exif/lib/src/makernote_fujifilm.dart
Normal file
101
exif/lib/src/makernote_fujifilm.dart
Normal 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'}),
|
||||
};
|
||||
}
|
||||
286
exif/lib/src/makernote_nikon.dart
Normal file
286
exif/lib/src/makernote_nikon.dart
Normal 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',
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
287
exif/lib/src/makernote_olympus.dart
Normal file
287
exif/lib/src/makernote_olympus.dart
Normal 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', ],
|
||||
};
|
||||
|
||||
*/
|
||||
38
exif/lib/src/print_exif.dart
Normal file
38
exif/lib/src/print_exif.dart
Normal 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
459
exif/lib/src/read_exif.dart
Normal 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
251
exif/lib/src/reader.dart
Normal 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
413
exif/lib/src/tags.dart
Normal 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'),
|
||||
};
|
||||
}
|
||||
25
exif/lib/src/tags_info.dart
Normal file
25
exif/lib/src/tags_info.dart
Normal 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
109
exif/lib/src/util.dart
Normal 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;
|
||||
}
|
||||
84
exif/lib/src/values_to_printable.dart
Normal file
84
exif/lib/src/values_to_printable.dart
Normal 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
25
exif/pubspec.yaml
Normal 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:
|
||||
BIN
exif/test/data/avif-test.avif
Normal file
BIN
exif/test/data/avif-test.avif
Normal file
Binary file not shown.
59
exif/test/data/avif-test.avif.dump
Normal file
59
exif/test/data/avif-test.avif.dump
Normal 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
|
||||
BIN
exif/test/data/heic-test.heic
Normal file
BIN
exif/test/data/heic-test.heic
Normal file
Binary file not shown.
80
exif/test/data/heic-test.heic.dump
Normal file
80
exif/test/data/heic-test.heic.dump
Normal 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
BIN
exif/test/data/png-test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
13
exif/test/data/png-test.png.dump
Normal file
13
exif/test/data/png-test.png.dump
Normal 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
0
exif/test/data/test-data
Normal file
BIN
exif/test/data/webp-test.webp
Normal file
BIN
exif/test/data/webp-test.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
59
exif/test/data/webp-test.webp.dump
Normal file
59
exif/test/data/webp-test.webp.dump
Normal 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
|
||||
52
exif/test/read_file_test.dart
Normal file
52
exif/test/read_file_test.dart
Normal 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");
|
||||
}
|
||||
94
exif/test/read_samples.dart
Normal file
94
exif/test/read_samples.dart
Normal 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;
|
||||
}
|
||||
23
exif/test/regression_test.dart
Normal file
23
exif/test/regression_test.dart
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
25
exif/test/sample_file.dart
Normal file
25
exif/test/sample_file.dart
Normal 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);
|
||||
}
|
||||
19
exif/test/sample_file.g.dart
Normal file
19
exif/test/sample_file.g.dart
Normal 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,
|
||||
};
|
||||
14
exif/test/samples_test.dart
Normal file
14
exif/test/samples_test.dart
Normal 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
11
exif/test/util_test.dart
Normal 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"));
|
||||
});
|
||||
}
|
||||
13
exif/test/web_hybrid_main.dart
Normal file
13
exif/test/web_hybrid_main.dart
Normal 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
21
exif/test/web_test.dart
Normal 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"));
|
||||
}
|
||||
44
pubspec.yaml
44
pubspec.yaml
|
|
@ -1,41 +1,5 @@
|
|||
dependency_overrides:
|
||||
adaptive_number:
|
||||
path: ./dependencies/adaptive_number
|
||||
dots_indicator:
|
||||
path: ./dependencies/dots_indicator
|
||||
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
|
||||
exif:
|
||||
path: ./dependencies/exif
|
||||
sprintf:
|
||||
path: ./dependencies/sprintf
|
||||
|
|
|
|||
22
sprintf/LICENSE
Normal file
22
sprintf/LICENSE
Normal 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
13
sprintf/lib/sprintf.dart
Normal 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();
|
||||
25
sprintf/lib/src/formatters/Formatter.dart
Normal file
25
sprintf/lib/src/formatters/Formatter.dart
Normal 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();
|
||||
}
|
||||
302
sprintf/lib/src/formatters/float_formatter.dart
Normal file
302
sprintf/lib/src/formatters/float_formatter.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
sprintf/lib/src/formatters/int_formatter.dart
Normal file
92
sprintf/lib/src/formatters/int_formatter.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
32
sprintf/lib/src/formatters/string_formatter.dart
Normal file
32
sprintf/lib/src/formatters/string_formatter.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
120
sprintf/lib/src/sprintf_impl.dart
Normal file
120
sprintf/lib/src/sprintf_impl.dart
Normal 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
12
sprintf/pubspec.yaml
Normal 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
|
||||
225
sprintf/test/sprintf_test.dart
Normal file
225
sprintf/test/sprintf_test.dart
Normal 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
12140
sprintf/test/testing_data.dart
Normal file
File diff suppressed because it is too large
Load diff
40
update.py
40
update.py
|
|
@ -1,13 +1,18 @@
|
|||
import yaml # pip install PyYAML
|
||||
import yaml # pip install PyYAML
|
||||
import os
|
||||
import shutil
|
||||
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:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
config_lock = {}
|
||||
|
||||
LOCK_FILE_NAME = "config.lock.yaml"
|
||||
|
||||
if os.path.exists(LOCK_FILE_NAME):
|
||||
|
|
@ -43,10 +48,10 @@ def integrate_package(folder_name, data):
|
|||
|
||||
repo_url = data['git']
|
||||
keep_list = ["lib", "test", "LICENSE", "pubspec.yaml", "android", "ios"]
|
||||
|
||||
if "keep" in data:
|
||||
keep_list += [item.rstrip('/') for item in data['keep']]
|
||||
|
||||
|
||||
print(f"Processing {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)
|
||||
else:
|
||||
config_lock[folder_name] = last_commit_hash
|
||||
|
||||
|
||||
for item in os.listdir(folder_name):
|
||||
if item not in keep_list:
|
||||
|
|
@ -77,21 +81,25 @@ pubspec = {
|
|||
"dependency_overrides": {},
|
||||
}
|
||||
|
||||
for folder_name, data in config.items():
|
||||
|
||||
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}"
|
||||
# Determine which repositories to integrate
|
||||
repos_to_update = [args.repo_name] if args.repo_name else config.keys()
|
||||
|
||||
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:
|
||||
yaml.safe_dump(config_lock, f, sort_keys=True)
|
||||
|
||||
with open("pubspec.yaml", "w") as f:
|
||||
yaml.safe_dump(pubspec, f, sort_keys=True)
|
||||
yaml.safe_dump(pubspec, f, sort_keys=True)
|
||||
|
|
|
|||
Loading…
Reference in a new issue