├── test ├── data │ ├── test-data │ ├── png-test.png │ ├── avif-test.avif │ ├── heic-test.heic │ ├── webp-test.webp │ ├── png-test.png.dump │ ├── avif-test.avif.dump │ ├── webp-test.webp.dump │ └── heic-test.heic.dump ├── web_hybrid_main.dart ├── samples_test.dart ├── util_test.dart ├── regression_test.dart ├── web_test.dart ├── sample_file.dart ├── sample_file.g.dart ├── read_file_test.dart └── read_samples.dart ├── .gitattributes ├── lib ├── exif.dart └── src │ ├── file_interface_generic.dart │ ├── tags_info.dart │ ├── makernote_apple.dart │ ├── file_interface_html.dart │ ├── print_exif.dart │ ├── file_interface_io.dart │ ├── linereader.dart │ ├── file_interface.dart │ ├── makernote_casio.dart │ ├── field_types.dart │ ├── values_to_printable.dart │ ├── util.dart │ ├── exif_types.dart │ ├── makernote_fujifilm.dart │ ├── exif_thumbnail.dart │ ├── exifheader.dart │ ├── reader.dart │ ├── heic.dart │ ├── makernote_olympus.dart │ ├── makernote_nikon.dart │ ├── exif_decode_makernote.dart │ ├── tags.dart │ ├── read_exif.dart │ └── makernote_canon.dart ├── analysis_options.yaml ├── pubspec.yaml ├── example ├── date_time.dart ├── example.dart └── gps_coords.dart ├── .gitignore ├── LICENSE ├── README.md ├── CHANGELOG.md ├── .github └── workflows │ └── dart.yml └── bin └── print_exif.dart /test/data/test-data: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /test/data/png-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigflood/dartexif/HEAD/test/data/png-test.png -------------------------------------------------------------------------------- /test/data/avif-test.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigflood/dartexif/HEAD/test/data/avif-test.avif -------------------------------------------------------------------------------- /test/data/heic-test.heic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigflood/dartexif/HEAD/test/data/heic-test.heic -------------------------------------------------------------------------------- /test/data/webp-test.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigflood/dartexif/HEAD/test/data/webp-test.webp -------------------------------------------------------------------------------- /lib/exif.dart: -------------------------------------------------------------------------------- 1 | library exif; 2 | 3 | export 'src/exif_types.dart'; 4 | export 'src/print_exif.dart' show printExifOfBytes; 5 | export 'src/read_exif.dart' show readExifFromBytes, readExifFromFile; 6 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | linter: 4 | rules: 5 | avoid_print: false 6 | avoid_catching_errors: false 7 | avoid_classes_with_only_static_members: false 8 | require_trailing_commas: false 9 | -------------------------------------------------------------------------------- /lib/src/file_interface_generic.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:exif/src/file_interface.dart'; 4 | 5 | Future createFileReaderFromFile(dynamic file) async { 6 | if (file is List) { 7 | return FileReader.fromBytes(file); 8 | } 9 | throw UnsupportedError("Can't read file of type: ${file.runtimeType}"); 10 | } 11 | -------------------------------------------------------------------------------- /test/web_hybrid_main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import "package:stream_channel/stream_channel.dart"; 4 | 5 | import 'read_samples.dart'; 6 | 7 | Future hybridMain(StreamChannel channel) async { 8 | await for (final file in readSamples()) { 9 | channel.sink.add(const JsonEncoder().convert(file)); 10 | } 11 | 12 | channel.sink.close(); 13 | } 14 | -------------------------------------------------------------------------------- /test/samples_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn("vm") 2 | import 'package:exif/exif.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import 'read_samples.dart'; 6 | 7 | Future main() async { 8 | await for (final file in readSamples()) { 9 | test(file.name, () async { 10 | final exifDump = await printExifOfBytes(file.getContent()); 11 | expect(exifDump, equals(file.dump)); 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/util_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/util.dart'; 2 | import "package:test/test.dart"; 3 | 4 | void main() { 5 | test("make_string_uc", () { 6 | expect(makeStringUc([]), equals("")); 7 | expect(makeStringUc([1, 2, 3, 4, 5, 6, 7]), equals("")); 8 | expect(makeStringUc([1, 2, 3, 4, 5, 6, 7, 8, 97, 98, 99]), equals("abc")); 9 | expect(makeString([0, 2, 0, 0]), equals("0200")); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /test/data/png-test.png.dump: -------------------------------------------------------------------------------- 1 | EXIF ComponentsConfiguration (Undefined): YCbCr 2 | EXIF ExifVersion (Undefined): 0232 3 | EXIF FlashPixVersion (Undefined): 0100 4 | EXIF UserComment (Undefined): exif test 5 | GPS GPSLatitude (Ratio): [44, 29, 76078/1397] 6 | GPS GPSLatitudeRef (ASCII): N 7 | GPS GPSLongitude (Ratio): [11, 19, 48947/1171] 8 | GPS GPSLongitudeRef (ASCII): E 9 | Image ExifOffset (Long): 88 10 | Image GPSInfo (Long): 160 11 | Image ImageDescription (ASCII): Flutter Dash 12 | Image ResolutionUnit (Short): Pixels/Inch 13 | Image YCbCrPositioning (Short): Centered 14 | -------------------------------------------------------------------------------- /test/regression_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/exif.dart'; 2 | import "package:test/test.dart"; 3 | 4 | void main() { 5 | test("range error", () async { 6 | final data = [ 7 | '', 8 | '\xFF', 9 | '\xFF\xD8', 10 | '\xFF\xD8abc', 11 | 'II', 12 | 'II*\x00', 13 | 'II*\x00ftypheic', 14 | 'MM', 15 | 'MM\x00*', 16 | ]; 17 | 18 | for (final x in data) { 19 | final exifDump = await printExifOfBytes(x.codeUnits); 20 | expect(exifDump, equals("No EXIF information found"), reason: x); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/tags_info.dart: -------------------------------------------------------------------------------- 1 | typedef MakerTagFunc = String Function(List list); 2 | 3 | class MakerTag { 4 | String name; 5 | Map? map; 6 | MakerTagFunc? func; 7 | MakerTagsWithName? tags; 8 | 9 | MakerTag.make(this.name); 10 | 11 | MakerTag.makeWithMap(this.name, this.map); 12 | 13 | MakerTag.makeWithFunc(this.name, this.func); 14 | 15 | MakerTag.makeWithTags(this.name, this.tags); 16 | } 17 | 18 | class MakerTagsWithName { 19 | String name; 20 | Map tags; 21 | 22 | MakerTagsWithName({this.name = "", this.tags = const {}}); 23 | } 24 | 25 | class TagsBase {} 26 | -------------------------------------------------------------------------------- /lib/src/makernote_apple.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/tags_info.dart' show MakerTag, TagsBase; 2 | 3 | // Makernote (proprietary) tag definitions for Apple iOS 4 | // Based on version 1.01 of ExifTool -> Image/ExifTool/Apple.pm 5 | // http://owl.phy.queensu.ca/~phil/exiftool/ 6 | 7 | class MakerNoteApple extends TagsBase { 8 | //static MakerTag _make(String name) => MakerTag.make(name); 9 | static MakerTag _withMap(String name, Map map) => 10 | MakerTag.makeWithMap(name, map); 11 | 12 | static final tags = { 13 | 0x000a: _withMap('HDRImageType', { 14 | 3: 'HDR Image', 15 | 4: 'Original Image', 16 | }), 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: exif 2 | version: 3.3.0 3 | description: >- 4 | Decode Exif metadata from digital image files. 5 | Supported formats: TIFF, JPEG, HEIC, PNG, WebP 6 | homepage: https://www.github.com/bigflood/dartexif 7 | environment: 8 | sdk: '>=2.12.0 <4.0.0' 9 | dependencies: 10 | args: ^2.0.0 11 | collection: ^1.15.0 12 | convert: ^3.0.0 13 | json_annotation: ^4.3.0 14 | sprintf: ^7.0.0 15 | dev_dependencies: 16 | archive: ^3.1.2 17 | build_runner: ^2.1.4 18 | http: '>=1.0.0 <2.0.0' 19 | json_serializable: ^6.0.0 20 | lints: ^2.0.1 21 | path: ^1.8.0 22 | stream_channel: ^2.1.0 23 | test: ^1.16.8 24 | executables: 25 | print_exif: 26 | -------------------------------------------------------------------------------- /test/web_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn("browser") 2 | import "dart:convert"; 3 | 4 | import 'package:exif/exif.dart'; 5 | import "package:test/test.dart"; 6 | 7 | import "sample_file.dart"; 8 | 9 | void main() { 10 | test("run hybrid main", () async { 11 | final channel = spawnHybridUri("web_hybrid_main.dart"); 12 | 13 | await for (final msg in channel.stream) { 14 | final file = SampleFile.fromJson( 15 | json.decode(msg as String) as Map); 16 | print(file.name); 17 | expect(await printExifOfBytes(file.getContent()), equals(file.dump), 18 | reason: "file=${file.name}"); 19 | } 20 | }, timeout: Timeout.parse("60s")); 21 | } 22 | -------------------------------------------------------------------------------- /example/date_time.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:exif/exif.dart'; 4 | 5 | Future main(List arguments) async { 6 | for (final filename in arguments) { 7 | print("read $filename .."); 8 | 9 | final fileBytes = File(filename).readAsBytesSync(); 10 | final data = await readExifFromBytes(fileBytes); 11 | 12 | if (data.isEmpty) { 13 | print("No EXIF information found"); 14 | return; 15 | } 16 | 17 | final datetime = data['EXIF DateTimeOriginal']?.toString(); 18 | if (datetime == null) { 19 | print("datetime information not found"); 20 | return; 21 | } 22 | 23 | print("datetime = $datetime"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/file_interface_html.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html' as dart_html; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:exif/src/file_interface.dart'; 6 | 7 | Future createFileReaderFromFile(dynamic file) async { 8 | if (file is dart_html.File) { 9 | final fileReader = dart_html.FileReader(); 10 | fileReader.readAsArrayBuffer(file); 11 | await fileReader.onLoad.first; 12 | final data = fileReader.result; 13 | if (data is Uint8List) { 14 | return FileReader.fromBytes(data); 15 | } 16 | } else if (file is List) { 17 | return FileReader.fromBytes(file); 18 | } 19 | throw UnsupportedError("Can't read file of type: ${file.runtimeType}"); 20 | } 21 | -------------------------------------------------------------------------------- /test/sample_file.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:json_annotation/json_annotation.dart'; 5 | 6 | part 'sample_file.g.dart'; 7 | 8 | @JsonSerializable() 9 | class SampleFile { 10 | String name; 11 | String encodedContent = ""; 12 | String? dump; 13 | 14 | List getContent() => base64.decode(encodedContent); 15 | 16 | SampleFile({this.name = "", this.dump = "", Uint8List? content}) { 17 | if (content != null) { 18 | encodedContent = base64.encode(content); 19 | } 20 | } 21 | 22 | factory SampleFile.fromJson(Map json) => 23 | _$SampleFileFromJson(json); 24 | Map toJson() => _$SampleFileToJson(this); 25 | } 26 | -------------------------------------------------------------------------------- /test/sample_file.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'sample_file.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SampleFile _$SampleFileFromJson(Map json) => SampleFile( 10 | name: json['name'] as String? ?? "", 11 | dump: json['dump'] as String? ?? "", 12 | )..encodedContent = json['encodedContent'] as String; 13 | 14 | Map _$SampleFileToJson(SampleFile instance) => 15 | { 16 | 'name': instance.name, 17 | 'encodedContent': instance.encodedContent, 18 | 'dump': instance.dump, 19 | }; 20 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:exif/exif.dart'; 4 | 5 | Future main(List arguments) async { 6 | for (final filename in arguments) { 7 | print("read $filename .."); 8 | 9 | final fileBytes = File(filename).readAsBytesSync(); 10 | final data = await readExifFromBytes(fileBytes); 11 | 12 | if (data.isEmpty) { 13 | print("No EXIF information found"); 14 | return; 15 | } 16 | 17 | if (data.containsKey('JPEGThumbnail')) { 18 | print('File has JPEG thumbnail'); 19 | data.remove('JPEGThumbnail'); 20 | } 21 | if (data.containsKey('TIFFThumbnail')) { 22 | print('File has TIFF thumbnail'); 23 | data.remove('TIFFThumbnail'); 24 | } 25 | 26 | for (final entry in data.entries) { 27 | print("${entry.key}: ${entry.value}"); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/tools/private-files.htmlmaster.tar.gz 2 | 3 | # Files and directories created by pub 4 | .buildlog 5 | .packages 6 | .project 7 | .pub/ 8 | build/ 9 | **/packages/ 10 | 11 | # Files created by dart2js 12 | # (Most Dart developers will use pub build to compile Dart, use/modify these 13 | # rules if you intend to use dart2js directly 14 | # Convention is to use extension '.dart.js' for Dart compiled to Javascript to 15 | # differentiate from explicit Javascript files) 16 | *.dart.js 17 | *.part.js 18 | *.js.deps 19 | *.js.map 20 | *.info.json 21 | 22 | # Directory created by dartdoc 23 | doc/api/ 24 | 25 | # Don't commit pubspec lock file 26 | # (Library packages only! Remove pattern if developing an application package) 27 | pubspec.lock 28 | 29 | .dart_tool 30 | /test/data/*-dump 31 | /test/data/*.tar.gz 32 | .idea/ 33 | -------------------------------------------------------------------------------- /lib/src/print_exif.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/file_interface.dart'; 2 | import 'package:exif/src/read_exif.dart'; 3 | 4 | Future printExifOfBytes(List bytes, 5 | {String? stopTag, 6 | bool details = true, 7 | bool strict = false, 8 | bool debug = false}) async { 9 | final data = 10 | readExifFromFileReader(FileReader.fromBytes(bytes), stopTag: stopTag); 11 | 12 | if (data.tags.isEmpty) { 13 | return "No EXIF information found"; 14 | } 15 | 16 | final prints = []; 17 | 18 | // prints.addAll(data.warnings); 19 | 20 | if (data.tags.containsKey('JPEGThumbnail')) { 21 | prints.add('File has JPEG thumbnail'); 22 | data.tags.remove('JPEGThumbnail'); 23 | } 24 | if (data.tags.containsKey('TIFFThumbnail')) { 25 | prints.add('File has TIFF thumbnail'); 26 | data.tags.remove('TIFFThumbnail'); 27 | } 28 | 29 | final tagKeys = data.tags.keys.toList(); 30 | tagKeys.sort(); 31 | 32 | for (final key in tagKeys) { 33 | final tag = data.tags[key]; 34 | prints.add("$key (${tag!.tagType}): $tag"); 35 | } 36 | 37 | return prints.join("\n"); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/file_interface_io.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:exif/src/file_interface.dart'; 5 | 6 | class _FileReader implements FileReader { 7 | final RandomAccessFile file; 8 | 9 | _FileReader(this.file); 10 | 11 | @override 12 | int positionSync() { 13 | return file.positionSync(); 14 | } 15 | 16 | @override 17 | int readByteSync() { 18 | return file.readByteSync(); 19 | } 20 | 21 | @override 22 | List readSync(int bytes) { 23 | return file.readSync(bytes).toList(growable: false); 24 | } 25 | 26 | @override 27 | void setPositionSync(int position) { 28 | file.setPositionSync(position); 29 | } 30 | } 31 | 32 | Future createFileReaderFromFile(dynamic file) async { 33 | if (file is RandomAccessFile) { 34 | return _FileReader(file); 35 | } else if (file is File) { 36 | final data = await file.readAsBytes(); 37 | return FileReader.fromBytes(data); 38 | } else if (file is List) { 39 | return FileReader.fromBytes(file); 40 | } 41 | throw UnsupportedError("Can't read file of type: ${file.runtimeType}"); 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 bigflood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exif 2 | 3 | [![Pub Package](https://img.shields.io/pub/v/exif.svg)](https://pub.dev/packages/exif) 4 | [![Dart CI](https://github.com/bigflood/dartexif/actions/workflows/dart.yml/badge.svg)](https://github.com/bigflood/dartexif/actions/workflows/dart.yml) 5 | 6 | Dart package to decode Exif data from TIFF, JPEG, HEIC, PNG and WebP files. 7 | 8 | Dart port of ianaré sévi's EXIF library: . 9 | 10 | ## Usage 11 | 12 | * Simple example: 13 | ```dart 14 | printExifOf(String path) async { 15 | 16 | final fileBytes = File(path).readAsBytesSync(); 17 | final data = await readExifFromBytes(fileBytes); 18 | 19 | if (data.isEmpty) { 20 | print("No EXIF information found"); 21 | return; 22 | } 23 | 24 | if (data.containsKey('JPEGThumbnail')) { 25 | print('File has JPEG thumbnail'); 26 | data.remove('JPEGThumbnail'); 27 | } 28 | if (data.containsKey('TIFFThumbnail')) { 29 | print('File has TIFF thumbnail'); 30 | data.remove('TIFFThumbnail'); 31 | } 32 | 33 | for (final entry in data.entries) { 34 | print("${entry.key}: ${entry.value}"); 35 | } 36 | 37 | } 38 | ``` 39 | 40 | * example app: https://github.com/bigflood/exifviewer 41 | -------------------------------------------------------------------------------- /lib/src/linereader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:exif/src/file_interface.dart'; 4 | 5 | class LineReader { 6 | FileReader file; 7 | final List _buffer = []; 8 | bool _endOfFile = false; 9 | 10 | LineReader(this.file); 11 | 12 | String popString(int n) { 13 | String s; 14 | 15 | if (n < _buffer.length) { 16 | s = utf8.decode(_buffer.sublist(0, n)); 17 | _buffer.removeRange(0, n); 18 | } else { 19 | s = utf8.decode(_buffer); 20 | _buffer.clear(); 21 | } 22 | 23 | return s; 24 | } 25 | 26 | String readLine() { 27 | int endOfLine = _buffer.indexOf(10); 28 | if (endOfLine >= 0) { 29 | return popString(endOfLine + 1); 30 | } 31 | 32 | if (_endOfFile) { 33 | return popString(_buffer.length); 34 | } 35 | 36 | while (true) { 37 | final r = file.readSync(1024 * 10); 38 | 39 | if (r.isEmpty) { 40 | _endOfFile = true; 41 | endOfLine = -1; 42 | } else { 43 | endOfLine = r.indexOf(10); 44 | _buffer.addAll(r); 45 | if (endOfLine >= 0) { 46 | endOfLine += _buffer.length; 47 | } 48 | } 49 | 50 | if (endOfLine >= 0) { 51 | return popString(endOfLine + 1); 52 | } else if (_endOfFile) { 53 | return popString(_buffer.length); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/gps_coords.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:exif/exif.dart'; 4 | 5 | Future main(List arguments) async { 6 | for (final filename in arguments) { 7 | print("read $filename .."); 8 | 9 | final fileBytes = File(filename).readAsBytesSync(); 10 | final data = await readExifFromBytes(fileBytes); 11 | 12 | if (data.isEmpty) { 13 | print("No EXIF information found"); 14 | return; 15 | } 16 | 17 | final latRef = data['GPS GPSLatitudeRef']?.toString(); 18 | var latVal = gpsValuesToFloat(data['GPS GPSLatitude']?.values); 19 | final lngRef = data['GPS GPSLongitudeRef']?.toString(); 20 | var lngVal = gpsValuesToFloat(data['GPS GPSLongitude']?.values); 21 | 22 | if (latRef == null || latVal == null || lngRef == null || lngVal == null) { 23 | print("GPS information not found"); 24 | return; 25 | } 26 | 27 | if (latRef == 'S') { 28 | latVal *= -1; 29 | } 30 | 31 | if (lngRef == 'W') { 32 | lngVal *= -1; 33 | } 34 | 35 | print("lat = $latVal"); 36 | print("lng = $lngVal"); 37 | } 38 | } 39 | 40 | double? gpsValuesToFloat(IfdValues? values) { 41 | if (values == null || values is! IfdRatios) { 42 | return null; 43 | } 44 | 45 | double sum = 0.0; 46 | double unit = 1.0; 47 | 48 | for (final v in values.ratios) { 49 | sum += v.toDouble() * unit; 50 | unit /= 60.0; 51 | } 52 | 53 | return sum; 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/file_interface.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:exif/src/file_interface_generic.dart' 4 | if (dart.library.html) "package:exif/src/file_interface_html.dart" 5 | if (dart.library.io) 'package:exif/src/file_interface_io.dart'; 6 | 7 | abstract class FileReader { 8 | static Future fromFile(dynamic file) async { 9 | return createFileReaderFromFile(file); 10 | } 11 | 12 | factory FileReader.fromBytes(List bytes) { 13 | return _BytesReader(bytes); 14 | } 15 | 16 | int readByteSync(); 17 | 18 | List readSync(int bytes); 19 | 20 | int positionSync(); 21 | 22 | void setPositionSync(int position); 23 | } 24 | 25 | class _BytesReader implements FileReader { 26 | List bytes; 27 | int readPos = 0; 28 | 29 | _BytesReader(this.bytes); 30 | 31 | @override 32 | int positionSync() { 33 | return readPos; 34 | } 35 | 36 | @override 37 | int readByteSync() { 38 | return bytes[readPos++]; 39 | } 40 | 41 | @override 42 | List readSync(int n) { 43 | final start = readPos; 44 | if (start >= bytes.length) { 45 | return []; 46 | } 47 | 48 | var end = readPos + n; 49 | if (end > bytes.length) { 50 | end = bytes.length; 51 | } 52 | final r = bytes.sublist(start, end); 53 | readPos += end - start; 54 | return r; 55 | } 56 | 57 | @override 58 | void setPositionSync(int position) { 59 | readPos = position; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.3.0 2 | 3 | - Add WebP Support 4 | 5 | ## 3.2.1 6 | 7 | - Add AVIF Support 8 | - Add PNG Support 9 | 10 | ## 3.1.4 11 | 12 | - Bump dependency `sprintf` to `7.0.0` 13 | - Fix noop_primitive_operations 14 | 15 | ## 3.1.2 16 | 17 | - Fix Bad state: No element while reading Exif 18 | 19 | ## 3.1.1 20 | 21 | - Fixed range error issue 22 | - Fixed some lint errors 23 | - Changed file parameter type of readExifFromFile function from dynamic to io.File 24 | 25 | ## 3.0.1 26 | 27 | - add time offset tag names 28 | - OffsetTime, OffsetTimeOriginal, OffsetTimeDigitized 29 | - upgrade dependencies 30 | 31 | ## 3.0.0 32 | 33 | - Breaking API Changes 34 | - Changed nullable type to non-nullable type if possible 35 | - Changed some parameters to camel-case 36 | - Added IfdValues and it's subtypes 37 | - IfdTag.values is now IfdValues type 38 | 39 | ## 2.2.0 40 | 41 | - Add HEIC support 42 | 43 | ## 2.1.0 44 | 45 | - fixed some minor issues 46 | 47 | ## 2.0.0 48 | 49 | - migrate to null-safety 50 | - change to MIT License 51 | 52 | ## 1.0.3 53 | 54 | - Make package portable between Dart Web (dart:html dependent) and Dart Native (dart:io dependent) 55 | - Upgraded Dart SDK to '>=2.0.0 <3.0.0' 56 | 57 | ## 1.0.2 58 | 59 | - Add RandomAccessFile-backed reader 60 | 61 | ## 1.0.1 62 | 63 | - bugfix: RangeError for some images 64 | 65 | ## 1.0.0 66 | 67 | - Removed dependency on io package 68 | - Removed readExifFromFile 69 | 70 | ## 0.1.5 71 | 72 | - Added tests 73 | 74 | ## 0.1.2 75 | 76 | - Initial version 77 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master, develop ] 7 | pull_request: 8 | branches: [ master, develop ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | analyze: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | sdk: [stable] 22 | steps: 23 | - uses: actions/checkout@v2.3.5 24 | - uses: dart-lang/setup-dart@v1.3 25 | with: 26 | sdk: ${{ matrix.sdk }} 27 | - id: install 28 | name: Install dependencies 29 | run: dart pub get 30 | - name: Verify formatting 31 | run: dart format --output=none --set-exit-if-changed . 32 | if: always() && steps.install.outcome == 'success' 33 | - name: Analyze project source 34 | run: dart analyze --fatal-infos 35 | if: always() && steps.install.outcome == 'success' 36 | 37 | test: 38 | needs: analyze 39 | runs-on: ${{ matrix.os }} 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | os: [ubuntu-latest] 44 | sdk: [stable, dev] 45 | steps: 46 | - uses: actions/checkout@v2.3.5 47 | - uses: dart-lang/setup-dart@v1.3 48 | with: 49 | sdk: ${{ matrix.sdk }} 50 | - id: install 51 | name: Install dependencies 52 | run: dart pub get 53 | - name: Run VM tests 54 | run: dart test --platform vm 55 | if: always() && steps.install.outcome == 'success' 56 | - name: Run Chrome tests 57 | run: dart test --platform chrome 58 | if: always() && steps.install.outcome == 'success' 59 | -------------------------------------------------------------------------------- /test/read_file_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn("vm") 2 | import "dart:io" as io; 3 | 4 | import 'package:exif/exif.dart'; 5 | import "package:test/test.dart"; 6 | 7 | void main() { 8 | test("read heic file test", () async { 9 | const filename = "test/data/heic-test.heic"; 10 | final file = io.File(filename); 11 | final output = tagsToString(await readExifFromFile(file)); 12 | final expected = await io.File("$filename.dump").readAsString(); 13 | expect(output, equals(expected.trim())); 14 | }); 15 | 16 | test("read png file test", () async { 17 | const filename = "test/data/png-test.png"; 18 | final file = io.File(filename); 19 | final output = tagsToString(await readExifFromFile(file)); 20 | final expected = await io.File("$filename.dump").readAsString(); 21 | expect(output, equals(expected.trim())); 22 | }); 23 | 24 | test("read avif file test", () async { 25 | const filename = "test/data/avif-test.avif"; 26 | final file = io.File(filename); 27 | final output = tagsToString(await readExifFromFile(file)); 28 | final expected = await io.File("$filename.dump").readAsString(); 29 | expect(output, equals(expected.trim())); 30 | }); 31 | 32 | test("read webp file test", () async { 33 | const filename = "test/data/webp-test.webp"; 34 | final file = io.File(filename); 35 | final output = tagsToString(await readExifFromFile(file)); 36 | final expected = await io.File("$filename.dump").readAsString(); 37 | expect(output, equals(expected.trim())); 38 | }); 39 | } 40 | 41 | String tagsToString(Map tags) { 42 | final tagKeys = tags.keys.toList(); 43 | tagKeys.sort(); 44 | final prints = []; 45 | 46 | for (final key in tagKeys) { 47 | final tag = tags[key]; 48 | prints.add("$key (${tag!.tagType}): $tag"); 49 | } 50 | 51 | return prints.join("\n"); 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/makernote_casio.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/tags_info.dart' show MakerTag, TagsBase; 2 | 3 | // Makernote (proprietary) tag definitions for casio. 4 | 5 | class MakerNoteCasio extends TagsBase { 6 | static MakerTag _make(String name) => MakerTag.make(name); 7 | 8 | static MakerTag _withMap(String name, Map map) => 9 | MakerTag.makeWithMap(name, map); 10 | 11 | static Map tags = { 12 | 0x0001: _withMap('RecordingMode', { 13 | 1: 'Single Shutter', 14 | 2: 'Panorama', 15 | 3: 'Night Scene', 16 | 4: 'Portrait', 17 | 5: 'Landscape', 18 | }), 19 | 0x0002: _withMap('Quality', {1: 'Economy', 2: 'Normal', 3: 'Fine'}), 20 | 0x0003: _withMap('FocusingMode', 21 | {2: 'Macro', 3: 'Auto Focus', 4: 'Manual Focus', 5: 'Infinity'}), 22 | 0x0004: _withMap('FlashMode', { 23 | 1: 'Auto', 24 | 2: 'On', 25 | 3: 'Off', 26 | 4: 'Red Eye Reduction', 27 | }), 28 | 0x0005: 29 | _withMap('FlashIntensity', {11: 'Weak', 13: 'Normal', 15: 'Strong'}), 30 | 0x0006: _make('Object Distance'), 31 | 0x0007: _withMap('WhiteBalance', { 32 | 1: 'Auto', 33 | 2: 'Tungsten', 34 | 3: 'Daylight', 35 | 4: 'Fluorescent', 36 | 5: 'Shade', 37 | 129: 'Manual' 38 | }), 39 | 0x000B: _withMap('Sharpness', { 40 | 0: 'Normal', 41 | 1: 'Soft', 42 | 2: 'Hard', 43 | }), 44 | 0x000C: _withMap('Contrast', { 45 | 0: 'Normal', 46 | 1: 'Low', 47 | 2: 'High', 48 | }), 49 | 0x000D: _withMap('Saturation', { 50 | 0: 'Normal', 51 | 1: 'Low', 52 | 2: 'High', 53 | }), 54 | 0x0014: _withMap('CCDSpeed', { 55 | 64: 'Normal', 56 | 80: 'Normal', 57 | 100: 'High', 58 | 125: '+1.0', 59 | 244: '+3.0', 60 | 250: '+2.0' 61 | }), 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /bin/print_exif.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:exif/exif.dart'; 5 | 6 | void usage(int exitStatus) { 7 | const msg = 'Usage: EXIF [OPTIONS] file1 [file2 ...]\n' 8 | 'Extract EXIF information from digital camera image files.\n' 9 | '\n' 10 | 'Options:\n' 11 | '-h --help Display usage information and exit.\n' 12 | '-q --quick Do not process MakerNotes.\n' 13 | '-t TAG --stop-tag TAG Stop processing when this tag is retrieved.\n' 14 | '-s --strict Run in strict mode (stop on errors).\n' 15 | '-d --debug Run in debug mode (display extra info).\n'; 16 | print(msg); 17 | exit(exitStatus); 18 | } 19 | 20 | Future main(List arguments) async { 21 | exitCode = 0; 22 | 23 | bool detailed = true; 24 | String? stopTag; 25 | bool debug = false; 26 | bool strict = false; 27 | 28 | final parser = ArgParser() 29 | ..addFlag('help', abbr: 'h', callback: (v) { 30 | if (v) usage(0); 31 | }) 32 | ..addFlag('quick', abbr: 'q', callback: (v) { 33 | detailed = !v; 34 | }) 35 | ..addOption('stop-tag', abbr: 't', callback: (v) { 36 | stopTag = v; 37 | }) 38 | ..addFlag('strict', abbr: 's', callback: (v) { 39 | strict = v; 40 | }) 41 | ..addFlag('debug', abbr: 'd', callback: (v) { 42 | debug = v; 43 | }); 44 | 45 | late List args; 46 | 47 | try { 48 | args = parser.parse(arguments).rest; 49 | } on ArgParserException { 50 | usage(2); 51 | } 52 | 53 | if (args.isEmpty) { 54 | usage(2); 55 | } 56 | 57 | for (final String filename in args) { 58 | print("Opening: $filename"); 59 | 60 | final fileBytes = File(filename).readAsBytesSync(); 61 | 62 | print(await printExifOfBytes(fileBytes, 63 | stopTag: stopTag, details: detailed, strict: strict, debug: debug)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/field_types.dart: -------------------------------------------------------------------------------- 1 | class FieldType { 2 | final int _value; 3 | final int length; 4 | final String abbr; 5 | final String name; 6 | final bool isValid; 7 | final bool isSigned; 8 | 9 | const FieldType(this._value, this.length, this.abbr, this.name, 10 | {this.isValid = true, this.isSigned = false}); 11 | 12 | factory FieldType.ofValue(int v) { 13 | if (v < 0 || v >= fieldTypes.length) { 14 | return FieldType(v, 0, 'X', 'Unknown', isValid: false); 15 | } 16 | return fieldTypes[v]; 17 | } 18 | 19 | @override 20 | bool operator ==(Object other) => 21 | other is FieldType && _value == other._value; 22 | 23 | @override 24 | int get hashCode => _value.hashCode; 25 | 26 | static const proprietary = 27 | FieldType(0, 0, 'X', 'Proprietary', isValid: false); // no such type 28 | static const byte = FieldType(1, 1, 'B', 'Byte'); 29 | static const ascii = FieldType(2, 1, 'A', 'ASCII'); 30 | static const short = FieldType(3, 2, 'S', 'Short'); 31 | static const long = FieldType(4, 4, 'L', 'Long'); 32 | static const ratio = FieldType(5, 8, 'R', 'Ratio'); 33 | static const signedByte = 34 | FieldType(6, 1, 'SB', 'Signed Byte', isSigned: true); 35 | static const undefined = FieldType(7, 1, 'U', 'Undefined'); 36 | static const signedShort = 37 | FieldType(8, 2, 'SS', 'Signed Short', isSigned: true); 38 | static const signedLong = 39 | FieldType(9, 4, 'SL', 'Signed Long', isSigned: true); 40 | static const signedRatio = 41 | FieldType(10, 8, 'SR', 'Signed Ratio', isSigned: true); 42 | static const f32 = 43 | FieldType(11, 4, 'F32', 'Single-Precision Floating Point (32-bit)'); 44 | static const f64 = 45 | FieldType(12, 8, 'F64', 'Double-Precision Floating Point (64-bit)'); 46 | static const ifd = FieldType(13, 4, 'L', 'IFD'); 47 | } 48 | 49 | // field type descriptions as (length, abbreviation, full name) tuples 50 | const fieldTypes = [ 51 | FieldType.proprietary, // no such type 52 | FieldType.byte, 53 | FieldType.ascii, 54 | FieldType.short, 55 | FieldType.long, 56 | FieldType.ratio, 57 | FieldType.signedByte, 58 | FieldType.undefined, 59 | FieldType.signedShort, 60 | FieldType.signedLong, 61 | FieldType.signedRatio, 62 | FieldType.f32, 63 | FieldType.f64, 64 | FieldType.ifd, 65 | ]; 66 | -------------------------------------------------------------------------------- /test/data/avif-test.avif.dump: -------------------------------------------------------------------------------- 1 | EXIF ApertureValue (Ratio): 4845/1918 2 | EXIF BrightnessValue (Signed Ratio): 7187/850 3 | EXIF ColorSpace (Short): Uncalibrated 4 | EXIF ComponentsConfiguration (Undefined): YCbCr 5 | EXIF CustomRendered (Short): 2 6 | EXIF DateTimeDigitized (ASCII): 2018:03:30 12:14:19 7 | EXIF DateTimeOriginal (ASCII): 2018:03:30 12:14:19 8 | EXIF ExifImageLength (Long): 180 9 | EXIF ExifImageWidth (Long): 240 10 | EXIF ExifVersion (Undefined): 0221 11 | EXIF ExposureBiasValue (Signed Ratio): 0 12 | EXIF ExposureMode (Short): Auto Exposure 13 | EXIF ExposureProgram (Short): Program Normal 14 | EXIF ExposureTime (Ratio): 1/209 15 | EXIF FNumber (Ratio): 12/5 16 | EXIF Flash (Short): Flash did not fire, compulsory flash mode 17 | EXIF FlashPixVersion (Undefined): 0100 18 | EXIF FocalLength (Ratio): 6 19 | EXIF FocalLengthIn35mmFilm (Short): 52 20 | EXIF ISOSpeedRatings (Short): 16 21 | EXIF LensMake (ASCII): Apple 22 | EXIF LensModel (ASCII): iPhone X back dual camera 6mm f/2.4 23 | EXIF LensSpecification (Ratio): [4, 6, 9/5, 12/5] 24 | EXIF MakerNote (Undefined): [] 25 | EXIF MeteringMode (Short): Pattern 26 | EXIF SceneCaptureType (Short): Standard 27 | EXIF SceneType (Undefined): Directly Photographed 28 | EXIF SensingMethod (Short): One-chip color area 29 | EXIF ShutterSpeedValue (Signed Ratio): 7789/1011 30 | EXIF SubSecTimeDigitized (ASCII): 365 31 | EXIF SubSecTimeOriginal (ASCII): 365 32 | EXIF SubjectArea (Short): [2007, 1503, 2209, 1327] 33 | EXIF WhiteBalance (Short): Auto 34 | GPS GPSAltitude (Ratio): 6568/1433 35 | GPS GPSAltitudeRef (Byte): 0 36 | GPS GPSDate (ASCII): 2018:03:30 37 | GPS GPSDestBearing (Ratio): 21278/339 38 | GPS GPSDestBearingRef (ASCII): M 39 | GPS GPSImgDirection (Ratio): 21278/339 40 | GPS GPSImgDirectionRef (ASCII): M 41 | GPS GPSLatitude (Ratio): [37, 45, 907/25] 42 | GPS GPSLatitudeRef (ASCII): N 43 | GPS GPSLongitude (Ratio): [122, 30, 861/25] 44 | GPS GPSLongitudeRef (ASCII): W 45 | GPS GPSSpeed (Ratio): 5709/10354 46 | GPS GPSSpeedRef (ASCII): K 47 | GPS GPSTimeStamp (Ratio): [19, 14, 1813/100] 48 | GPS Tag 0x001F (Ratio): 6619/1103 49 | Image DateTime (ASCII): 2018:03:30 12:14:19 50 | Image ExifOffset (Long): 204 51 | Image GPSInfo (Long): 784 52 | Image Make (ASCII): Apple 53 | Image Model (ASCII): iPhone X 54 | Image Orientation (Short): Horizontal (normal) 55 | Image ResolutionUnit (Short): Pixels/Inch 56 | Image Software (ASCII): 12.0 57 | Image XResolution (Ratio): 127/5 58 | Image YCbCrPositioning (Short): Centered 59 | Image YResolution (Ratio): 127/5 60 | -------------------------------------------------------------------------------- /test/data/webp-test.webp.dump: -------------------------------------------------------------------------------- 1 | EXIF ApertureValue (Ratio): 4845/1918 2 | EXIF BrightnessValue (Signed Ratio): 7187/850 3 | EXIF ColorSpace (Short): Uncalibrated 4 | EXIF ComponentsConfiguration (Undefined): YCbCr 5 | EXIF CustomRendered (Short): 2 6 | EXIF DateTimeDigitized (ASCII): 2018:03:30 12:14:19 7 | EXIF DateTimeOriginal (ASCII): 2018:03:30 12:14:19 8 | EXIF ExifImageLength (Long): 180 9 | EXIF ExifImageWidth (Long): 240 10 | EXIF ExifVersion (Undefined): 0221 11 | EXIF ExposureBiasValue (Signed Ratio): 0 12 | EXIF ExposureMode (Short): Auto Exposure 13 | EXIF ExposureProgram (Short): Program Normal 14 | EXIF ExposureTime (Ratio): 1/209 15 | EXIF FNumber (Ratio): 12/5 16 | EXIF Flash (Short): Flash did not fire, compulsory flash mode 17 | EXIF FlashPixVersion (Undefined): 0100 18 | EXIF FocalLength (Ratio): 6 19 | EXIF FocalLengthIn35mmFilm (Short): 52 20 | EXIF ISOSpeedRatings (Short): 16 21 | EXIF LensMake (ASCII): Apple 22 | EXIF LensModel (ASCII): iPhone X back dual camera 6mm f/2.4 23 | EXIF LensSpecification (Ratio): [4, 6, 9/5, 12/5] 24 | EXIF MakerNote (Undefined): [] 25 | EXIF MeteringMode (Short): Pattern 26 | EXIF SceneCaptureType (Short): Standard 27 | EXIF SceneType (Undefined): Directly Photographed 28 | EXIF SensingMethod (Short): One-chip color area 29 | EXIF ShutterSpeedValue (Signed Ratio): 7789/1011 30 | EXIF SubSecTimeDigitized (ASCII): 365 31 | EXIF SubSecTimeOriginal (ASCII): 365 32 | EXIF SubjectArea (Short): [2007, 1503, 2209, 1327] 33 | EXIF WhiteBalance (Short): Auto 34 | GPS GPSAltitude (Ratio): 6568/1433 35 | GPS GPSAltitudeRef (Byte): 0 36 | GPS GPSDate (ASCII): 2018:03:30 37 | GPS GPSDestBearing (Ratio): 21278/339 38 | GPS GPSDestBearingRef (ASCII): M 39 | GPS GPSImgDirection (Ratio): 21278/339 40 | GPS GPSImgDirectionRef (ASCII): M 41 | GPS GPSLatitude (Ratio): [37, 45, 907/25] 42 | GPS GPSLatitudeRef (ASCII): N 43 | GPS GPSLongitude (Ratio): [122, 30, 861/25] 44 | GPS GPSLongitudeRef (ASCII): W 45 | GPS GPSSpeed (Ratio): 5709/10354 46 | GPS GPSSpeedRef (ASCII): K 47 | GPS GPSTimeStamp (Ratio): [19, 14, 1813/100] 48 | GPS Tag 0x001F (Ratio): 6619/1103 49 | Image DateTime (ASCII): 2018:03:30 12:14:19 50 | Image ExifOffset (Long): 204 51 | Image GPSInfo (Long): 784 52 | Image Make (ASCII): Apple 53 | Image Model (ASCII): iPhone X 54 | Image Orientation (Short): Horizontal (normal) 55 | Image ResolutionUnit (Short): Pixels/Inch 56 | Image Software (ASCII): 12.0 57 | Image XResolution (Ratio): 127/5 58 | Image YCbCrPositioning (Short): Centered 59 | Image YResolution (Ratio): 127/5 60 | -------------------------------------------------------------------------------- /lib/src/values_to_printable.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:exif/src/exif_types.dart'; 4 | import 'package:exif/src/field_types.dart'; 5 | import 'package:exif/src/reader.dart'; 6 | import 'package:exif/src/tags_info.dart'; 7 | 8 | class ValuesToPrintable { 9 | final String value; 10 | final bool malformed; 11 | 12 | const ValuesToPrintable(this.value) : malformed = false; 13 | 14 | const ValuesToPrintable.malformed(this.value) : malformed = true; 15 | 16 | factory ValuesToPrintable.convert(IfdValues values, IfdEntry entry, 17 | {required MakerTag? tagEntry, required bool truncateTags}) { 18 | // compute printable version of values 19 | if (tagEntry != null) { 20 | // optional 2nd tag element is present 21 | if (tagEntry.func != null) { 22 | // call mapping function 23 | final printable = 24 | tagEntry.func!(values.toList().whereType().toList()); 25 | return ValuesToPrintable(printable); 26 | } else if (tagEntry.map != null) { 27 | final sb = StringBuffer(); 28 | for (final i in values.toList()) { 29 | // use lookup table for this tag 30 | if (i is int) { 31 | sb.write(tagEntry.map![i] ?? i); 32 | } else { 33 | sb.write(i); 34 | } 35 | } 36 | return ValuesToPrintable(sb.toString()); 37 | } 38 | } 39 | 40 | if (entry.fieldType == FieldType.ascii && values is IfdBytes) { 41 | final bytes = values.bytes; 42 | try { 43 | return ValuesToPrintable(utf8.decode(bytes)); 44 | } on FormatException { 45 | if (truncateTags && bytes.length > 20) { 46 | return ValuesToPrintable.malformed( 47 | 'b"${bytesToStringRepr(bytes.sublist(0, 20))}, ... ]'); 48 | } 49 | return ValuesToPrintable.malformed("b'${bytesToStringRepr(bytes)}'"); 50 | } 51 | } else if (entry.count == 1) { 52 | return ValuesToPrintable(values.toList()[0].toString()); 53 | } 54 | 55 | if (entry.count > 50 && values.length > 20) { 56 | if (truncateTags) { 57 | final s = values.toList().sublist(0, 20).toString(); 58 | return ValuesToPrintable("${s.substring(0, s.length - 1)}, ... ]"); 59 | } 60 | } 61 | 62 | return ValuesToPrintable(values.toString()); 63 | } 64 | 65 | static String bytesToStringRepr(List bytes) => bytes.map((e) { 66 | switch (e) { 67 | case 9: 68 | return r'\t'; 69 | case 10: 70 | return r'\n'; 71 | case 13: 72 | return r'\r'; 73 | case 92: 74 | return r'\\'; 75 | } 76 | 77 | if (e < 32 || e >= 128) { 78 | final hex = e.toRadixString(16).padLeft(2, '0'); 79 | return "\\x$hex"; 80 | } 81 | 82 | return String.fromCharCode(e); 83 | }).join(); 84 | } 85 | -------------------------------------------------------------------------------- /test/read_samples.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io' as io; 4 | import 'dart:typed_data'; 5 | 6 | import 'package:archive/archive.dart'; 7 | import 'package:http/http.dart' as http; 8 | import 'package:path/path.dart' as p; 9 | 10 | import 'sample_file.dart'; 11 | 12 | Stream readSamples() async* { 13 | yield* readIanareSamples(); 14 | 15 | yield await readSampleFile("test/data/heic-test.heic"); 16 | } 17 | 18 | Future readSampleFile(String filename) async { 19 | final fileBytes = await io.File(filename).readAsBytes(); 20 | final dump = await io.File("$filename.dump").readAsString(); 21 | 22 | return SampleFile( 23 | name: filename, 24 | content: fileBytes, 25 | dump: dump.trim(), 26 | ); 27 | } 28 | 29 | Stream readIanareSamples() async* { 30 | const commit = "2a62d69683c154ffe03b4502bdfa3248d8a1b05c"; 31 | final filenamePrefix = p.join("test", "data", "$commit-"); 32 | 33 | final dumpFile = await downloadUrl( 34 | filenamePrefix, 35 | "https://raw.githubusercontent.com/ianare/exif-samples/$commit/dump", 36 | ); 37 | 38 | final nameToDumps = readDumpFile(dumpFile); 39 | 40 | final path = await downloadUrl( 41 | filenamePrefix, 42 | "https://github.com/ianare/exif-samples/archive/$commit.tar.gz", 43 | ); 44 | 45 | final data = io.File(path).readAsBytesSync(); 46 | 47 | final ar = TarDecoder().decodeBytes(GZipDecoder().decodeBytes(data)); 48 | 49 | for (final file in ar) { 50 | file.name = 51 | file.name.replaceAll("exif-samples-$commit", "exif-samples-master"); 52 | 53 | if (!file.name.endsWith('.jpg') && !file.name.endsWith('.tiff')) { 54 | continue; 55 | } 56 | 57 | if (!nameToDumps.containsKey(file.name)) { 58 | file.name = utf8.decode(file.name.codeUnits); 59 | } 60 | 61 | yield SampleFile( 62 | name: file.name, 63 | content: file.content as Uint8List, 64 | dump: nameToDumps[file.name], 65 | ); 66 | } 67 | } 68 | 69 | Map readDumpFile(String dumpFile) { 70 | final fileDumps = io.File(dumpFile).readAsStringSync().trim().split("\n\n"); 71 | 72 | final nameAndDumps = fileDumps.map((e) => e.split("\n")).map((e) => MapEntry( 73 | e[0].split("Opening: ")[1], 74 | e 75 | .sublist(1) 76 | .where((e) => 77 | !e.startsWith("Possibly corrupted ") && 78 | !e.startsWith("No values found for ")) 79 | .join("\n"))); 80 | 81 | return Map.fromEntries(nameAndDumps); 82 | } 83 | 84 | Future downloadUrl(String filenamePrefix, String url) async { 85 | final filename = filenamePrefix + Uri.parse(url).pathSegments.last; 86 | 87 | if (!await io.File(filename).exists()) { 88 | print('downloading $filename ..'); 89 | final res = await http.get(Uri.parse(url)); 90 | await io.File(filename).writeAsBytes(res.bodyBytes); 91 | } 92 | 93 | return filename; 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:collection/collection.dart' show ListEquality; 4 | import 'package:sprintf/sprintf.dart' show sprintf; 5 | 6 | bool listRangeEqual(List list1, int begin, int end, List list2) { 7 | var beginIndex = begin >= 0 ? begin : 0; 8 | beginIndex = beginIndex < list1.length ? beginIndex : list1.length; 9 | 10 | var endIndex = end >= begin ? end : begin; 11 | endIndex = endIndex < list1.length ? endIndex : list1.length; 12 | 13 | return listEqual(list1.sublist(beginIndex, endIndex), list2); 14 | } 15 | 16 | final listEqual = const ListEquality().equals; 17 | 18 | bool listHasPrefix(List list, List prefix, {int start = 0}) { 19 | if (prefix.isEmpty) { 20 | return true; 21 | } 22 | if (list.length - start < prefix.length) { 23 | return false; 24 | } 25 | return listEqual(list.sublist(start, start + prefix.length), prefix); 26 | } 27 | 28 | bool listContainedIn(List a, List> b) => 29 | b.any((i) => listEqual(i, a)); 30 | 31 | void printf(String a, List b) => print(sprintf(a, b)); 32 | 33 | // Don't throw an exception when given an out of range character. 34 | String makeString(List seq) { 35 | String s = String.fromCharCodes(seq.where((c) => 32 <= c && c < 256)); 36 | if (s.isEmpty) { 37 | if (seq.isEmpty || seq.reduce(max) == 0) { 38 | return ""; 39 | } 40 | s = seq.map((e) => e.toString()).join(); 41 | } 42 | return s.trim(); 43 | } 44 | 45 | // Special version to deal with the code in the first 8 bytes of a user comment. 46 | // First 8 bytes gives coding system e.g. ASCII vs. JIS vs Unicode. 47 | String makeStringUc(List seq) { 48 | if (seq.length <= 8) { 49 | return ""; 50 | } 51 | 52 | // Remove code from sequence only if it is valid 53 | if ({'ASCII', 'UNICODE', 'JIS', ''} 54 | .contains(makeString(seq.sublist(0, 8)).toUpperCase())) { 55 | seq = seq.sublist(8); 56 | } 57 | 58 | // Of course, this is only correct if ASCII, and the standard explicitly 59 | // allows JIS and Unicode. 60 | return makeString(seq); 61 | } 62 | 63 | // Extract multi-byte integer in little endian. 64 | int s2nBigEndian(List s, {bool signed = false}) { 65 | if (s.isEmpty) { 66 | return 0; 67 | } 68 | 69 | int xor = 0; 70 | if (signed && s[0] >= 128) { 71 | xor = 0xff; 72 | } 73 | 74 | int x = 0; 75 | for (final c in s) { 76 | x = (x << 8) | (c ^ xor); 77 | } 78 | 79 | if (xor != 0) { 80 | x = -(x + 1); 81 | } 82 | 83 | return x; 84 | } 85 | 86 | // Extract multi-byte integer in little endian. 87 | int s2nLittleEndian(List s, {bool signed = false}) { 88 | if (s.isEmpty) { 89 | return 0; 90 | } 91 | 92 | int xor = 0; 93 | if (signed && s.last >= 128) { 94 | xor = 0xff; 95 | } 96 | 97 | int x = 0; 98 | int y = 0; 99 | for (final int c in s) { 100 | x = x | ((c ^ xor) << y); 101 | y += 8; 102 | } 103 | 104 | if (xor != 0) { 105 | x = -(x + 1); 106 | } 107 | 108 | return x; 109 | } 110 | -------------------------------------------------------------------------------- /lib/src/exif_types.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | class IfdTag { 4 | /// tag ID number 5 | final int tag; 6 | 7 | final String tagType; 8 | 9 | /// printable version of data 10 | final String printable; 11 | 12 | /// list of data items (int(char or number) or Ratio) 13 | final IfdValues values; 14 | 15 | IfdTag({ 16 | required this.tag, 17 | required this.tagType, 18 | required this.printable, 19 | required this.values, 20 | }); 21 | 22 | @override 23 | String toString() => printable; 24 | } 25 | 26 | abstract class IfdValues { 27 | const IfdValues(); 28 | 29 | List toList(); 30 | 31 | int get length; 32 | 33 | int firstAsInt(); 34 | } 35 | 36 | class IfdNone extends IfdValues { 37 | const IfdNone(); 38 | 39 | @override 40 | List toList() => []; 41 | 42 | @override 43 | int get length => 0; 44 | 45 | @override 46 | int firstAsInt() => 0; 47 | 48 | @override 49 | String toString() => "[]"; 50 | } 51 | 52 | class IfdRatios extends IfdValues { 53 | final List ratios; 54 | 55 | const IfdRatios(this.ratios); 56 | 57 | @override 58 | List toList() => ratios; 59 | 60 | @override 61 | int get length => ratios.length; 62 | 63 | @override 64 | int firstAsInt() => ratios[0].toInt(); 65 | 66 | @override 67 | String toString() => ratios.toString(); 68 | } 69 | 70 | class IfdInts extends IfdValues { 71 | final List ints; 72 | 73 | const IfdInts(this.ints); 74 | 75 | @override 76 | List toList() => ints; 77 | 78 | @override 79 | int get length => ints.length; 80 | 81 | @override 82 | int firstAsInt() => ints[0]; 83 | 84 | @override 85 | String toString() => ints.toString(); 86 | } 87 | 88 | class IfdBytes extends IfdValues { 89 | final Uint8List bytes; 90 | 91 | IfdBytes(this.bytes); 92 | 93 | IfdBytes.empty() : bytes = Uint8List(0); 94 | 95 | IfdBytes.fromList(List list) : bytes = Uint8List.fromList(list); 96 | 97 | @override 98 | List toList() => bytes; 99 | 100 | @override 101 | int get length => bytes.length; 102 | 103 | @override 104 | int firstAsInt() => bytes[0]; 105 | 106 | @override 107 | String toString() => bytes.toString(); 108 | } 109 | 110 | /// Ratio object that eventually will be able to reduce itself to lowest 111 | /// common denominator for printing. 112 | class Ratio { 113 | final int numerator; 114 | final int denominator; 115 | 116 | factory Ratio(int num, int den) { 117 | if (den < 0) { 118 | num *= -1; 119 | den *= -1; 120 | } 121 | 122 | final d = num.gcd(den); 123 | if (d > 1) { 124 | num = num ~/ d; 125 | den = den ~/ d; 126 | } 127 | 128 | return Ratio._internal(num, den); 129 | } 130 | 131 | Ratio._internal(this.numerator, this.denominator); 132 | 133 | @override 134 | String toString() => 135 | (denominator == 1) ? '$numerator' : '$numerator/$denominator'; 136 | 137 | int toInt() => numerator ~/ denominator; 138 | 139 | double toDouble() => numerator / denominator; 140 | } 141 | 142 | class ExifData { 143 | final Map tags; 144 | final List warnings; 145 | 146 | const ExifData(this.tags, this.warnings); 147 | 148 | ExifData.withWarning(String warning) : this(const {}, [warning]); 149 | } 150 | -------------------------------------------------------------------------------- /test/data/heic-test.heic.dump: -------------------------------------------------------------------------------- 1 | EXIF ApertureValue (Ratio): 4845/1918 2 | EXIF BrightnessValue (Signed Ratio): 7187/850 3 | EXIF ColorSpace (Short): Uncalibrated 4 | EXIF ComponentsConfiguration (Undefined): YCbCr 5 | EXIF CustomRendered (Short): 2 6 | EXIF DateTimeDigitized (ASCII): 2018:03:30 12:14:19 7 | EXIF DateTimeOriginal (ASCII): 2018:03:30 12:14:19 8 | EXIF ExifImageLength (Long): 3024 9 | EXIF ExifImageWidth (Long): 4032 10 | EXIF ExifVersion (Undefined): 0221 11 | EXIF ExposureBiasValue (Signed Ratio): 0 12 | EXIF ExposureMode (Short): Auto Exposure 13 | EXIF ExposureProgram (Short): Program Normal 14 | EXIF ExposureTime (Ratio): 1/209 15 | EXIF FNumber (Ratio): 12/5 16 | EXIF Flash (Short): Flash did not fire, compulsory flash mode 17 | EXIF FlashPixVersion (Undefined): 0100 18 | EXIF FocalLength (Ratio): 6 19 | EXIF FocalLengthIn35mmFilm (Short): 52 20 | EXIF ISOSpeedRatings (Short): 16 21 | EXIF LensMake (ASCII): Apple 22 | EXIF LensModel (ASCII): iPhone X back dual camera 6mm f/2.4 23 | EXIF LensSpecification (Ratio): [4, 6, 9/5, 12/5] 24 | EXIF MakerNote (Undefined): [65, 112, 112, 108, 101, 32, 105, 79, 83, 0, 0, 1, 77, 77, 0, 21, 0, 1, 0, 9, ... ] 25 | EXIF MeteringMode (Short): Pattern 26 | EXIF SceneCaptureType (Short): Standard 27 | EXIF SceneType (Undefined): Directly Photographed 28 | EXIF SensingMethod (Short): One-chip color area 29 | EXIF ShutterSpeedValue (Signed Ratio): 7789/1011 30 | EXIF SubSecTimeDigitized (ASCII): 365 31 | EXIF SubSecTimeOriginal (ASCII): 365 32 | EXIF SubjectArea (Short): [2007, 1503, 2209, 1327] 33 | EXIF WhiteBalance (Short): Auto 34 | GPS GPSAltitude (Ratio): 6568/1433 35 | GPS GPSAltitudeRef (Byte): 0 36 | GPS GPSDate (ASCII): 2018:03:30 37 | GPS GPSDestBearing (Ratio): 21278/339 38 | GPS GPSDestBearingRef (ASCII): M 39 | GPS GPSImgDirection (Ratio): 21278/339 40 | GPS GPSImgDirectionRef (ASCII): M 41 | GPS GPSLatitude (Ratio): [37, 45, 907/25] 42 | GPS GPSLatitudeRef (ASCII): N 43 | GPS GPSLongitude (Ratio): [122, 30, 861/25] 44 | GPS GPSLongitudeRef (ASCII): W 45 | GPS GPSSpeed (Ratio): 5709/10354 46 | GPS GPSSpeedRef (ASCII): K 47 | GPS GPSTimeStamp (Ratio): [19, 14, 1813/100] 48 | GPS Tag 0x001F (Ratio): 6619/1103 49 | Image DateTime (ASCII): 2018:03:30 12:14:19 50 | Image ExifOffset (Long): 204 51 | Image GPSInfo (Long): 1818 52 | Image Make (ASCII): Apple 53 | Image Model (ASCII): iPhone X 54 | Image Orientation (Short): Rotated 180 55 | Image ResolutionUnit (Short): Pixels/Inch 56 | Image Software (ASCII): 12.0 57 | Image XResolution (Ratio): 72 58 | Image YCbCrPositioning (Short): Centered 59 | Image YResolution (Ratio): 72 60 | MakerNote HDRImageType (Signed Long): 2 61 | MakerNote Tag 0x0001 (Signed Long): 10 62 | MakerNote Tag 0x0002 (Undefined): [2, 1, 122, 0, 107, 0, 120, 0, 67, 0, 69, 0, 59, 0, 57, 0, 28, 1, 85, 2, ... ] 63 | MakerNote Tag 0x0003 (Undefined): [6, 7, 8, 85, 102, 108, 97, 103, 115, 85, 118, 97, 108, 117, 101, 89, 116, 105, 109, 101, ... ] 64 | MakerNote Tag 0x0004 (Signed Long): 1 65 | MakerNote Tag 0x0005 (Signed Long): 173 66 | MakerNote Tag 0x0006 (Signed Long): 170 67 | MakerNote Tag 0x0007 (Signed Long): 1 68 | MakerNote Tag 0x0008 (Signed Ratio): [-69926911/22675456, 22145/723, 256/7305] 69 | MakerNote Tag 0x000C (Signed Ratio): [2100775/105295456, 1/200] 70 | MakerNote Tag 0x000D (Signed Long): 25 71 | MakerNote Tag 0x000E (Signed Long): 4 72 | MakerNote Tag 0x0010 (Signed Long): 1 73 | MakerNote Tag 0x0014 (Signed Long): 3 74 | MakerNote Tag 0x0017 (Signed Long): 2048 75 | MakerNote Tag 0x0019 (Signed Long): 34 76 | MakerNote Tag 0x001A (ASCII): C55AB7 77 | MakerNote Tag 0x001D (Signed Ratio): 218813911/175351561 78 | MakerNote Tag 0x001F (Signed Long): 1 79 | MakerNote Tag 0x0020 (ASCII): 48FC-9F8C-9CCF675F4C62 80 | MakerNote Tag 0x0021 (Signed Ratio): 1/6 81 | -------------------------------------------------------------------------------- /lib/src/makernote_fujifilm.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/tags_info.dart' show MakerTag, MakerTagFunc, TagsBase; 2 | import 'package:exif/src/util.dart'; 3 | 4 | // Makernote (proprietary) tag definitions for FujiFilm. 5 | // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/FujiFilm.html 6 | 7 | class MakerNoteFujifilm extends TagsBase { 8 | static MakerTag _make(String name) => MakerTag.make(name); 9 | 10 | static MakerTag _withMap(String name, Map map) => 11 | MakerTag.makeWithMap(name, map); 12 | 13 | static MakerTag _withFunc(String name, MakerTagFunc func) => 14 | MakerTag.makeWithFunc(name, func); 15 | 16 | static final tags = { 17 | 0x0000: _withFunc('NoteVersion', makeString), 18 | 0x0010: _make('InternalSerialNumber'), 19 | 0x1000: _make('Quality'), 20 | 0x1001: _withMap('Sharpness', { 21 | 0x1: 'Soft', 22 | 0x2: 'Soft', 23 | 0x3: 'Normal', 24 | 0x4: 'Hard', 25 | 0x5: 'Hard2', 26 | 0x82: 'Medium Soft', 27 | 0x84: 'Medium Hard', 28 | 0x8000: 'Film Simulation' 29 | }), 30 | 0x1002: _withMap('WhiteBalance', { 31 | 0x0: 'Auto', 32 | 0x100: 'Daylight', 33 | 0x200: 'Cloudy', 34 | 0x300: 'Daylight Fluorescent', 35 | 0x301: 'Day White Fluorescent', 36 | 0x302: 'White Fluorescent', 37 | 0x303: 'Warm White Fluorescent', 38 | 0x304: 'Living Room Warm White Fluorescent', 39 | 0x400: 'Incandescent', 40 | 0x500: 'Flash', 41 | 0x600: 'Underwater', 42 | 0xf00: 'Custom', 43 | 0xf01: 'Custom2', 44 | 0xf02: 'Custom3', 45 | 0xf03: 'Custom4', 46 | 0xf04: 'Custom5', 47 | 0xff0: 'Kelvin' 48 | }), 49 | 0x1003: _withMap('Saturation', { 50 | 0x0: 'Normal', 51 | 0x80: 'Medium High', 52 | 0x100: 'High', 53 | 0x180: 'Medium Low', 54 | 0x200: 'Low', 55 | 0x300: 'None (B&W)', 56 | 0x301: 'B&W Red Filter', 57 | 0x302: 'B&W Yellow Filter', 58 | 0x303: 'B&W Green Filter', 59 | 0x310: 'B&W Sepia', 60 | 0x400: 'Low 2', 61 | 0x8000: 'Film Simulation' 62 | }), 63 | 0x1004: _withMap('Contrast', { 64 | 0x0: 'Normal', 65 | 0x80: 'Medium High', 66 | 0x100: 'High', 67 | 0x180: 'Medium Low', 68 | 0x200: 'Low', 69 | 0x8000: 'Film Simulation' 70 | }), 71 | 0x1005: _make('ColorTemperature'), 72 | 0x1006: _withMap('Contrast', {0x0: 'Normal', 0x100: 'High', 0x300: 'Low'}), 73 | 0x100a: _make('WhiteBalanceFineTune'), 74 | 0x1010: _withMap( 75 | 'FlashMode', {0: 'Auto', 1: 'On', 2: 'Off', 3: 'Red Eye Reduction'}), 76 | 0x1011: _make('FlashStrength'), 77 | 0x1020: _withMap('Macro', {0: 'Off', 1: 'On'}), 78 | 0x1021: _withMap('FocusMode', {0: 'Auto', 1: 'Manual'}), 79 | 0x1022: _withMap('AFPointSet', {0: 'Yes', 1: 'No'}), 80 | 0x1023: _make('FocusPixel'), 81 | 0x1030: _withMap('SlowSync', {0: 'Off', 1: 'On'}), 82 | 0x1031: _withMap('PictureMode', { 83 | 0: 'Auto', 84 | 1: 'Portrait', 85 | 2: 'Landscape', 86 | 4: 'Sports', 87 | 5: 'Night', 88 | 6: 'Program AE', 89 | 256: 'Aperture Priority AE', 90 | 512: 'Shutter Priority AE', 91 | 768: 'Manual Exposure' 92 | }), 93 | 0x1032: _make('ExposureCount'), 94 | 0x1100: _withMap('MotorOrBracket', {0: 'Off', 1: 'On'}), 95 | 0x1210: 96 | _withMap('ColorMode', {0x0: 'Standard', 0x10: 'Chrome', 0x30: 'B & W'}), 97 | 0x1300: _withMap('BlurWarning', {0: 'Off', 1: 'On'}), 98 | 0x1301: _withMap('FocusWarning', {0: 'Off', 1: 'On'}), 99 | 0x1302: _withMap('ExposureWarning', {0: 'Off', 1: 'On'}), 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/exif_thumbnail.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:exif/src/exifheader.dart'; 4 | import 'package:exif/src/field_types.dart'; 5 | import 'package:exif/src/reader.dart'; 6 | 7 | class Thumbnail { 8 | final Map tags; 9 | final IfdReader file; 10 | 11 | Thumbnail(this.tags, this.file); 12 | 13 | // Extract uncompressed TIFF thumbnail. 14 | // Take advantage of the pre-existing layout in the thumbnail IFD as 15 | // much as possible 16 | List? extractTiffThumbnail(int thumbIfd) { 17 | final thumb = tags['Thumbnail Compression']; 18 | if (thumb == null || thumb.tag.printable != 'Uncompressed TIFF') { 19 | return null; 20 | } 21 | 22 | List tiff; 23 | int stripOff = 0; 24 | int stripLen = 0; 25 | 26 | final entries = file.readInt(thumbIfd, 2); 27 | // this is header plus offset to IFD ... 28 | if (file.endian == Endian.big) { 29 | tiff = 'MM\x00*\x00\x00\x00\x08'.codeUnits; 30 | } else { 31 | tiff = 'II*\x00\x08\x00\x00\x00'.codeUnits; 32 | // ... plus thumbnail IFD data plus a null "next IFD" pointer 33 | } 34 | 35 | tiff.addAll(file.readSlice(thumbIfd, entries * 12 + 2)); 36 | tiff.addAll([0, 0, 0, 0]); 37 | 38 | // fix up large value offset pointers into data area 39 | for (int i = 0; i < entries; i++) { 40 | final entry = thumbIfd + 2 + 12 * i; 41 | final tag = file.readInt(entry, 2); 42 | final fieldType = file.readInt(entry + 2, 2); 43 | final typeLength = fieldTypes[fieldType].length; 44 | final count = file.readInt(entry + 4, 4); 45 | final oldOffset = file.readInt(entry + 8, 4); 46 | // start of the 4-byte pointer area in entry 47 | final ptr = i * 12 + 18; 48 | // remember strip offsets location 49 | if (tag == 0x0111) { 50 | stripOff = ptr; 51 | stripLen = count * typeLength; 52 | // is it in the data area? 53 | } 54 | if (count * typeLength > 4) { 55 | // update offset pointer (nasty "strings are immutable" crap) 56 | // should be able to say "tiff[ptr:ptr+4]=newOffset" 57 | final tiff0 = tiff; 58 | final newOffset = tiff0.length; 59 | tiff = tiff0.sublist(0, ptr); 60 | tiff.addAll(file.offsetToBytes(newOffset, 4)); 61 | tiff.addAll(tiff0.sublist(ptr + 4)); 62 | // remember strip offsets location 63 | if (tag == 0x0111) { 64 | stripOff = newOffset; 65 | stripLen = 4; 66 | } 67 | // get original data and store it 68 | tiff.addAll(file.readSlice(oldOffset, count * typeLength)); 69 | } 70 | } 71 | 72 | // add pixel strips and update strip offset info 73 | final oldOffsets = tags['Thumbnail StripOffsets']?.tag.values.toList(); 74 | final oldCounts = tags['Thumbnail StripByteCounts']?.tag.values.toList(); 75 | if (oldOffsets == null || oldCounts == null) { 76 | return null; 77 | } 78 | 79 | for (int i = 0; i < oldOffsets.length; i++) { 80 | // update offset pointer (more nasty "strings are immutable" crap) 81 | final tiff0 = tiff; 82 | final offset = file.offsetToBytes(tiff0.length, stripLen); 83 | tiff = tiff0.sublist(0, stripOff); 84 | tiff.addAll(offset); 85 | tiff.addAll(tiff0.sublist(stripOff + stripLen)); 86 | stripOff += stripLen; 87 | // add pixel strip to end 88 | tiff.addAll(file.readSlice(oldOffsets[i] as int, oldCounts[i] as int)); 89 | } 90 | 91 | return tiff; 92 | } 93 | 94 | // Extract JPEG thumbnail. 95 | // (Thankfully the JPEG data is stored as a unit.) 96 | List? extractJpegThumbnail() { 97 | final thumbFmt = tags['Thumbnail JPEGInterchangeFormat']; 98 | final thumbFmtLen = tags['Thumbnail JPEGInterchangeFormatLength']; 99 | if (thumbFmt != null && thumbFmtLen != null) { 100 | final size = thumbFmtLen.tag.values.firstAsInt(); 101 | final values = file.readSlice(thumbFmt.tag.values.firstAsInt(), size); 102 | return values; 103 | } 104 | 105 | // Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote 106 | // since it's not allowed in a uncompressed TIFF IFD 107 | final thumbnail = tags['MakerNote JPEGThumbnail']; 108 | if (thumbnail != null) { 109 | final values = file.readSlice( 110 | thumbnail.tag.values.firstAsInt(), thumbnail.fieldLength); 111 | return values; 112 | } 113 | 114 | return null; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/src/exifheader.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/exif_thumbnail.dart'; 2 | import 'package:exif/src/exif_types.dart'; 3 | import 'package:exif/src/field_types.dart'; 4 | import 'package:exif/src/reader.dart'; 5 | import 'package:exif/src/tags.dart'; 6 | import 'package:exif/src/tags_info.dart'; 7 | import 'package:exif/src/values_to_printable.dart'; 8 | import 'package:sprintf/sprintf.dart' show sprintf; 9 | 10 | const defaultStopTag = 'UNDEF'; 11 | 12 | // To ignore when quick processing 13 | const ignoreTags = [ 14 | 0x9286, // user comment 15 | 0x927C, // MakerNote Tags 16 | 0x02BC, // XPM 17 | ]; 18 | 19 | // Eases dealing with tags. 20 | class IfdTagImpl { 21 | final IfdTag tag; 22 | 23 | final FieldType fieldType; 24 | 25 | // offset of start of field in bytes from beginning of IFD 26 | int fieldOffset; 27 | 28 | // length of data field in bytes 29 | int fieldLength; 30 | 31 | IfdTagImpl({ 32 | this.fieldType = FieldType.proprietary, 33 | this.fieldOffset = 0, 34 | this.fieldLength = 0, 35 | String printable = '', 36 | int tag = -1, 37 | IfdValues values = const IfdNone(), 38 | }) : tag = IfdTag( 39 | tag: tag, 40 | tagType: fieldType.name, 41 | printable: printable, 42 | values: values, 43 | ); 44 | } 45 | 46 | /// Handle an EXIF header. 47 | class ExifHeader { 48 | bool strict; 49 | bool debug; 50 | bool detailed; 51 | bool truncateTags; 52 | Map tags = {}; 53 | List warnings = []; 54 | IfdReader file; 55 | 56 | ExifHeader({ 57 | required this.file, 58 | required this.strict, 59 | this.debug = false, 60 | this.detailed = true, 61 | this.truncateTags = true, 62 | }); 63 | 64 | // Return a list of entries in the given IFD. 65 | void dumpIfd(int ifd, String ifdName, 66 | {Map? tagDict, bool relative = false, String? stopTag}) { 67 | stopTag ??= defaultStopTag; 68 | tagDict ??= StandardTags.tags; 69 | 70 | // make sure we can process the entries 71 | List entries; 72 | try { 73 | entries = file.readIfdEntries(ifd, relative: relative); 74 | } catch (e) { 75 | warnings.add("Possibly corrupted IFD: $ifd"); 76 | return; 77 | } 78 | 79 | for (final entry in entries) { 80 | // get tag name early to avoid errors, help debug 81 | final MakerTag? tagEntry = tagDict[entry.tag]; 82 | String tagName; 83 | if (tagEntry != null) { 84 | tagName = tagEntry.name; 85 | } else { 86 | tagName = sprintf('Tag 0x%04X', [entry.tag]); 87 | } 88 | 89 | // ignore certain tags for faster processing 90 | if (detailed || !ignoreTags.contains(entry.tag)) { 91 | processTag( 92 | ifd: ifd, 93 | ifdName: ifdName, 94 | tagEntry: tagEntry, 95 | entry: entry, 96 | tagName: tagName, 97 | relative: relative, 98 | stopTag: stopTag); 99 | 100 | if (tagName == stopTag) { 101 | break; 102 | } 103 | } 104 | } 105 | } 106 | 107 | void processTag( 108 | {required int ifd, 109 | required String ifdName, 110 | required MakerTag? tagEntry, 111 | required IfdEntry entry, 112 | required String tagName, 113 | required bool relative, 114 | required String? stopTag}) { 115 | // unknown field type 116 | if (!entry.fieldType.isValid) { 117 | if (!strict) { 118 | return; 119 | } else { 120 | throw FormatException(sprintf( 121 | 'Unknown type %d in tag 0x%04X', [entry.fieldType, entry.tag])); 122 | } 123 | } 124 | 125 | final values = file.readField(entry, tagName: tagName); 126 | 127 | // now 'values' is either a string or an array 128 | final printable = ValuesToPrintable.convert(values, entry, 129 | tagEntry: tagEntry, truncateTags: truncateTags); 130 | if (printable.malformed) { 131 | warnings.add("Possibly corrupted field $tagName in $ifdName IFD"); 132 | } 133 | 134 | final makerTags = tagEntry?.tags; 135 | if (makerTags != null) { 136 | try { 137 | dumpIfd(values.firstAsInt(), makerTags.name, 138 | tagDict: makerTags.tags, stopTag: stopTag); 139 | } on RangeError { 140 | warnings.add('No values found for ${makerTags.name} SubIFD'); 141 | } 142 | } 143 | 144 | tags['$ifdName $tagName'] = IfdTagImpl( 145 | printable: printable.value, 146 | tag: entry.tag, 147 | fieldType: entry.fieldType, 148 | values: values, 149 | fieldOffset: entry.fieldOffset, 150 | fieldLength: entry.count * entry.fieldType.length); 151 | 152 | // var t = tags[ifd_name + ' ' + tag_name]; 153 | } 154 | 155 | void extractTiffThumbnail(int thumbIfd) { 156 | final values = Thumbnail(tags, file).extractTiffThumbnail(thumbIfd); 157 | if (values != null) { 158 | tags['TIFFThumbnail'] = IfdTagImpl(values: IfdBytes.fromList(values)); 159 | } 160 | } 161 | 162 | void extractJpegThumbnail() { 163 | final values = Thumbnail(tags, file).extractJpegThumbnail(); 164 | if (values != null) { 165 | tags['JPEGThumbnail'] = IfdTagImpl(values: IfdBytes.fromList(values)); 166 | } 167 | } 168 | 169 | void parseXmp(String xmpString) { 170 | tags['Image ApplicationNotes'] = 171 | IfdTagImpl(printable: xmpString, fieldType: FieldType.byte); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/src/reader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:exif/src/exif_types.dart'; 4 | import 'package:exif/src/field_types.dart'; 5 | import 'package:exif/src/file_interface.dart'; 6 | import 'package:exif/src/makernote_canon.dart'; 7 | import 'package:exif/src/util.dart'; 8 | 9 | class Reader { 10 | FileReader file; 11 | int baseOffset; 12 | Endian endian; 13 | 14 | Reader(this.file, this.baseOffset, this.endian); 15 | 16 | List readSlice(int relativePos, int length) { 17 | file.setPositionSync(baseOffset + relativePos); 18 | return file.readSync(length); 19 | } 20 | 21 | // Convert slice to integer, based on sign and endian flags. 22 | // Usually this offset is assumed to be relative to the beginning of the 23 | // start of the EXIF information. 24 | // For some cameras that use relative tags, this offset may be relative 25 | // to some other starting point. 26 | int readInt(int offset, int length, {bool signed = false}) { 27 | final sliced = readSlice(offset, length); 28 | int val; 29 | 30 | if (endian == Endian.little) { 31 | val = s2nLittleEndian(sliced, signed: signed); 32 | } else { 33 | val = s2nBigEndian(sliced, signed: signed); 34 | } 35 | 36 | return val; 37 | } 38 | 39 | Ratio readRatio(int offset, {required bool signed}) { 40 | final n = readInt(offset, 4, signed: signed); 41 | final d = readInt(offset + 4, 4, signed: signed); 42 | return Ratio(n, d); 43 | } 44 | 45 | // Convert offset to string. 46 | List offsetToBytes(int readOffset, int length) { 47 | final List s = []; 48 | for (int dummy = 0; dummy < length; dummy++) { 49 | if (endian == Endian.little) { 50 | s.add(readOffset & 0xFF); 51 | } else { 52 | s.insert(0, readOffset & 0xFF); 53 | } 54 | readOffset = readOffset >> 8; 55 | } 56 | return s; 57 | } 58 | 59 | static Endian endianOfByte(int b) { 60 | if (b == 'I'.codeUnitAt(0)) { 61 | return Endian.little; 62 | } 63 | return Endian.big; 64 | } 65 | } 66 | 67 | class IfdReader { 68 | Reader file; 69 | final bool fakeExif; 70 | 71 | IfdReader(this.file, {required this.fakeExif}); 72 | 73 | // Return first IFD. 74 | int _firstIfd() => file.readInt(4, 4); 75 | 76 | // Return the pointer to next IFD. 77 | int _nextIfd(int ifd) { 78 | final entries = file.readInt(ifd, 2); 79 | final nextIfd = file.readInt(ifd + 2 + 12 * entries, 4); 80 | if (nextIfd == ifd) { 81 | return 0; 82 | } else { 83 | return nextIfd; 84 | } 85 | } 86 | 87 | // Return the list of IFDs in the header. 88 | List listIfd() { 89 | int i = _firstIfd(); 90 | final List ifds = []; 91 | while (i > 0) { 92 | ifds.add(i); 93 | i = _nextIfd(i); 94 | } 95 | return ifds; 96 | } 97 | 98 | List readIfdEntries(int ifd, {required bool relative}) { 99 | final numEntries = file.readInt(ifd, 2); 100 | 101 | return List.generate(numEntries, (i) { 102 | // entry is index of start of this IFD in the file 103 | final offset = ifd + 2 + 12 * i; 104 | final tag = file.readInt(offset, 2); 105 | final fieldType = FieldType.ofValue(file.readInt(offset + 2, 2)); 106 | final count = file.readInt(offset + 4, 4); 107 | 108 | final typeLength = fieldType.length; 109 | 110 | // Adjust for tag id/type/count (2+2+4 bytes) 111 | // Now we point at either the data or the 2nd level offset 112 | int fieldOffset = offset + 8; 113 | 114 | // If the value fits in 4 bytes, it is inlined, else we 115 | // need to jump ahead again. 116 | if (count * typeLength > 4) { 117 | // offset is not the value; it's a pointer to the value 118 | // if relative we set things up so s2n will seek to the right 119 | // place when it adds this.offset. Note that this 'relative' 120 | // is for the Nikon type 3 makernote. Other cameras may use 121 | // other relative offsets, which would have to be computed here 122 | // slightly differently. 123 | if (relative) { 124 | fieldOffset = file.readInt(fieldOffset, 4) + ifd - 8; 125 | if (fakeExif) { 126 | fieldOffset += 18; 127 | } 128 | } else { 129 | fieldOffset = file.readInt(fieldOffset, 4); 130 | } 131 | } 132 | 133 | return IfdEntry( 134 | fieldOffset: fieldOffset, 135 | tag: tag, 136 | fieldType: fieldType, 137 | count: count); 138 | }); 139 | } 140 | 141 | Endian get endian => file.endian; 142 | 143 | set endian(Endian e) { 144 | file.endian = e; 145 | } 146 | 147 | int get baseOffset => file.baseOffset; 148 | 149 | set baseOffset(int v) { 150 | file.baseOffset = v; 151 | } 152 | 153 | int readInt(int offset, int length, {bool signed = false}) { 154 | return file.readInt(offset, length, signed: signed); 155 | } 156 | 157 | List readSlice(int relativePos, int length) { 158 | return file.readSlice(relativePos, length); 159 | } 160 | 161 | IfdRatios _readIfdRatios(IfdEntry entry) { 162 | final List values = []; 163 | var pos = entry.fieldOffset; 164 | for (int dummy = 0; dummy < entry.count; dummy++) { 165 | values.add(file.readRatio(pos, signed: entry.fieldType.isSigned)); 166 | pos += entry.fieldType.length; 167 | } 168 | return IfdRatios(values); 169 | } 170 | 171 | IfdInts _readIfdInts(IfdEntry entry) { 172 | final List values = []; 173 | var pos = entry.fieldOffset; 174 | for (int dummy = 0; dummy < entry.count; dummy++) { 175 | values.add(file.readInt(pos, entry.fieldType.length, 176 | signed: entry.fieldType.isSigned)); 177 | pos += entry.fieldType.length; 178 | } 179 | return IfdInts(values); 180 | } 181 | 182 | IfdBytes _readAscii(IfdEntry entry) { 183 | var count = entry.count; 184 | // special case: null-terminated ASCII string 185 | // XXX investigate 186 | // sometimes gets too big to fit in int value 187 | if (count <= 0) { 188 | return IfdBytes.empty(); 189 | } 190 | 191 | if (count > 1024 * 1024) { 192 | count = 1024 * 1024; 193 | } 194 | 195 | try { 196 | // and count < (2**31)) // 2E31 is hardware dependant. --gd 197 | var values = file.readSlice(entry.fieldOffset, count); 198 | // Drop any garbage after a null. 199 | final i = values.indexOf(0); 200 | if (i >= 0) { 201 | values = values.sublist(0, i); 202 | } 203 | return IfdBytes(Uint8List.fromList(values)); 204 | } catch (e) { 205 | // warnings.add("exception($e) at position: $filePosition, length: $count"); 206 | return IfdBytes.empty(); 207 | } 208 | } 209 | 210 | IfdValues readField(IfdEntry entry, {required String tagName}) { 211 | if (entry.fieldType == FieldType.ascii) { 212 | return _readAscii(entry); 213 | } 214 | 215 | // XXX investigate 216 | // some entries get too big to handle could be malformed 217 | // file or problem with this.s2n 218 | if (entry.count < 1000) { 219 | if (entry.fieldType == FieldType.ratio || 220 | entry.fieldType == FieldType.signedRatio) { 221 | return _readIfdRatios(entry); 222 | } else { 223 | return _readIfdInts(entry); 224 | } 225 | // The test above causes problems with tags that are 226 | // supposed to have long values! Fix up one important case. 227 | } else if (tagName == 'MakerNote' || 228 | tagName == MakerNoteCanon.cameraInfoTagName) { 229 | return _readIfdInts(entry); 230 | } 231 | return const IfdNone(); 232 | } 233 | 234 | List offsetToBytes(int readOffset, int length) { 235 | return file.offsetToBytes(readOffset, length); 236 | } 237 | } 238 | 239 | class IfdEntry { 240 | final int fieldOffset; 241 | final int tag; 242 | final FieldType fieldType; 243 | final int count; 244 | 245 | IfdEntry({ 246 | required this.fieldOffset, 247 | required this.tag, 248 | required this.fieldType, 249 | required this.count, 250 | }); 251 | } 252 | -------------------------------------------------------------------------------- /lib/src/heic.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:exif/src/file_interface.dart'; 4 | import 'package:exif/src/util.dart'; 5 | 6 | class HeicBox { 7 | final String name; 8 | 9 | int version = 0; 10 | int minorVersion = 0; 11 | int itemCount = 0; 12 | int size = 0; 13 | int after = 0; 14 | int pos = 0; 15 | List compat = []; 16 | 17 | // this is full of boxes, but not in a predictable order. 18 | Map subs = {}; 19 | Map>> locs = {}; 20 | HeicBox? exifInfe; 21 | int itemId = 0; 22 | Uint8List? itemType; 23 | Uint8List? itemName; 24 | int itemProtectionIndex = 0; 25 | Uint8List? majorBrand; 26 | int flags = 0; 27 | 28 | HeicBox(this.name); 29 | 30 | void setFull(int vflags) { 31 | /** 32 | ISO boxes come in 'old' and 'full' variants. 33 | The 'full' variant contains version and flags information. 34 | */ 35 | version = vflags >> 24; 36 | flags = vflags & 0x00ffffff; 37 | } 38 | } 39 | 40 | class HEICExifFinder { 41 | final FileReader fileReader; 42 | 43 | const HEICExifFinder(this.fileReader); 44 | 45 | Uint8List getBytes(int nbytes) { 46 | final bytes = fileReader.readSync(nbytes); 47 | if (bytes.length != nbytes) { 48 | throw Exception("Bad size"); 49 | } 50 | return Uint8List.fromList(bytes); 51 | } 52 | 53 | int getInt(int size) { 54 | // some fields have variant-sized data. 55 | if (size == 2) { 56 | return ByteData.view(getBytes(2).buffer).getInt16(0); 57 | } 58 | if (size == 4) { 59 | return ByteData.view(getBytes(4).buffer).getInt32(0); 60 | } 61 | if (size == 8) { 62 | return ByteData.view(getBytes(8).buffer).getInt64(0); 63 | } 64 | if (size == 0) { 65 | return 0; 66 | } 67 | throw Exception("Bad size"); 68 | } 69 | 70 | Uint8List getString() { 71 | final List read = []; 72 | while (true) { 73 | final char = getBytes(1); 74 | if (listEqual(char, Uint8List.fromList('\x00'.codeUnits))) { 75 | break; 76 | } 77 | read.add(char); 78 | } 79 | return Uint8List.fromList(read.expand((x) => x).toList()); 80 | } 81 | 82 | List getInt4x2() { 83 | final num = getBytes(1).single; 84 | final num0 = num >> 4; 85 | final num1 = num & 0xf; 86 | return [num0, num1]; 87 | } 88 | 89 | HeicBox nextBox() { 90 | final pos = fileReader.positionSync(); 91 | int size = ByteData.view(getBytes(4).buffer).getInt32(0); 92 | final kind = String.fromCharCodes(getBytes(4)); 93 | final box = HeicBox(kind); 94 | if (size == 0) { 95 | // signifies 'to the end of the file', we shouldn't see this. 96 | throw Exception("Unknown error"); 97 | } 98 | if (size == 1) { 99 | // 64-bit size follows type. 100 | size = ByteData.view(getBytes(8).buffer).getInt64(0); 101 | box.size = size - 16; 102 | box.after = pos + size; 103 | } else { 104 | box.size = size - 8; 105 | box.after = pos + size; 106 | } 107 | box.pos = fileReader.positionSync(); 108 | return box; 109 | } 110 | 111 | void _parseFtyp(HeicBox box) { 112 | box.majorBrand = getBytes(4); 113 | box.minorVersion = ByteData.view(getBytes(4).buffer).getInt32(0); 114 | box.compat = []; 115 | int size = box.size - 8; 116 | while (size > 0) { 117 | box.compat.add(getBytes(4)); 118 | size -= 4; 119 | } 120 | } 121 | 122 | void _parseMeta(HeicBox meta) { 123 | meta.setFull(ByteData.view(getBytes(4).buffer).getInt32(0)); 124 | while (fileReader.positionSync() < meta.after) { 125 | final box = nextBox(); 126 | final psub = getParser(box); 127 | if (psub != null) { 128 | psub(box); 129 | meta.subs[box.name] = box; 130 | } 131 | // skip any unparsed data 132 | fileReader.setPositionSync(box.after); 133 | } 134 | } 135 | 136 | void _parseInfe(HeicBox box) { 137 | box.setFull(ByteData.view(getBytes(4).buffer).getInt32(0)); 138 | if (box.version >= 2) { 139 | if (box.version == 2) { 140 | box.itemId = ByteData.view(getBytes(2).buffer).getInt16(0); 141 | } else if (box.version == 3) { 142 | box.itemId = ByteData.view(getBytes(4).buffer).getInt32(0); 143 | } 144 | box.itemProtectionIndex = ByteData.view(getBytes(2).buffer).getInt16(0); 145 | box.itemType = getBytes(4); 146 | box.itemName = getString(); 147 | // ignore the rest 148 | } 149 | } 150 | 151 | void _parseIinf(HeicBox box) { 152 | box.setFull(ByteData.view(getBytes(4).buffer).getInt32(0)); 153 | final count = ByteData.view(getBytes(2).buffer).getInt16(0); 154 | box.exifInfe = null; 155 | for (var i = 0; i < count; i += 1) { 156 | final infe = expectParse('infe'); 157 | if (listEqual(infe.itemType, Uint8List.fromList('Exif'.codeUnits))) { 158 | box.exifInfe = infe; 159 | break; 160 | } 161 | } 162 | } 163 | 164 | void _parseIloc(HeicBox box) { 165 | box.setFull(ByteData.view(getBytes(4).buffer).getInt32(0)); 166 | final size = getInt4x2(); 167 | final size2 = getInt4x2(); 168 | 169 | final offsetSize = size[0]; 170 | final lengthSize = size[1]; 171 | final baseOffsetSize = size2[0]; 172 | final indexSize = size2[1]; 173 | 174 | if (box.version < 2) { 175 | box.itemCount = ByteData.view(getBytes(2).buffer).getInt16(0); 176 | } else if (box.version == 2) { 177 | box.itemCount = ByteData.view(getBytes(4).buffer).getInt32(0); 178 | } else { 179 | throw Exception("Box version 2, ${box.version}"); 180 | } 181 | box.locs = {}; 182 | for (var i = 0; i < box.itemCount; i += 1) { 183 | int itemId; 184 | if (box.version < 2) { 185 | itemId = ByteData.view(getBytes(2).buffer).getInt16(0); 186 | } else if (box.version == 2) { 187 | itemId = ByteData.view(getBytes(4).buffer).getInt32(0); 188 | } else { 189 | throw Exception("Box version 2, ${box.version}"); 190 | } 191 | 192 | if (box.version == 1 || box.version == 2) { 193 | // ignore construction_method 194 | ByteData.view(getBytes(2).buffer).getInt16(0); 195 | } 196 | // ignore data_reference_index 197 | ByteData.view(getBytes(2).buffer).getInt16(0); 198 | final baseOffset = getInt(baseOffsetSize); 199 | final extentCount = ByteData.view(getBytes(2).buffer).getInt16(0); 200 | final List> extent = []; 201 | for (var i = 0; i < extentCount; i += 1) { 202 | if ((box.version == 1 || box.version == 2) && indexSize > 0) { 203 | getInt(indexSize); 204 | } 205 | final extentOffset = getInt(offsetSize); 206 | final extentLength = getInt(lengthSize); 207 | extent.add([baseOffset + extentOffset, extentLength]); 208 | } 209 | box.locs[itemId] = extent; 210 | } 211 | } 212 | 213 | void Function(HeicBox)? getParser(HeicBox box) { 214 | final defs = { 215 | 'ftyp': _parseFtyp, 216 | 'meta': _parseMeta, 217 | 'infe': _parseInfe, 218 | 'iinf': _parseIinf, 219 | 'iloc': _parseIloc, 220 | }; 221 | return defs[box.name]; 222 | } 223 | 224 | HeicBox parseBox(HeicBox box) { 225 | final probe = getParser(box); 226 | if (probe == null) { 227 | throw Exception('Unhandled box'); 228 | } 229 | probe(box); 230 | // in case anything is left unread 231 | fileReader.setPositionSync(box.after); 232 | return box; 233 | } 234 | 235 | HeicBox expectParse(String name) { 236 | while (true) { 237 | final box = nextBox(); 238 | if (box.name == name) { 239 | return parseBox(box); 240 | } 241 | fileReader.setPositionSync(box.after); 242 | } 243 | } 244 | 245 | List findExif() { 246 | final ftyp = expectParse('ftyp'); 247 | assert(listEqual(ftyp.majorBrand, Uint8List.fromList('heic'.codeUnits)) || 248 | listEqual(ftyp.majorBrand, Uint8List.fromList('avif'.codeUnits))); 249 | assert(ftyp.minorVersion == 0); 250 | final meta = expectParse('meta'); 251 | final itemId = meta.subs['iinf']?.exifInfe?.itemId; 252 | if (itemId == null) { 253 | return []; 254 | } 255 | final extents = meta.subs['iloc']?.locs[itemId]; 256 | // we expect the Exif data to be in one piece. 257 | if (extents == null || extents.length != 1) { 258 | return []; 259 | } 260 | final int pos = extents[0][0]; 261 | // looks like there's a kind of pseudo-box here. 262 | fileReader.setPositionSync(pos); 263 | // the payload of "Exif" item may be start with either 264 | // b'\xFF\xE1\xSS\xSSExif\x00\x00' (with APP1 marker, e.g. Android Q) 265 | // or 266 | // b'Exif\x00\x00' (without APP1 marker, e.g. iOS) 267 | // according to "ISO/IEC 23008-12, 2017-12", both of them are legal 268 | final exifTiffHeaderOffset = ByteData.view(getBytes(4).buffer).getInt32(0); 269 | assert(exifTiffHeaderOffset >= 6); 270 | getBytes(exifTiffHeaderOffset); 271 | // assert self.get(exif_tiff_header_offset)[-6:] == b'Exif\x00\x00' 272 | final offset = fileReader.positionSync(); 273 | final endian = fileReader.readSync(1)[0]; 274 | return [offset, endian]; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /lib/src/makernote_olympus.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/tags_info.dart' show MakerTag, MakerTagFunc, TagsBase; 2 | import 'package:exif/src/util.dart'; 3 | import 'package:sprintf/sprintf.dart' show sprintf; 4 | 5 | // Makernote (proprietary) tag definitions for olympus. 6 | 7 | class MakerNoteOlympus extends TagsBase { 8 | static Map tags = _buildTags(); 9 | 10 | static MakerTag _make(String name) => MakerTag.make(name); 11 | 12 | static MakerTag _withMap(String name, Map map) => 13 | MakerTag.makeWithMap(name, map); 14 | 15 | static MakerTag _withFunc(String name, MakerTagFunc func) => 16 | MakerTag.makeWithFunc(name, func); 17 | 18 | // decode Olympus SpecialMode tag in MakerNote 19 | static String _specialMode(List v) { 20 | final Map mode1 = { 21 | 0: 'Normal', 22 | 1: 'Unknown', 23 | 2: 'Fast', 24 | 3: 'Panorama', 25 | }; 26 | final Map mode2 = { 27 | 0: 'Non-panoramic', 28 | 1: 'Left to right', 29 | 2: 'Right to left', 30 | 3: 'Bottom to top', 31 | 4: 'Top to bottom', 32 | }; 33 | 34 | if (v.isEmpty) { 35 | return ''; 36 | } 37 | 38 | if (v.length < 3 || 39 | (!mode1.containsKey(v[0]) || !mode2.containsKey(v[2]))) { 40 | return v.toString(); 41 | } 42 | 43 | return sprintf('%s - sequence %d - %s', [mode1[v[0]], v[1], mode2[v[2]]]); 44 | } 45 | 46 | static Map _buildTags() { 47 | return { 48 | // ah HAH! those sneeeeeaky bastids! this is how they get past the fact 49 | // that a JPEG thumbnail is not allowed in an uncompressed TIFF file 50 | 0x0100: _make('JPEGThumbnail'), 51 | 0x0200: _withFunc('SpecialMode', _specialMode), 52 | 0x0201: _withMap('JPEGQual', { 53 | 1: 'SQ', 54 | 2: 'HQ', 55 | 3: 'SHQ', 56 | }), 57 | 0x0202: _withMap('Macro', {0: 'Normal', 1: 'Macro', 2: 'SuperMacro'}), 58 | 0x0203: _withMap('BWMode', {0: 'Off', 1: 'On'}), 59 | 0x0204: _make('DigitalZoom'), 60 | 0x0205: _make('FocalPlaneDiagonal'), 61 | 0x0206: _make('LensDistortionParams'), 62 | 0x0207: _make('SoftwareRelease'), 63 | 0x0208: _make('PictureInfo'), 64 | 0x0209: _withFunc('CameraID', makeString), 65 | // print as string 66 | 0x0F00: _make('DataDump'), 67 | 0x0300: _make('PreCaptureFrames'), 68 | 0x0404: _make('SerialNumber'), 69 | 0x1000: _make('ShutterSpeedValue'), 70 | 0x1001: _make('ISOValue'), 71 | 0x1002: _make('ApertureValue'), 72 | 0x1003: _make('BrightnessValue'), 73 | 0x1004: _withMap('FlashMode', {2: 'On', 3: 'Off'}), 74 | 0x1005: _withMap('FlashDevice', 75 | {0: 'None', 1: 'Internal', 4: 'External', 5: 'Internal + External'}), 76 | 0x1006: _make('ExposureCompensation'), 77 | 0x1007: _make('SensorTemperature'), 78 | 0x1008: _make('LensTemperature'), 79 | 0x100b: _withMap('FocusMode', {0: 'Auto', 1: 'Manual'}), 80 | 0x1017: _make('RedBalance'), 81 | 0x1018: _make('BlueBalance'), 82 | 0x101a: _make('SerialNumber'), 83 | 0x1023: _make('FlashExposureComp'), 84 | 0x1026: _withMap('ExternalFlashBounce', {0: 'No', 1: 'Yes'}), 85 | 0x1027: _make('ExternalFlashZoom'), 86 | 0x1028: _make('ExternalFlashMode'), 87 | 0x1029: _withMap('Contrast int16u', {0: 'High', 1: 'Normal', 2: 'Low'}), 88 | 0x102a: _make('SharpnessFactor'), 89 | 0x102b: _make('ColorControl'), 90 | 0x102c: _make('ValidBits'), 91 | 0x102d: _make('CoringFilter'), 92 | 0x102e: _make('OlympusImageWidth'), 93 | 0x102f: _make('OlympusImageHeight'), 94 | 0x1034: _make('CompressionRatio'), 95 | 0x1035: _withMap('PreviewImageValid', {0: 'No', 1: 'Yes'}), 96 | 0x1036: _make('PreviewImageStart'), 97 | 0x1037: _make('PreviewImageLength'), 98 | 0x1039: _withMap('CCDScanMode', {0: 'Interlaced', 1: 'Progressive'}), 99 | 0x103a: _withMap('NoiseReduction', {0: 'Off', 1: 'On'}), 100 | 0x103b: _make('InfinityLensStep'), 101 | 0x103c: _make('NearLensStep'), 102 | 103 | // TODO - these need extra definitions 104 | // http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.90/html/TagNames/Olympus.html 105 | 0x2010: _make('Equipment'), 106 | 0x2020: _make('CameraSettings'), 107 | 0x2030: _make('RawDevelopment'), 108 | 0x2040: _make('ImageProcessing'), 109 | 0x2050: _make('FocusInfo'), 110 | 0x3000: _make('RawInfo '), 111 | }; 112 | } 113 | } 114 | 115 | /* 116 | // 0x2020 CameraSettings 117 | static Map TAG_0x2020 = { 118 | 0x0100: ['PreviewImageValid', { 119 | 0: 'No', 120 | 1: 'Yes' 121 | }], 122 | 0x0101: ['PreviewImageStart', ], 123 | 0x0102: ['PreviewImageLength', ], 124 | 0x0200: ['ExposureMode', { 125 | 1: 'Manual', 126 | 2: 'Program', 127 | 3: 'Aperture-priority AE', 128 | 4: 'Shutter speed priority AE', 129 | 5: 'Program-shift' 130 | }], 131 | 0x0201: ['AELock', { 132 | 0: 'Off', 133 | 1: 'On' 134 | }], 135 | 0x0202: ['MeteringMode', { 136 | 2: 'Center Weighted', 137 | 3: 'Spot', 138 | 5: 'ESP', 139 | 261: 'Pattern+AF', 140 | 515: 'Spot+Highlight control', 141 | 1027: 'Spot+Shadow control' 142 | }], 143 | 0x0300: ['MacroMode', { 144 | 0: 'Off', 145 | 1: 'On' 146 | }], 147 | 0x0301: ['FocusMode', { 148 | 0: 'Single AF', 149 | 1: 'Sequential shooting AF', 150 | 2: 'Continuous AF', 151 | 3: 'Multi AF', 152 | 10: 'MF' 153 | }], 154 | 0x0302: ['FocusProcess', { 155 | 0: 'AF Not Used', 156 | 1: 'AF Used' 157 | }], 158 | 0x0303: ['AFSearch', { 159 | 0: 'Not Ready', 160 | 1: 'Ready' 161 | }], 162 | 0x0304: ['AFAreas', ], 163 | 0x0401: ['FlashExposureCompensation', ], 164 | 0x0500: ['WhiteBalance2', { 165 | 0: 'Auto', 166 | 16: '7500K (Fine Weather with Shade)', 167 | 17: '6000K (Cloudy)', 168 | 18: '5300K (Fine Weather)', 169 | 20: '3000K (Tungsten light)', 170 | 21: '3600K (Tungsten light-like)', 171 | 33: '6600K (Daylight fluorescent)', 172 | 34: '4500K (Neutral white fluorescent)', 173 | 35: '4000K (Cool white fluorescent)', 174 | 48: '3600K (Tungsten light-like)', 175 | 256: 'Custom WB 1', 176 | 257: 'Custom WB 2', 177 | 258: 'Custom WB 3', 178 | 259: 'Custom WB 4', 179 | 512: 'Custom WB 5400K', 180 | 513: 'Custom WB 2900K', 181 | 514: 'Custom WB 8000K', 182 | }], 183 | 0x0501: ['WhiteBalanceTemperature', ], 184 | 0x0502: ['WhiteBalanceBracket', ], 185 | 0x0503: ['CustomSaturation', ], // (3 numbers: 1. CS Value, 2. Min, 3. Max) 186 | 0x0504: ['ModifiedSaturation', { 187 | 0: 'Off', 188 | 1: 'CM1 (Red Enhance)', 189 | 2: 'CM2 (Green Enhance)', 190 | 3: 'CM3 (Blue Enhance)', 191 | 4: 'CM4 (Skin Tones)', 192 | }], 193 | 0x0505: ['ContrastSetting', ], // (3 numbers: 1. Contrast, 2. Min, 3. Max) 194 | 0x0506: ['SharpnessSetting', ], // (3 numbers: 1. Sharpness, 2. Min, 3. Max) 195 | 0x0507: ['ColorSpace', { 196 | 0: 'sRGB', 197 | 1: 'Adobe RGB', 198 | 2: 'Pro Photo RGB' 199 | }], 200 | 0x0509: ['SceneMode', { 201 | 0: 'Standard', 202 | 6: 'Auto', 203 | 7: 'Sport', 204 | 8: 'Portrait', 205 | 9: 'Landscape+Portrait', 206 | 10: 'Landscape', 207 | 11: 'Night scene', 208 | 13: 'Panorama', 209 | 16: 'Landscape+Portrait', 210 | 17: 'Night+Portrait', 211 | 19: 'Fireworks', 212 | 20: 'Sunset', 213 | 22: 'Macro', 214 | 25: 'Documents', 215 | 26: 'Museum', 216 | 28: 'Beach&Snow', 217 | 30: 'Candle', 218 | 35: 'Underwater Wide1', 219 | 36: 'Underwater Macro', 220 | 39: 'High Key', 221 | 40: 'Digital Image Stabilization', 222 | 44: 'Underwater Wide2', 223 | 45: 'Low Key', 224 | 46: 'Children', 225 | 48: 'Nature Macro', 226 | }], 227 | 0x050a: ['NoiseReduction', { 228 | 0: 'Off', 229 | 1: 'Noise Reduction', 230 | 2: 'Noise Filter', 231 | 3: 'Noise Reduction + Noise Filter', 232 | 4: 'Noise Filter (ISO Boost)', 233 | 5: 'Noise Reduction + Noise Filter (ISO Boost)' 234 | }], 235 | 0x050b: ['DistortionCorrection', { 236 | 0: 'Off', 237 | 1: 'On' 238 | }], 239 | 0x050c: ['ShadingCompensation', { 240 | 0: 'Off', 241 | 1: 'On' 242 | }], 243 | 0x050d: ['CompressionFactor', ], 244 | 0x050f: ['Gradation', { 245 | '-1 -1 1': 'Low Key', 246 | '0 -1 1': 'Normal', 247 | '1 -1 1': 'High Key' 248 | }], 249 | 0x0520: ['PictureMode', { 250 | 1: 'Vivid', 251 | 2: 'Natural', 252 | 3: 'Muted', 253 | 256: 'Monotone', 254 | 512: 'Sepia' 255 | }], 256 | 0x0521: ['PictureModeSaturation', ], 257 | 0x0522: ['PictureModeHue?', ], 258 | 0x0523: ['PictureModeContrast', ], 259 | 0x0524: ['PictureModeSharpness', ], 260 | 0x0525: ['PictureModeBWFilter', { 261 | 0: 'n/a', 262 | 1: 'Neutral', 263 | 2: 'Yellow', 264 | 3: 'Orange', 265 | 4: 'Red', 266 | 5: 'Green' 267 | }], 268 | 0x0526: ['PictureModeTone', { 269 | 0: 'n/a', 270 | 1: 'Neutral', 271 | 2: 'Sepia', 272 | 3: 'Blue', 273 | 4: 'Purple', 274 | 5: 'Green' 275 | }], 276 | 0x0600: ['Sequence', ], // 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits 277 | 0x0601: ['PanoramaMode', ], // (2 numbers: 1. Mode, 2. Shot number) 278 | 0x0603: ['ImageQuality2', { 279 | 1: 'SQ', 280 | 2: 'HQ', 281 | 3: 'SHQ', 282 | 4: 'RAW', 283 | }], 284 | 0x0901: ['ManometerReading', ], 285 | }; 286 | 287 | */ 288 | -------------------------------------------------------------------------------- /lib/src/makernote_nikon.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/exif_types.dart'; 2 | import 'package:exif/src/tags_info.dart' show MakerTag, MakerTagFunc, TagsBase; 3 | import 'package:exif/src/util.dart'; 4 | import 'package:sprintf/sprintf.dart' show sprintf; 5 | 6 | // Makernote (proprietary) tag definitions for Nikon. 7 | 8 | class MakerNoteNikon extends TagsBase { 9 | static Map tagsNew = _buildTagsNew(); 10 | static Map tagsOld = _buildTagsOld(); 11 | 12 | static MakerTag _make(String name) => MakerTag.make(name); 13 | 14 | static MakerTag _withMap(String name, Map map) => 15 | MakerTag.makeWithMap(name, map); 16 | 17 | static MakerTag _withFunc(String name, MakerTagFunc func) => 18 | MakerTag.makeWithFunc(name, func); 19 | 20 | // First digit seems to be in steps of 1/6 EV. 21 | // Does the third value mean the step size? It is usually 6, 22 | // but it is 12 for the ExposureDifference. 23 | // Check for an error condition that could cause a crash. 24 | // This only happens if something has gone really wrong in 25 | // reading the Nikon MakerNote. 26 | // http://tomtia.plala.jp/DigitalCamera/MakerNote/index.asp 27 | static String _evBias(List seq) { 28 | if (seq.length < 4) { 29 | return ''; 30 | } 31 | if (listEqual(seq, [252, 1, 6, 0])) { 32 | return '-2/3 EV'; 33 | } 34 | if (listEqual(seq, [253, 1, 6, 0])) { 35 | return '-1/2 EV'; 36 | } 37 | if (listEqual(seq, [254, 1, 6, 0])) { 38 | return '-1/3 EV'; 39 | } 40 | if (listEqual(seq, [0, 1, 6, 0])) { 41 | return '0 EV'; 42 | } 43 | if (listEqual(seq, [2, 1, 6, 0])) { 44 | return '+1/3 EV'; 45 | } 46 | if (listEqual(seq, [3, 1, 6, 0])) { 47 | return '+1/2 EV'; 48 | } 49 | if (listEqual(seq, [4, 1, 6, 0])) { 50 | return '+2/3 EV'; 51 | } 52 | // Handle combinations not in the table. 53 | 54 | int a = seq[0]; 55 | String? retStr; 56 | // Causes headaches for the +/- logic, so special case it. 57 | if (a == 0) { 58 | return '0 EV'; 59 | } 60 | if (a > 127) { 61 | a = 256 - a; 62 | retStr = '-'; 63 | } else { 64 | retStr = '+'; 65 | } 66 | 67 | final step = seq[2]; // Assume third value means the step size 68 | final whole = a ~/ step; 69 | a = a % step; 70 | 71 | if (whole != 0) { 72 | retStr = sprintf('%s%s ', [retStr, whole.toString()]); 73 | } 74 | 75 | if (a == 0) { 76 | retStr += 'EV'; 77 | } else { 78 | final r = Ratio(a, step); 79 | retStr = '$retStr$r EV'; 80 | } 81 | 82 | return retStr; 83 | } 84 | 85 | // Nikon E99x MakerNote Tags 86 | static Map _buildTagsNew() { 87 | return { 88 | 0x0001: _withFunc('MakernoteVersion', makeString), // Sometimes binary 89 | 0x0002: _make('ISOSetting'), 90 | 0x0003: _make('ColorMode'), 91 | 0x0004: _make('Quality'), 92 | 0x0005: _make('Whitebalance'), 93 | 0x0006: _make('ImageSharpening'), 94 | 0x0007: _make('FocusMode'), 95 | 0x0008: _make('FlashSetting'), 96 | 0x0009: _make('AutoFlashMode'), 97 | 0x000B: _make('WhiteBalanceBias'), 98 | 0x000C: _make('WhiteBalanceRBCoeff'), 99 | 0x000D: _withFunc('ProgramShift', _evBias), 100 | // Nearly the same as the other EV vals, but step size is 1/12 EV [] 101 | 0x000E: _withFunc('ExposureDifference', _evBias), 102 | 0x000F: _make('ISOSelection'), 103 | 0x0010: _make('DataDump'), 104 | 0x0011: _make('NikonPreview'), 105 | 0x0012: _withFunc('FlashCompensation', _evBias), 106 | 0x0013: _make('ISOSpeedRequested'), 107 | 0x0016: _make('PhotoCornerCoordinates'), 108 | 0x0017: _withFunc('ExternalFlashExposureComp', _evBias), 109 | 0x0018: _withFunc('FlashBracketCompensationApplied', _evBias), 110 | 0x0019: _make('AEBracketCompensationApplied'), 111 | 0x001A: _make('ImageProcessing'), 112 | 0x001B: _make('CropHiSpeed'), 113 | 0x001C: _make('ExposureTuning'), 114 | 0x001D: _make('SerialNumber'), // Conflict with 0x00A0 ? 115 | 0x001E: _make('ColorSpace'), 116 | 0x001F: _make('VRInfo'), 117 | 0x0020: _make('ImageAuthentication'), 118 | 0x0022: _make('ActiveDLighting'), 119 | 0x0023: _make('PictureControl'), 120 | 0x0024: _make('WorldTime'), 121 | 0x0025: _make('ISOInfo'), 122 | 0x0080: _make('ImageAdjustment'), 123 | 0x0081: _make('ToneCompensation'), 124 | 0x0082: _make('AuxiliaryLens'), 125 | 0x0083: _make('LensType'), 126 | 0x0084: _make('LensMinMaxFocalMaxAperture'), 127 | 0x0085: _make('ManualFocusDistance'), 128 | 0x0086: _make('DigitalZoomFactor'), 129 | 0x0087: _withMap('FlashMode', { 130 | 0x00: 'Did Not Fire', 131 | 0x01: 'Fired, Manual', 132 | 0x07: 'Fired, External', 133 | 0x08: 'Fired, Commander Mode ', 134 | 0x09: 'Fired, TTL Mode', 135 | }), 136 | 0x0088: _withMap('AFFocusPosition', { 137 | 0x0000: 'Center', 138 | 0x0100: 'Top', 139 | 0x0200: 'Bottom', 140 | 0x0300: 'Left', 141 | 0x0400: 'Right', 142 | }), 143 | 0x0089: _withMap('BracketingMode', { 144 | 0x00: 'Single frame, no bracketing', 145 | 0x01: 'Continuous, no bracketing', 146 | 0x02: 'Timer, no bracketing', 147 | 0x10: 'Single frame, exposure bracketing', 148 | 0x11: 'Continuous, exposure bracketing', 149 | 0x12: 'Timer, exposure bracketing', 150 | 0x40: 'Single frame, white balance bracketing', 151 | 0x41: 'Continuous, white balance bracketing', 152 | 0x42: 'Timer, white balance bracketing' 153 | }), 154 | 0x008A: _make('AutoBracketRelease'), 155 | 0x008B: _make('LensFStops'), 156 | 0x008C: _make('NEFCurve1'), // ExifTool calls this 'ContrastCurve' 157 | 0x008D: _make('ColorMode'), 158 | 0x008F: _make('SceneMode'), 159 | 0x0090: _make('LightingType'), 160 | 0x0091: _make('ShotInfo'), // First 4 bytes are a version number in ASCII 161 | 0x0092: _make('HueAdjustment'), 162 | // ExifTool calls this 'NEFCompression', should be 1-4 163 | 0x0093: _make('Compression'), 164 | 0x0094: _withMap('Saturation', { 165 | -3: 'B&W', 166 | -2: '-2', 167 | -1: '-1', 168 | 0: '0', 169 | 1: '1', 170 | 2: '2', 171 | }), 172 | 0x0095: _make('NoiseReduction'), 173 | 0x0096: _make('NEFCurve2'), // ExifTool calls this 'LinearizationTable' 174 | 0x0097: 175 | _make('ColorBalance'), // First 4 bytes are a version number in ASCII 176 | 0x0098: _make('LensData'), // First 4 bytes are a version number in ASCII 177 | 0x0099: _make('RawImageCenter'), 178 | 0x009A: _make('SensorPixelSize'), 179 | 0x009C: _make('Scene Assist'), 180 | 0x009E: _make('RetouchHistory'), 181 | 0x00A0: _make('SerialNumber'), 182 | 0x00A2: _make('ImageDataSize'), 183 | // 00A3: unknown - a single byte 0 184 | // 00A4: In NEF, looks like a 4 byte ASCII version number ('0200') 185 | 0x00A5: _make('ImageCount'), 186 | 0x00A6: _make('DeletedImageCount'), 187 | 0x00A7: _make('TotalShutterReleases'), 188 | // First 4 bytes are a version number in ASCII, with version specific 189 | // info to follow. Its hard to treat it as a string due to embedded nulls. 190 | 0x00A8: _make('FlashInfo'), 191 | 0x00A9: _make('ImageOptimization'), 192 | 0x00AA: _make('Saturation'), 193 | 0x00AB: _make('DigitalVariProgram'), 194 | 0x00AC: _make('ImageStabilization'), 195 | 0x00AD: _make('AFResponse'), 196 | 0x00B0: _make('MultiExposure'), 197 | 0x00B1: _make('HighISONoiseReduction'), 198 | 0x00B6: _make('PowerUpTime'), 199 | 0x00B7: _make('AFInfo2'), 200 | 0x00B8: _make('FileInfo'), 201 | 0x00B9: _make('AFTune'), 202 | 0x0100: _make('DigitalICE'), 203 | 0x0103: _withMap('PreviewCompression', { 204 | 1: 'Uncompressed', 205 | 2: 'CCITT 1D', 206 | 3: 'T4/Group 3 Fax', 207 | 4: 'T6/Group 4 Fax', 208 | 5: 'LZW', 209 | 6: 'JPEG (old-style)', 210 | 7: 'JPEG', 211 | 8: 'Adobe Deflate', 212 | 9: 'JBIG B&W', 213 | 10: 'JBIG Color', 214 | 32766: 'Next', 215 | 32769: 'Epson ERF Compressed', 216 | 32771: 'CCIRLEW', 217 | 32773: 'PackBits', 218 | 32809: 'Thunderscan', 219 | 32895: 'IT8CTPAD', 220 | 32896: 'IT8LW', 221 | 32897: 'IT8MP', 222 | 32898: 'IT8BL', 223 | 32908: 'PixarFilm', 224 | 32909: 'PixarLog', 225 | 32946: 'Deflate', 226 | 32947: 'DCS', 227 | 34661: 'JBIG', 228 | 34676: 'SGILog', 229 | 34677: 'SGILog24', 230 | 34712: 'JPEG 2000', 231 | 34713: 'Nikon NEF Compressed', 232 | 65000: 'Kodak DCR Compressed', 233 | 65535: 'Pentax PEF Compressed', 234 | }), 235 | 0x0201: _make('PreviewImageStart'), 236 | 0x0202: _make('PreviewImageLength'), 237 | 0x0213: _withMap('PreviewYCbCrPositioning', { 238 | 1: 'Centered', 239 | 2: 'Co-sited', 240 | }), 241 | 0x0E09: _make('NikonCaptureVersion'), 242 | 0x0E0E: _make('NikonCaptureOffsets'), 243 | 0x0E10: _make('NikonScan'), 244 | 0x0E22: _make('NEFBitDepth'), 245 | }; 246 | } 247 | 248 | static Map _buildTagsOld() { 249 | return { 250 | 0x0003: _withMap('Quality', { 251 | 1: 'VGA Basic', 252 | 2: 'VGA Normal', 253 | 3: 'VGA Fine', 254 | 4: 'SXGA Basic', 255 | 5: 'SXGA Normal', 256 | 6: 'SXGA Fine', 257 | }), 258 | 0x0004: _withMap('ColorMode', { 259 | 1: 'Color', 260 | 2: 'Monochrome', 261 | }), 262 | 0x0005: _withMap('ImageAdjustment', { 263 | 0: 'Normal', 264 | 1: 'Bright+', 265 | 2: 'Bright-', 266 | 3: 'Contrast+', 267 | 4: 'Contrast-', 268 | }), 269 | 0x0006: _withMap('CCDSpeed', { 270 | 0: 'ISO 80', 271 | 2: 'ISO 160', 272 | 4: 'ISO 320', 273 | 5: 'ISO 100', 274 | }), 275 | 0x0007: _withMap('WhiteBalance', { 276 | 0: 'Auto', 277 | 1: 'Preset', 278 | 2: 'Daylight', 279 | 3: 'Incandescent', 280 | 4: 'Fluorescent', 281 | 5: 'Cloudy', 282 | 6: 'Speed Light', 283 | }), 284 | }; 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /lib/src/exif_decode_makernote.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:exif/src/exifheader.dart'; 4 | import 'package:exif/src/field_types.dart'; 5 | import 'package:exif/src/makernote_apple.dart'; 6 | import 'package:exif/src/makernote_canon.dart'; 7 | import 'package:exif/src/makernote_casio.dart'; 8 | import 'package:exif/src/makernote_fujifilm.dart'; 9 | import 'package:exif/src/makernote_nikon.dart'; 10 | import 'package:exif/src/makernote_olympus.dart'; 11 | import 'package:exif/src/reader.dart'; 12 | import 'package:exif/src/tags_info.dart'; 13 | import 'package:exif/src/util.dart'; 14 | 15 | class DecodeMakerNote { 16 | final Map tags; 17 | final IfdReader file; 18 | 19 | void Function(int ifd, String ifdName, 20 | {Map? tagDict, bool relative}) dumpIfdFunc; 21 | 22 | DecodeMakerNote(this.tags, this.file, this.dumpIfdFunc); 23 | 24 | // deal with MakerNote contained in EXIF IFD 25 | // (Some apps use MakerNote tags but do not use a format for which we 26 | // have a description, do not process these). 27 | void decode() { 28 | final note = tags['EXIF MakerNote']; 29 | if (note == null) { 30 | return; 31 | } 32 | 33 | // Some apps use MakerNote tags but do not use a format for which we 34 | // have a description, so just do a raw dump for these. 35 | final make = tags['Image Make']?.tag.printable ?? ''; 36 | if (make == '') { 37 | return; 38 | } 39 | 40 | _decodeMakerNote(note: note, make: make); 41 | } 42 | 43 | // Decode all the camera-specific MakerNote formats 44 | // Note is the data that comprises this MakerNote. 45 | // The MakerNote will likely have pointers in it that point to other 46 | // parts of the file. We'll use this.offset as the starting point for 47 | // most of those pointers, since they are relative to the beginning 48 | // of the file. 49 | // If the MakerNote is in a newer format, it may use relative addressing 50 | // within the MakerNote. In that case we'll use relative addresses for 51 | // the pointers. 52 | // As an aside: it's not just to be annoying that the manufacturers use 53 | // relative offsets. It's so that if the makernote has to be moved by the 54 | // picture software all of the offsets don't have to be adjusted. Overall, 55 | // this is probably the right strategy for makernotes, though the spec is 56 | // ambiguous. 57 | // The spec does not appear to imagine that makernotes would 58 | // follow EXIF format internally. Once they did, it's ambiguous whether 59 | // the offsets should be from the header at the start of all the EXIF info, 60 | // or from the header at the start of the makernote. 61 | void _decodeMakerNote({required IfdTagImpl note, required String make}) { 62 | if (_decodeNikon(note, make)) { 63 | return; 64 | } 65 | 66 | if (_decodeOlympus(note, make)) { 67 | return; 68 | } 69 | 70 | if (_decodeCasio(note, make)) { 71 | return; 72 | } 73 | 74 | if (_decodeFujifilm(note, make)) { 75 | return; 76 | } 77 | 78 | if (_decodeApple(note, make)) { 79 | return; 80 | } 81 | 82 | if (_decodeCanon(note, make)) { 83 | return; 84 | } 85 | } 86 | 87 | bool _decodeNikon(IfdTagImpl note, String make) { 88 | // Nikon 89 | // The maker note usually starts with the word Nikon, followed by the 90 | // type of the makernote (1 or 2, as a short). If the word Nikon is 91 | // not at the start of the makernote, it's probably type 2, since some 92 | // cameras work that way. 93 | if (!make.contains('NIKON')) { 94 | return false; 95 | } 96 | 97 | if (listHasPrefix( 98 | note.tag.values.toList(), [78, 105, 107, 111, 110, 0, 1])) { 99 | // Looks like a type 1 Nikon MakerNote 100 | _dumpIfd(note.fieldOffset + 8, tagDict: MakerNoteNikon.tagsOld); 101 | } else if (listHasPrefix( 102 | note.tag.values.toList(), [78, 105, 107, 111, 110, 0, 2])) { 103 | // Looks like a labeled type 2 Nikon MakerNote 104 | if (!listHasPrefix(note.tag.values.toList(), [0, 42], start: 12) && 105 | !listHasPrefix(note.tag.values.toList(), [42, 0], start: 12)) { 106 | throw const FormatException("Missing marker tag '42' in MakerNote."); 107 | // skip the Makernote label and the TIFF header 108 | } 109 | _dumpIfd(note.fieldOffset + 10 + 8, 110 | tagDict: MakerNoteNikon.tagsNew, relative: true); 111 | } else { 112 | // E99x or D1 113 | // Looks like an unlabeled type 2 Nikon MakerNote 114 | _dumpIfd(note.fieldOffset, tagDict: MakerNoteNikon.tagsNew); 115 | } 116 | return true; 117 | } 118 | 119 | bool _decodeOlympus(IfdTagImpl note, String make) { 120 | if (make.startsWith('OLYMPUS')) { 121 | _dumpIfd(note.fieldOffset + 8, tagDict: MakerNoteOlympus.tags); 122 | // TODO 123 | //for i in (('MakerNote Tag 0x2020', makernote.OLYMPUS_TAG_0x2020),): 124 | // this.decode_olympus_tag(tags[i[0]].values, i[1]) 125 | //return 126 | return true; 127 | } 128 | return false; 129 | } 130 | 131 | bool _decodeCasio(IfdTagImpl note, String make) { 132 | if (make.contains('CASIO') || make.contains('Casio')) { 133 | _dumpIfd(note.fieldOffset, tagDict: MakerNoteCasio.tags); 134 | return true; 135 | } 136 | return false; 137 | } 138 | 139 | bool _decodeFujifilm(IfdTagImpl note, String make) { 140 | if (make != 'FUJIFILM') { 141 | return false; 142 | } 143 | 144 | // bug: everything else is "Motorola" endian, but the MakerNote 145 | // is "Intel" endian 146 | const endian = Endian.little; 147 | 148 | // bug: IFD offsets are from beginning of MakerNote, not 149 | // beginning of file header 150 | final newBaseOffset = file.baseOffset + note.fieldOffset; 151 | 152 | // process note with bogus values (note is actually at offset 12) 153 | _dumpIfd2(12, 154 | tagDict: MakerNoteFujifilm.tags, 155 | baseOffset: newBaseOffset, 156 | endian: endian); 157 | 158 | return true; 159 | } 160 | 161 | bool _decodeApple(IfdTagImpl note, String make) { 162 | if (!_makerIsApple(note, make)) { 163 | return false; 164 | } 165 | 166 | final newBaseOffset = file.baseOffset + note.fieldOffset + 14; 167 | 168 | _dumpIfd2(0, 169 | tagDict: MakerNoteApple.tags, 170 | baseOffset: newBaseOffset, 171 | endian: file.endian); 172 | 173 | return true; 174 | } 175 | 176 | bool _makerIsApple(IfdTagImpl note, String make) => 177 | make == 'Apple' && 178 | listHasPrefix(note.tag.values.toList(), 179 | [65, 112, 112, 108, 101, 32, 105, 79, 83, 0]); 180 | 181 | bool _decodeCanon(IfdTagImpl note, String make) { 182 | if (make != 'Canon') { 183 | return false; 184 | } 185 | 186 | _dumpIfd(note.fieldOffset, tagDict: MakerNoteCanon.tags); 187 | 188 | MakerNoteCanon.tagsXxx.forEach((name, makerTags) { 189 | final tag = tags[name]; 190 | if (tag != null) { 191 | _canonDecodeTag( 192 | tag.tag.values.toList().whereType().toList(), makerTags); 193 | tags.remove(name); 194 | } 195 | }); 196 | 197 | final cannonTag = tags[MakerNoteCanon.cameraInfoTagName]; 198 | if (cannonTag != null) { 199 | _canonDecodeCameraInfo(cannonTag); 200 | tags.remove(MakerNoteCanon.cameraInfoTagName); 201 | } 202 | 203 | return true; 204 | } 205 | 206 | // TODO Decode Olympus MakerNote tag based on offset within tag 207 | // void _olympus_decode_tag(List value, mn_tags) {} 208 | 209 | // Decode Canon MakerNote tag based on offset within tag. 210 | // See http://www.burren.cx/david/canon.html by David Burren 211 | void _canonDecodeTag(List value, Map mnTags) { 212 | for (int i = 1; i < value.length; i++) { 213 | final tag = mnTags[i] ?? MakerTag.make('Unknown'); 214 | final name = tag.name; 215 | String val; 216 | if (tag.map != null) { 217 | val = tag.map![value[i]] ?? 'Unknown'; 218 | } else { 219 | val = value[i].toString(); 220 | } 221 | 222 | // it's not a real IFD Tag but we fake one to make everybody 223 | // happy. this will have a "proprietary" type 224 | tags['MakerNote $name'] = IfdTagImpl(printable: val); 225 | } 226 | } 227 | 228 | // Decode the variable length encoded camera info section. 229 | void _canonDecodeCameraInfo(IfdTagImpl cameraInfoTag) { 230 | final modelTag = tags['Image Model']; 231 | if (modelTag == null) { 232 | return; 233 | } 234 | 235 | final model = modelTag.tag.values.toString(); 236 | 237 | Map? cameraInfoTags; 238 | for (final modelNameRegExp in MakerNoteCanon.cameraInfoModelMap.keys) { 239 | final tagDesc = MakerNoteCanon.cameraInfoModelMap[modelNameRegExp]; 240 | if (RegExp(modelNameRegExp).hasMatch(model)) { 241 | cameraInfoTags = tagDesc; 242 | break; 243 | } 244 | } 245 | 246 | if (cameraInfoTags == null) { 247 | return; 248 | } 249 | 250 | // We are assuming here that these are all unsigned bytes (Byte or 251 | // Unknown) 252 | if (cameraInfoTag.fieldType != FieldType.byte && 253 | cameraInfoTag.fieldType != FieldType.undefined) { 254 | return; 255 | } 256 | 257 | if (cameraInfoTag.tag.values is! List) { 258 | return; 259 | } 260 | 261 | final cameraInfo = cameraInfoTag.tag.values as List; 262 | 263 | // Look for each data value and decode it appropriately. 264 | for (final entry in cameraInfoTags.entries) { 265 | final offset = entry.key; 266 | final tag = entry.value; 267 | final tagSize = tag.tagSize; 268 | if (cameraInfo.length < offset + tagSize) { 269 | continue; 270 | } 271 | 272 | final packedTagValue = cameraInfo.sublist(offset, offset + tagSize); 273 | final tagValue = s2nLittleEndian(packedTagValue); 274 | 275 | tags['MakerNote ${tag.tagName}'] = 276 | IfdTagImpl(printable: tag.function(tagValue)); 277 | } 278 | } 279 | 280 | void _dumpIfd(int ifd, 281 | {required Map tagDict, bool relative = false}) { 282 | dumpIfdFunc(ifd, 'MakerNote', tagDict: tagDict, relative: relative); 283 | } 284 | 285 | void _dumpIfd2(int ifd, 286 | {required Map? tagDict, 287 | bool relative = false, 288 | required int baseOffset, 289 | required Endian endian}) { 290 | final originalEndian = file.endian; 291 | final originalOffset = file.baseOffset; 292 | 293 | file.endian = endian; 294 | file.baseOffset = baseOffset; 295 | 296 | dumpIfdFunc(ifd, 'MakerNote', tagDict: tagDict, relative: relative); 297 | 298 | file.endian = originalEndian; 299 | file.baseOffset = originalOffset; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /lib/src/tags.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/tags_info.dart' 2 | show MakerTag, MakerTagFunc, TagsBase, MakerTagsWithName; 3 | import 'package:exif/src/util.dart'; 4 | 5 | // Standard tag definitions. 6 | 7 | class StandardTags extends TagsBase { 8 | static MakerTag _make(String name) => MakerTag.make(name); 9 | 10 | static MakerTag _withMap(String name, Map map) => 11 | MakerTag.makeWithMap(name, map); 12 | 13 | static MakerTag _withFunc(String name, MakerTagFunc func) => 14 | MakerTag.makeWithFunc(name, func); 15 | 16 | static MakerTag _withTags(String name, MakerTagsWithName tags) => 17 | MakerTag.makeWithTags(name, tags); 18 | 19 | // Interoperability tags 20 | static final Map _interopTags = { 21 | 0x0001: _make('InteroperabilityIndex'), 22 | 0x0002: _make('InteroperabilityVersion'), 23 | 0x1000: _make('RelatedImageFileFormat'), 24 | 0x1001: _make('RelatedImageWidth'), 25 | 0x1002: _make('RelatedImageLength'), 26 | }; 27 | 28 | static final MakerTagsWithName _interopInfo = 29 | MakerTagsWithName(name: 'Interoperability', tags: _interopTags); 30 | 31 | // GPS tags 32 | static final Map _gpsTags = { 33 | 0x0000: _make('GPSVersionID'), 34 | 0x0001: _make('GPSLatitudeRef'), 35 | 0x0002: _make('GPSLatitude'), 36 | 0x0003: _make('GPSLongitudeRef'), 37 | 0x0004: _make('GPSLongitude'), 38 | 0x0005: _make('GPSAltitudeRef'), 39 | 0x0006: _make('GPSAltitude'), 40 | 0x0007: _make('GPSTimeStamp'), 41 | 0x0008: _make('GPSSatellites'), 42 | 0x0009: _make('GPSStatus'), 43 | 0x000A: _make('GPSMeasureMode'), 44 | 0x000B: _make('GPSDOP'), 45 | 0x000C: _make('GPSSpeedRef'), 46 | 0x000D: _make('GPSSpeed'), 47 | 0x000E: _make('GPSTrackRef'), 48 | 0x000F: _make('GPSTrack'), 49 | 0x0010: _make('GPSImgDirectionRef'), 50 | 0x0011: _make('GPSImgDirection'), 51 | 0x0012: _make('GPSMapDatum'), 52 | 0x0013: _make('GPSDestLatitudeRef'), 53 | 0x0014: _make('GPSDestLatitude'), 54 | 0x0015: _make('GPSDestLongitudeRef'), 55 | 0x0016: _make('GPSDestLongitude'), 56 | 0x0017: _make('GPSDestBearingRef'), 57 | 0x0018: _make('GPSDestBearing'), 58 | 0x0019: _make('GPSDestDistanceRef'), 59 | 0x001A: _make('GPSDestDistance'), 60 | 0x001B: _make('GPSProcessingMethod'), 61 | 0x001C: _make('GPSAreaInformation'), 62 | 0x001D: _make('GPSDate'), 63 | 0x001E: _make('GPSDifferential'), 64 | }; 65 | 66 | static final MakerTagsWithName _gpsInfo = 67 | MakerTagsWithName(name: 'GPS', tags: _gpsTags); 68 | 69 | // Main Exif tag names 70 | static final Map tags = { 71 | 0x00FE: _withMap('SubfileType', { 72 | 0x0: 'Full-resolution Image', 73 | 0x1: 'Reduced-resolution image', 74 | 0x2: 'Single page of multi-page image', 75 | 0x3: 'Single page of multi-page reduced-resolution image', 76 | 0x4: 'Transparency mask', 77 | 0x5: 'Transparency mask of reduced-resolution image', 78 | 0x6: 'Transparency mask of multi-page image', 79 | 0x7: 'Transparency mask of reduced-resolution multi-page image', 80 | 0x10001: 'Alternate reduced-resolution image', 81 | 0xffffffff: 'invalid ', 82 | }), 83 | 0x00FF: _withMap('OldSubfileType', { 84 | 1: 'Full-resolution image', 85 | 2: 'Reduced-resolution image', 86 | 3: 'Single page of multi-page image', 87 | }), 88 | 0x0100: _make('ImageWidth'), 89 | 0x0101: _make('ImageLength'), 90 | 0x0102: _make('BitsPerSample'), 91 | 0x0103: _withMap('Compression', const { 92 | 1: 'Uncompressed', 93 | 2: 'CCITT 1D', 94 | 3: 'T4/Group 3 Fax', 95 | 4: 'T6/Group 4 Fax', 96 | 5: 'LZW', 97 | 6: 'JPEG (old-style)', 98 | 7: 'JPEG', 99 | 8: 'Adobe Deflate', 100 | 9: 'JBIG B&W', 101 | 10: 'JBIG Color', 102 | 32766: 'Next', 103 | 32769: 'Epson ERF Compressed', 104 | 32771: 'CCIRLEW', 105 | 32773: 'PackBits', 106 | 32809: 'Thunderscan', 107 | 32895: 'IT8CTPAD', 108 | 32896: 'IT8LW', 109 | 32897: 'IT8MP', 110 | 32898: 'IT8BL', 111 | 32908: 'PixarFilm', 112 | 32909: 'PixarLog', 113 | 32946: 'Deflate', 114 | 32947: 'DCS', 115 | 34661: 'JBIG', 116 | 34676: 'SGILog', 117 | 34677: 'SGILog24', 118 | 34712: 'JPEG 2000', 119 | 34713: 'Nikon NEF Compressed', 120 | 65000: 'Kodak DCR Compressed', 121 | 65535: 'Pentax PEF Compressed' 122 | }), 123 | 0x0106: _make('PhotometricInterpretation'), 124 | 0x0107: _make('Thresholding'), 125 | 0x0108: _make('CellWidth'), 126 | 0x0109: _make('CellLength'), 127 | 0x010A: _make('FillOrder'), 128 | 0x010D: _make('DocumentName'), 129 | 0x010E: _make('ImageDescription'), 130 | 0x010F: _make('Make'), 131 | 0x0110: _make('Model'), 132 | 0x0111: _make('StripOffsets'), 133 | 0x0112: _withMap('Orientation', const { 134 | 1: 'Horizontal (normal)', 135 | 2: 'Mirrored horizontal', 136 | 3: 'Rotated 180', 137 | 4: 'Mirrored vertical', 138 | 5: 'Mirrored horizontal then rotated 90 CCW', 139 | 6: 'Rotated 90 CW', 140 | 7: 'Mirrored horizontal then rotated 90 CW', 141 | 8: 'Rotated 90 CCW' 142 | }), 143 | 0x0115: _make('SamplesPerPixel'), 144 | 0x0116: _make('RowsPerStrip'), 145 | 0x0117: _make('StripByteCounts'), 146 | 0x0118: _make('MinSampleValue'), 147 | 0x0119: _make('MaxSampleValue'), 148 | 0x011A: _make('XResolution'), 149 | 0x011B: _make('YResolution'), 150 | 0x011C: _make('PlanarConfiguration'), 151 | 0x011D: _withFunc('PageName', makeString), 152 | 0x011E: _make('XPosition'), 153 | 0x011F: _make('YPosition'), 154 | 0x0122: _withMap('GrayResponseUnit', const { 155 | 1: '0.1', 156 | 2: '0.001', 157 | 3: '0.0001', 158 | 4: '1e-05', 159 | 5: '1e-06', 160 | }), 161 | 0x0123: _make('GrayResponseCurve'), 162 | 0x0124: _make('T4Options'), 163 | 0x0125: _make('T6Options'), 164 | 0x0128: _withMap('ResolutionUnit', 165 | const {1: 'Not Absolute', 2: 'Pixels/Inch', 3: 'Pixels/Centimeter'}), 166 | 0x0129: _make('PageNumber'), 167 | 0x012C: _make('ColorResponseUnit'), 168 | 0x012D: _make('TransferFunction'), 169 | 0x0131: _make('Software'), 170 | 0x0132: _make('DateTime'), 171 | 0x013B: _make('Artist'), 172 | 0x013C: _make('HostComputer'), 173 | 0x013D: 174 | _withMap('Predictor', const {1: 'None', 2: 'Horizontal differencing'}), 175 | 0x013E: _make('WhitePoint'), 176 | 0x013F: _make('PrimaryChromaticities'), 177 | 0x0140: _make('ColorMap'), 178 | 0x0141: _make('HalftoneHints'), 179 | 0x0142: _make('TileWidth'), 180 | 0x0143: _make('TileLength'), 181 | 0x0144: _make('TileOffsets'), 182 | 0x0145: _make('TileByteCounts'), 183 | 0x0146: _make('BadFaxLines'), 184 | 0x0147: _withMap( 185 | 'CleanFaxData', const {0: 'Clean', 1: 'Regenerated', 2: 'Unclean'}), 186 | 0x0148: _make('ConsecutiveBadFaxLines'), 187 | 0x014C: _withMap('InkSet', const {1: 'CMYK', 2: 'Not CMYK'}), 188 | 0x014D: _make('InkNames'), 189 | 0x014E: _make('NumberofInks'), 190 | 0x0150: _make('DotRange'), 191 | 0x0151: _make('TargetPrinter'), 192 | 0x0152: _withMap('ExtraSamples', const { 193 | 0: 'Unspecified', 194 | 1: 'Associated Alpha', 195 | 2: 'Unassociated Alpha' 196 | }), 197 | 0x0153: _withMap('SampleFormat', const { 198 | 1: 'Unsigned', 199 | 2: 'Signed', 200 | 3: 'Float', 201 | 4: 'Undefined', 202 | 5: 'Complex int', 203 | 6: 'Complex float' 204 | }), 205 | 0x0154: _make('SMinSampleValue'), 206 | 0x0155: _make('SMaxSampleValue'), 207 | 0x0156: _make('TransferRange'), 208 | 0x0157: _make('ClipPath'), 209 | 0x0200: _make('JPEGProc'), 210 | 0x0201: _make('JPEGInterchangeFormat'), 211 | 0x0202: _make('JPEGInterchangeFormatLength'), 212 | 0x0211: _make('YCbCrCoefficients'), 213 | 0x0212: _make('YCbCrSubSampling'), 214 | 0x0213: _withMap('YCbCrPositioning', const {1: 'Centered', 2: 'Co-sited'}), 215 | 0x0214: _make('ReferenceBlackWhite'), 216 | 0x02BC: _make('ApplicationNotes'), // XPM Info 217 | 0x4746: _make('Rating'), 218 | 0x828D: _make('CFARepeatPatternDim'), 219 | 0x828E: _make('CFAPattern'), 220 | 0x828F: _make('BatteryLevel'), 221 | 0x8298: _make('Copyright'), 222 | 0x829A: _make('ExposureTime'), 223 | 0x829D: _make('FNumber'), 224 | 0x83BB: _make('IPTC/NAA'), 225 | 0x8769: _make('ExifOffset'), // Exif Tags 226 | 0x8773: _make('InterColorProfile'), 227 | 0x8822: _withMap('ExposureProgram', const { 228 | 0: 'Unidentified', 229 | 1: 'Manual', 230 | 2: 'Program Normal', 231 | 3: 'Aperture Priority', 232 | 4: 'Shutter Priority', 233 | 5: 'Program Creative', 234 | 6: 'Program Action', 235 | 7: 'Portrait Mode', 236 | 8: 'Landscape Mode' 237 | }), 238 | 0x8824: _make('SpectralSensitivity'), 239 | 0x8825: _withTags('GPSInfo', _gpsInfo), // GPS tags 240 | 0x8827: _make('ISOSpeedRatings'), 241 | 0x8828: _make('OECF'), 242 | 0x8830: _withMap('SensitivityType', const { 243 | 0: 'Unknown', 244 | 1: 'Standard Output Sensitivity', 245 | 2: 'Recommended Exposure Index', 246 | 3: 'ISO Speed', 247 | 4: 'Standard Output Sensitivity and Recommended Exposure Index', 248 | 5: 'Standard Output Sensitivity and ISO Speed', 249 | 6: 'Recommended Exposure Index and ISO Speed', 250 | 7: 'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed' 251 | }), 252 | 0x8832: _make('RecommendedExposureIndex'), 253 | 0x8833: _make('ISOSpeed'), 254 | 0x9000: _withFunc('ExifVersion', makeString), 255 | 0x9003: _make('DateTimeOriginal'), 256 | 0x9004: _make('DateTimeDigitized'), 257 | 0x9010: _make('OffsetTime'), 258 | 0x9011: _make('OffsetTimeOriginal'), 259 | 0x9012: _make('OffsetTimeDigitized'), 260 | 0x9101: _withMap('ComponentsConfiguration', const { 261 | 0: '', 262 | 1: 'Y', 263 | 2: 'Cb', 264 | 3: 'Cr', 265 | 4: 'Red', 266 | 5: 'Green', 267 | 6: 'Blue' 268 | }), 269 | 0x9102: _make('CompressedBitsPerPixel'), 270 | 0x9201: _make('ShutterSpeedValue'), 271 | 0x9202: _make('ApertureValue'), 272 | 0x9203: _make('BrightnessValue'), 273 | 0x9204: _make('ExposureBiasValue'), 274 | 0x9205: _make('MaxApertureValue'), 275 | 0x9206: _make('SubjectDistance'), 276 | 0x9207: _withMap('MeteringMode', const { 277 | 0: 'Unidentified', 278 | 1: 'Average', 279 | 2: 'CenterWeightedAverage', 280 | 3: 'Spot', 281 | 4: 'MultiSpot', 282 | 5: 'Pattern', 283 | 6: 'Partial', 284 | 255: 'other' 285 | }), 286 | 0x9208: _withMap('LightSource', const { 287 | 0: 'Unknown', 288 | 1: 'Daylight', 289 | 2: 'Fluorescent', 290 | 3: 'Tungsten (incandescent light)', 291 | 4: 'Flash', 292 | 9: 'Fine weather', 293 | 10: 'Cloudy weather', 294 | 11: 'Shade', 295 | 12: 'Daylight fluorescent (D 5700 - 7100K)', 296 | 13: 'Day white fluorescent (N 4600 - 5400K)', 297 | 14: 'Cool white fluorescent (W 3900 - 4500K)', 298 | 15: 'White fluorescent (WW 3200 - 3700K)', 299 | 17: 'Standard light A', 300 | 18: 'Standard light B', 301 | 19: 'Standard light C', 302 | 20: 'D55', 303 | 21: 'D65', 304 | 22: 'D75', 305 | 23: 'D50', 306 | 24: 'ISO studio tungsten', 307 | 255: 'other light source' 308 | }), 309 | 0x9209: _withMap('Flash', const { 310 | 0: 'Flash did not fire', 311 | 1: 'Flash fired', 312 | 5: 'Strobe return light not detected', 313 | 7: 'Strobe return light detected', 314 | 9: 'Flash fired, compulsory flash mode', 315 | 13: 'Flash fired, compulsory flash mode, return light not detected', 316 | 15: 'Flash fired, compulsory flash mode, return light detected', 317 | 16: 'Flash did not fire, compulsory flash mode', 318 | 24: 'Flash did not fire, auto mode', 319 | 25: 'Flash fired, auto mode', 320 | 29: 'Flash fired, auto mode, return light not detected', 321 | 31: 'Flash fired, auto mode, return light detected', 322 | 32: 'No flash function', 323 | 65: 'Flash fired, red-eye reduction mode', 324 | 69: 'Flash fired, red-eye reduction mode, return light not detected', 325 | 71: 'Flash fired, red-eye reduction mode, return light detected', 326 | 73: 'Flash fired, compulsory flash mode, red-eye reduction mode', 327 | 77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', 328 | 79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected', 329 | 89: 'Flash fired, auto mode, red-eye reduction mode', 330 | 93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode', 331 | 95: 'Flash fired, auto mode, return light detected, red-eye reduction mode' 332 | }), 333 | 0x920A: _make('FocalLength'), 334 | 0x9214: _make('SubjectArea'), 335 | 0x927C: _make('MakerNote'), 336 | 0x9286: _withFunc('UserComment', makeStringUc), 337 | 0x9290: _make('SubSecTime'), 338 | 0x9291: _make('SubSecTimeOriginal'), 339 | 0x9292: _make('SubSecTimeDigitized'), 340 | 341 | // used by Windows Explorer 342 | 0x9C9B: _make('XPTitle'), 343 | 0x9C9C: _make('XPComment'), 344 | 0x9C9D: _withFunc('XPAuthor', 345 | makeString), // const [gnored by Windows Explorer if Artist exists] 346 | 0x9C9E: _make('XPKeywords'), 347 | 0x9C9F: _make('XPSubject'), 348 | 0xA000: _withFunc('FlashPixVersion', makeString), 349 | 0xA001: _withMap( 350 | 'ColorSpace', const {1: 'sRGB', 2: 'Adobe RGB', 65535: 'Uncalibrated'}), 351 | 0xA002: _make('ExifImageWidth'), 352 | 0xA003: _make('ExifImageLength'), 353 | 0xA004: _make('RelatedSoundFile'), 354 | 0xA005: _withTags('InteroperabilityOffset', _interopInfo), 355 | 0xA20B: _make('FlashEnergy'), // 0x920B in TIFF/EP 356 | 0xA20C: _make('SpatialFrequencyResponse'), // 0x920C 357 | 0xA20E: _make('FocalPlaneXResolution'), // 0x920E 358 | 0xA20F: _make('FocalPlaneYResolution'), // 0x920F 359 | 0xA210: _make('FocalPlaneResolutionUnit'), // 0x9210 360 | 0xA214: _make('SubjectLocation'), // 0x9214 361 | 0xA215: _make('ExposureIndex'), // 0x9215 362 | 0xA217: _withMap('SensingMethod', const { 363 | // 0x9217 364 | 1: 'Not defined', 365 | 2: 'One-chip color area', 366 | 3: 'Two-chip color area', 367 | 4: 'Three-chip color area', 368 | 5: 'Color sequential area', 369 | 7: 'Trilinear', 370 | 8: 'Color sequential linear' 371 | }), 372 | 0xA300: _withMap('FileSource', const { 373 | 1: 'Film Scanner', 374 | 2: 'Reflection Print Scanner', 375 | 3: 'Digital Camera' 376 | }), 377 | 0xA301: _withMap('SceneType', const {1: 'Directly Photographed'}), 378 | 0xA302: _make('CVAPattern'), 379 | 0xA401: _withMap('CustomRendered', const {0: 'Normal', 1: 'Custom'}), 380 | 0xA402: _withMap('ExposureMode', 381 | const {0: 'Auto Exposure', 1: 'Manual Exposure', 2: 'Auto Bracket'}), 382 | 0xA403: _withMap('WhiteBalance', const {0: 'Auto', 1: 'Manual'}), 383 | 0xA404: _make('DigitalZoomRatio'), 384 | 0xA405: _make('FocalLengthIn35mmFilm'), 385 | 0xA406: _withMap('SceneCaptureType', 386 | const {0: 'Standard', 1: 'Landscape', 2: 'Portrait', 3: 'Night]'}), 387 | 0xA407: _withMap('GainControl', const { 388 | 0: 'None', 389 | 1: 'Low gain up', 390 | 2: 'High gain up', 391 | 3: 'Low gain down', 392 | 4: 'High gain down' 393 | }), 394 | 0xA408: _withMap('Contrast', const {0: 'Normal', 1: 'Soft', 2: 'Hard'}), 395 | 0xA409: _withMap('Saturation', const {0: 'Normal', 1: 'Soft', 2: 'Hard'}), 396 | 0xA40A: _withMap('Sharpness', const {0: 'Normal', 1: 'Soft', 2: 'Hard'}), 397 | 0xA40B: _make('DeviceSettingDescription'), 398 | 0xA40C: _make('SubjectDistanceRange'), 399 | 0xA420: _make('ImageUniqueID'), 400 | 0xA430: _make('CameraOwnerName'), 401 | 0xA431: _make('BodySerialNumber'), 402 | 0xA432: _make('LensSpecification'), 403 | 0xA433: _make('LensMake'), 404 | 0xA434: _make('LensModel'), 405 | 0xA435: _make('LensSerialNumber'), 406 | 0xA500: _make('Gamma'), 407 | 0xC4A5: _make('PrintIM'), 408 | 0xEA1C: _make('Padding'), 409 | 0xEA1D: _make('OffsetSchema'), 410 | 0xFDE8: _make('OwnerName'), 411 | 0xFDE9: _make('SerialNumber'), 412 | }; 413 | } 414 | -------------------------------------------------------------------------------- /lib/src/read_exif.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:exif/src/exif_decode_makernote.dart'; 6 | import 'package:exif/src/exif_types.dart'; 7 | import 'package:exif/src/exifheader.dart'; 8 | import 'package:exif/src/file_interface.dart'; 9 | import 'package:exif/src/heic.dart'; 10 | import 'package:exif/src/linereader.dart'; 11 | import 'package:exif/src/reader.dart'; 12 | import 'package:exif/src/util.dart'; 13 | 14 | int _incrementBase(List data, int base) { 15 | return (data[base + 2]) * 256 + (data[base + 3]) + 2; 16 | } 17 | 18 | /// Process an image file data. 19 | /// This is the function that has to deal with all the arbitrary nasty bits 20 | /// of the EXIF standard. 21 | Future> readExifFromBytes(List bytes, 22 | {String? stopTag, 23 | bool details = true, 24 | bool strict = false, 25 | bool debug = false, 26 | bool truncateTags = true}) async { 27 | return readExifFromFileReader(FileReader.fromBytes(bytes), 28 | stopTag: stopTag, 29 | details: details, 30 | strict: strict, 31 | debug: debug, 32 | truncateTags: truncateTags) 33 | .tags; 34 | } 35 | 36 | /// Streaming version of [readExifFromBytes]. 37 | Future> readExifFromFile(File file, 38 | {String? stopTag, 39 | bool details = true, 40 | bool strict = false, 41 | bool debug = false, 42 | bool truncateTags = true}) async { 43 | final randomAccessFile = file.openSync(); 44 | final fileReader = await FileReader.fromFile(randomAccessFile); 45 | final r = readExifFromFileReader(fileReader, 46 | stopTag: stopTag, 47 | details: details, 48 | strict: strict, 49 | debug: debug, 50 | truncateTags: truncateTags); 51 | randomAccessFile.closeSync(); 52 | return r.tags; 53 | } 54 | 55 | /// Process an image file (expects an open file object). 56 | /// This is the function that has to deal with all the arbitrary nasty bits 57 | /// of the EXIF standard. 58 | ExifData readExifFromFileReader(FileReader f, 59 | {String? stopTag, 60 | bool details = true, 61 | bool strict = false, 62 | bool debug = false, 63 | bool truncateTags = true}) { 64 | ReadParams readParams; 65 | 66 | // determine whether it's a JPEG or TIFF 67 | final header = f.readSync(12); 68 | if (_isTiff(header)) { 69 | readParams = _tiffReadParams(f); 70 | } else if (_isHeic(header) || _isAvif(header)) { 71 | readParams = _heicReadParams(f); 72 | } else if (_isJpeg(header)) { 73 | readParams = _jpegReadParams(f); 74 | } else if (_isPng(header)) { 75 | readParams = _pngReadParams(f); 76 | } else if (_isWebp(header)) { 77 | readParams = _webpReadParams(f); 78 | } else { 79 | return ExifData.withWarning("File format not recognized."); 80 | } 81 | 82 | if (readParams.error != "") { 83 | return ExifData.withWarning(readParams.error); 84 | } 85 | 86 | final file = IfdReader(Reader(f, readParams.offset, readParams.endian), 87 | fakeExif: readParams.fakeExif); 88 | 89 | final hdr = ExifHeader( 90 | file: file, 91 | strict: strict, 92 | debug: debug, 93 | detailed: details, 94 | truncateTags: truncateTags); 95 | 96 | final ifdList = file.listIfd(); 97 | 98 | ifdList.asMap().forEach((ifdIndex, ifd) { 99 | hdr.dumpIfd(ifd, _ifdNameOfIndex(ifdIndex), stopTag: stopTag); 100 | }); 101 | 102 | // EXIF IFD 103 | final exifOff = hdr.tags['Image ExifOffset']; 104 | if (exifOff != null && exifOff.tag.values is IfdInts) { 105 | hdr.dumpIfd(exifOff.tag.values.firstAsInt(), 'EXIF', stopTag: stopTag); 106 | } 107 | 108 | if (details) { 109 | DecodeMakerNote(hdr.tags, hdr.file, hdr.dumpIfd).decode(); 110 | } 111 | 112 | if (details && ifdList.length >= 2) { 113 | hdr.extractTiffThumbnail(ifdList[1]); 114 | hdr.extractJpegThumbnail(); 115 | } 116 | 117 | // parse XMP tags (experimental) 118 | if (debug && details) { 119 | _parseXmpTags(f, hdr); 120 | } 121 | 122 | return ExifData( 123 | hdr.tags.map((key, value) => MapEntry(key, value.tag)), hdr.warnings); 124 | } 125 | 126 | String _ifdNameOfIndex(int index) { 127 | if (index == 0) { 128 | return 'Image'; 129 | } else if (index == 1) { 130 | return 'Thumbnail'; 131 | } else { 132 | return 'IFD $index'; 133 | } 134 | } 135 | 136 | void _parseXmpTags(FileReader f, ExifHeader hdr) { 137 | String xmpString = ''; 138 | // Easy we already have them 139 | final imageApplicationNotes = hdr.tags['Image ApplicationNotes']; 140 | if (imageApplicationNotes != null) { 141 | // XMP present in Exif 142 | final tag = imageApplicationNotes.tag; 143 | xmpString = 144 | (tag is IfdInts) ? makeString((tag as IfdInts).ints) : tag.toString(); 145 | // We need to look in the entire file for the XML 146 | } else { 147 | // XMP not in Exif, searching file for XMP info... 148 | bool xmlStarted = false; 149 | bool xmlFinished = false; 150 | final reader = LineReader(f); 151 | while (true) { 152 | String line = reader.readLine(); 153 | if (line.isEmpty) break; 154 | 155 | final openTag = line.indexOf(''); 157 | 158 | if (openTag != -1) { 159 | xmlStarted = true; 160 | line = line.substring(openTag); 161 | // printf('** XMP found opening tag at line position %s', [open_tag]); 162 | } 163 | 164 | if (closeTag != -1) { 165 | // printf('** XMP found closing tag at line position %s', [close_tag]); 166 | int lineOffset = 0; 167 | if (openTag != -1) { 168 | lineOffset = openTag; 169 | } 170 | line = line.substring(0, (closeTag - lineOffset) + 12); 171 | xmlFinished = true; 172 | } 173 | 174 | if (xmlStarted) { 175 | xmpString += line; 176 | } 177 | 178 | if (xmlFinished) { 179 | break; 180 | } 181 | } 182 | 183 | // print('** XMP Finished searching for info'); 184 | if (xmpString.isNotEmpty) { 185 | hdr.parseXmp(xmpString); 186 | } 187 | } 188 | } 189 | 190 | bool _isTiff(List header) => 191 | header.length >= 4 && 192 | listContainedIn( 193 | header.sublist(0, 4), ['II*\x00'.codeUnits, 'MM\x00*'.codeUnits]); 194 | 195 | bool _isHeic(List header) => 196 | listRangeEqual(header, 4, 12, 'ftypheic'.codeUnits); 197 | 198 | bool _isAvif(List header) => 199 | listRangeEqual(header, 4, 12, 'ftypavif'.codeUnits); 200 | 201 | bool _isJpeg(List header) => 202 | listRangeEqual(header, 0, 2, '\xFF\xD8'.codeUnits); 203 | 204 | bool _isPng(List header) => 205 | listRangeEqual(header, 0, 8, '\x89PNG\r\n\x1a\n'.codeUnits); 206 | 207 | bool _isWebp(List header) => 208 | listRangeEqual(header, 0, 4, 'RIFF'.codeUnits) && 209 | listRangeEqual(header, 8, 12, 'WEBP'.codeUnits); 210 | 211 | ReadParams _heicReadParams(FileReader f) { 212 | f.setPositionSync(0); 213 | final heic = HEICExifFinder(f); 214 | final res = heic.findExif(); 215 | if (res.length != 2) { 216 | return ReadParams.error("Possibly corrupted heic data"); 217 | } 218 | final int offset = res[0]; 219 | final Endian endian = Reader.endianOfByte(res[1]); 220 | return ReadParams(endian: endian, offset: offset); 221 | } 222 | 223 | ReadParams _jpegReadParams(FileReader f) { 224 | // by default do not fake an EXIF beginning 225 | var fakeExif = false; 226 | int offset; 227 | Endian endian; 228 | 229 | f.setPositionSync(0); 230 | 231 | const headerLength = 12; 232 | var data = f.readSync(headerLength); 233 | if (data.length != headerLength) { 234 | return ReadParams.error("File format not recognized."); 235 | } 236 | 237 | var base = 2; 238 | while (data[2] == 0xFF && 239 | listContainedIn(data.sublist(6, 10), [ 240 | 'JFIF'.codeUnits, 241 | 'JFXX'.codeUnits, 242 | 'OLYM'.codeUnits, 243 | 'Phot'.codeUnits 244 | ])) { 245 | final length = data[4] * 256 + data[5]; 246 | // printf("** Length offset is %d", [length]); 247 | f.readSync(length - 8); 248 | // fake an EXIF beginning of file 249 | // I don't think this is used. --gd 250 | data = [0xFF, 0x00]; 251 | data.addAll(f.readSync(10)); 252 | fakeExif = true; 253 | if (base > 2) { 254 | // print("** Added to base"); 255 | base = base + length + 4 - 2; 256 | } else { 257 | // print("** Added to zero"); 258 | base = length + 4; 259 | } 260 | // printf("** Set segment base to 0x%X", [base]); 261 | } 262 | 263 | // Big ugly patch to deal with APP2 (or other) data coming before APP1 264 | f.setPositionSync(0); 265 | // in theory, this could be insufficient since 64K is the maximum size--gd 266 | // print('** f.position=${f.positionSync()}, base=$base'); 267 | data = f.readSync(base + 4000); 268 | // print('** data.length=${data.length}'); 269 | 270 | // base = 2 271 | while (true) { 272 | // print('** base=$base'); 273 | 274 | // if (data.length == 4020) { 275 | // print("** data.length=${data.length}, base=$base"); 276 | // } 277 | if (listRangeEqual(data, base, base + 2, [0xFF, 0xE1])) { 278 | // APP1 279 | // print("** APP1 at base $base"); 280 | // print("** Length: (${data[base + 2]}, ${data[base + 3]})"); 281 | // print("** Code: ${new String.fromCharCodes(data.sublist(base + 4,base + 8))}"); 282 | if (listRangeEqual(data, base + 4, base + 8, "Exif".codeUnits)) { 283 | // print("** Decrement base by 2 to get to pre-segment header (for compatibility with later code)"); 284 | base -= 2; 285 | break; 286 | } 287 | base += _incrementBase(data, base); 288 | } else if (listRangeEqual(data, base, base + 2, [0xFF, 0xE0])) { 289 | // APP0 290 | // print("** APP0 at base $base"); 291 | // printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]); 292 | // printf("** Code: %s", [data.sublist(base + 4, base + 8)]); 293 | base += _incrementBase(data, base); 294 | } else if (listRangeEqual(data, base, base + 2, [0xFF, 0xE2])) { 295 | // APP2 296 | // printf("** APP2 at base 0x%X", [base]); 297 | // printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]); 298 | // printf("** Code: %s", [data.sublist(base + 4,base + 8)]); 299 | base += _incrementBase(data, base); 300 | } else if (listRangeEqual(data, base, base + 2, [0xFF, 0xEE])) { 301 | // APP14 302 | // printf("** APP14 Adobe segment at base 0x%X", [base]); 303 | // printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]); 304 | // printf("** Code: %s", [data.sublist(base + 4,base + 8)]); 305 | base += _incrementBase(data, base); 306 | // print("** There is useful EXIF-like data here, but we have no parser for it."); 307 | } else if (listRangeEqual(data, base, base + 2, [0xFF, 0xDB])) { 308 | // printf("** JPEG image data at base 0x%X No more segments are expected.", [base]); 309 | break; 310 | } else if (listRangeEqual(data, base, base + 2, [0xFF, 0xD8])) { 311 | // APP12 312 | // printf("** FFD8 segment at base 0x%X", [base]); 313 | // printf("** Got 0x%X 0x%X and %s instead", [data[base], data[base + 1], data.sublist(4 + base,10 + base)]); 314 | // printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]); 315 | // printf("** Code: %s", [data.sublist(base + 4,base + 8)]); 316 | base += _incrementBase(data, base); 317 | } else if (listRangeEqual(data, base, base + 2, [0xFF, 0xEC])) { 318 | // APP12 319 | // printf("** APP12 XMP (Ducky) or Pictureinfo segment at base 0x%X", [base]); 320 | // printf("** Got 0x%X and 0x%X instead", [data[base], data[base + 1]]); 321 | // printf("** Length: 0x%X 0x%X", [data[base + 2], data[base + 3]]); 322 | // printf("** Code: %s", [data.sublist(base + 4,base + 8)]); 323 | base += _incrementBase(data, base); 324 | // print("** There is useful EXIF-like data here (quality, comment, copyright), but we have no parser for it."); 325 | } else { 326 | try { 327 | base += _incrementBase(data, base); 328 | } on RangeError { 329 | return ReadParams.error( 330 | "Unexpected/unhandled segment type or file content."); 331 | } 332 | } 333 | } 334 | 335 | f.setPositionSync(base + 12); 336 | if (data[2 + base] == 0xFF && 337 | listRangeEqual(data, 6 + base, 10 + base, 'Exif'.codeUnits)) { 338 | // detected EXIF header 339 | offset = f.positionSync(); 340 | endian = Reader.endianOfByte(f.readByteSync()); 341 | //HACK TEST: endian = 'M' 342 | } else if (data[2 + base] == 0xFF && 343 | listRangeEqual(data, 6 + base, 10 + base + 1, 'Ducky'.codeUnits)) { 344 | // detected Ducky header. 345 | // printf("** EXIF-like header (normally 0xFF and code): 0x%X and %s", 346 | // [data[2 + base], data.sublist(6 + base,10 + base + 1)]); 347 | offset = f.positionSync(); 348 | endian = Reader.endianOfByte(f.readByteSync()); 349 | } else if (data[2 + base] == 0xFF && 350 | listRangeEqual(data, 6 + base, 10 + base + 1, 'Adobe'.codeUnits)) { 351 | // detected APP14 (Adobe); 352 | // printf("** EXIF-like header (normally 0xFF and code): 0x%X and %s", 353 | // [data[2 + base], data.sublist(6 + base,10 + base + 1)]); 354 | offset = f.positionSync(); 355 | endian = Reader.endianOfByte(f.readByteSync()); 356 | } else { 357 | // print("** No EXIF header expected data[2+base]==0xFF and data[6+base:10+base]===Exif (or Duck)"); 358 | // printf("** Did get 0x%X and %s", 359 | // [data[2 + base], data.sublist(6 + base,10 + base + 1)]); 360 | return ReadParams.error("No EXIF information found"); 361 | } 362 | 363 | return ReadParams(endian: endian, offset: offset, fakeExif: fakeExif); 364 | } 365 | 366 | ReadParams _pngReadParams(FileReader f) { 367 | f.setPositionSync(8); 368 | while (true) { 369 | final data = f.readSync(8); 370 | final chunk = String.fromCharCodes(data.sublist(4, 8)); 371 | 372 | if (chunk.isEmpty || chunk == "IEND") { 373 | break; 374 | } 375 | if (chunk == "eXIf") { 376 | final offset = f.positionSync(); 377 | final endian = Reader.endianOfByte(f.readByteSync()); 378 | return ReadParams(endian: endian, offset: offset); 379 | } 380 | 381 | final chunkSize = 382 | Int8List.fromList(data.sublist(0, 4)).buffer.asByteData().getInt32(0); 383 | f.setPositionSync(f.positionSync() + chunkSize + 4); 384 | } 385 | 386 | return ReadParams.error("No EXIF information found"); 387 | } 388 | 389 | ReadParams _webpReadParams(FileReader f) { 390 | // Each RIFF box is a 4-byte ASCII tag, followed by a little-endian uint32 391 | // length, and finally that number of bytes of data. The file starts with an 392 | // outer box with the tag 'RIFF', whose content is the file format ('WEBP') 393 | // followed by a series of inner boxes. We need the inner 'EXIF' box. 394 | // 395 | // The outer box encapsulates the entire file, so we can safely skip forward 396 | // to the first inner box. 397 | f.setPositionSync(12); 398 | while (true) { 399 | final header = f.readSync(8); 400 | if (header.isEmpty) { 401 | return ReadParams.error("No EXIF information found"); 402 | } else if (header.length < 8) { 403 | return ReadParams.error("Invalid RIFF encoding"); 404 | } 405 | 406 | final tag = String.fromCharCodes(header.sublist(0, 4)); 407 | final length = Int8List.fromList(header.sublist(4, 8)) 408 | .buffer 409 | .asByteData() 410 | .getInt32(0, Endian.little); 411 | 412 | // According to exiftool's RIFF documentation, WebP uses "EXIF" as tag 413 | // name while other RIFF-based files tend to use "Exif". 414 | if (tag == "EXIF") { 415 | // Look for Exif\x00\x00, and skip it if present. The WebP implementation 416 | // in Exiv2 also handles a \xFF\x01\xFF\xE1\x00\x00 prefix, but with no 417 | // explanation or test file present, so we ignore that for now. 418 | final exifHeader = f.readSync(6); 419 | if (!listEqual( 420 | exifHeader, Uint8List.fromList('Exif\x00\x00'.codeUnits))) { 421 | // There was no Exif\x00\x00 marker, rewind 422 | f.setPositionSync(f.positionSync() - exifHeader.length); 423 | } 424 | 425 | final offset = f.positionSync(); 426 | final endian = Reader.endianOfByte(f.readByteSync()); 427 | return ReadParams(endian: endian, offset: offset); 428 | } 429 | 430 | // Skip forward to the next box. 431 | f.setPositionSync(f.positionSync() + length); 432 | } 433 | } 434 | 435 | ReadParams _tiffReadParams(FileReader f) { 436 | f.setPositionSync(0); 437 | final endian = Reader.endianOfByte(f.readByteSync()); 438 | f.readSync(1); 439 | return ReadParams(endian: endian, offset: 0); 440 | } 441 | 442 | class ReadParams { 443 | final bool fakeExif; 444 | final Endian endian; 445 | final int offset; 446 | final String error; 447 | 448 | ReadParams({ 449 | required this.endian, 450 | required this.offset, 451 | // by default do not fake an EXIF beginning 452 | this.fakeExif = false, 453 | }) : error = ""; 454 | 455 | ReadParams.error(this.error) 456 | : endian = Endian.little, 457 | offset = 0, 458 | fakeExif = false; 459 | } 460 | -------------------------------------------------------------------------------- /lib/src/makernote_canon.dart: -------------------------------------------------------------------------------- 1 | import 'package:exif/src/tags_info.dart' show MakerTag, TagsBase; 2 | import 'package:sprintf/sprintf.dart' show sprintf; 3 | 4 | // Makernote (proprietary) tag definitions for Canon. 5 | // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html 6 | 7 | class MakerNoteCanon extends TagsBase { 8 | static MakerTag _make(String name) => MakerTag.make(name); 9 | 10 | static MakerTag _withMap(String name, Map map) => 11 | MakerTag.makeWithMap(name, map); 12 | 13 | static final tags = { 14 | 0x0003: _make('FlashInfo'), 15 | 0x0006: _make('ImageType'), 16 | 0x0007: _make('FirmwareVersion'), 17 | 0x0008: _make('ImageNumber'), 18 | 0x0009: _make('OwnerName'), 19 | 0x000c: _make('SerialNumber'), 20 | 0x000e: _make('FileLength'), 21 | 0x0010: _withMap('ModelID', { 22 | 0x1010000: 'PowerShot A30', 23 | 0x1040000: 'PowerShot S300 / Digital IXUS 300 / IXY Digital 300', 24 | 0x1060000: 'PowerShot A20', 25 | 0x1080000: 'PowerShot A10', 26 | 0x1090000: 'PowerShot S110 / Digital IXUS v / IXY Digital 200', 27 | 0x1100000: 'PowerShot G2', 28 | 0x1110000: 'PowerShot S40', 29 | 0x1120000: 'PowerShot S30', 30 | 0x1130000: 'PowerShot A40', 31 | 0x1140000: 'EOS D30', 32 | 0x1150000: 'PowerShot A100', 33 | 0x1160000: 'PowerShot S200 / Digital IXUS v2 / IXY Digital 200a', 34 | 0x1170000: 'PowerShot A200', 35 | 0x1180000: 'PowerShot S330 / Digital IXUS 330 / IXY Digital 300a', 36 | 0x1190000: 'PowerShot G3', 37 | 0x1210000: 'PowerShot S45', 38 | 0x1230000: 'PowerShot SD100 / Digital IXUS II / IXY Digital 30', 39 | 0x1240000: 'PowerShot S230 / Digital IXUS v3 / IXY Digital 320', 40 | 0x1250000: 'PowerShot A70', 41 | 0x1260000: 'PowerShot A60', 42 | 0x1270000: 'PowerShot S400 / Digital IXUS 400 / IXY Digital 400', 43 | 0x1290000: 'PowerShot G5', 44 | 0x1300000: 'PowerShot A300', 45 | 0x1310000: 'PowerShot S50', 46 | 0x1340000: 'PowerShot A80', 47 | 0x1350000: 'PowerShot SD10 / Digital IXUS i / IXY Digital L', 48 | 0x1360000: 'PowerShot S1 IS', 49 | 0x1370000: 'PowerShot Pro1', 50 | 0x1380000: 'PowerShot S70', 51 | 0x1390000: 'PowerShot S60', 52 | 0x1400000: 'PowerShot G6', 53 | 0x1410000: 'PowerShot S500 / Digital IXUS 500 / IXY Digital 500', 54 | 0x1420000: 'PowerShot A75', 55 | 0x1440000: 'PowerShot SD110 / Digital IXUS IIs / IXY Digital 30a', 56 | 0x1450000: 'PowerShot A400', 57 | 0x1470000: 'PowerShot A310', 58 | 0x1490000: 'PowerShot A85', 59 | 0x1520000: 'PowerShot S410 / Digital IXUS 430 / IXY Digital 450', 60 | 0x1530000: 'PowerShot A95', 61 | 0x1540000: 'PowerShot SD300 / Digital IXUS 40 / IXY Digital 50', 62 | 0x1550000: 'PowerShot SD200 / Digital IXUS 30 / IXY Digital 40', 63 | 0x1560000: 'PowerShot A520', 64 | 0x1570000: 'PowerShot A510', 65 | 0x1590000: 'PowerShot SD20 / Digital IXUS i5 / IXY Digital L2', 66 | 0x1640000: 'PowerShot S2 IS', 67 | 0x1650000: 68 | 'PowerShot SD430 / Digital IXUS Wireless / IXY Digital Wireless', 69 | 0x1660000: 'PowerShot SD500 / Digital IXUS 700 / IXY Digital 600', 70 | 0x1668000: 'EOS D60', 71 | 0x1700000: 'PowerShot SD30 / Digital IXUS i Zoom / IXY Digital L3', 72 | 0x1740000: 'PowerShot A430', 73 | 0x1750000: 'PowerShot A410', 74 | 0x1760000: 'PowerShot S80', 75 | 0x1780000: 'PowerShot A620', 76 | 0x1790000: 'PowerShot A610', 77 | 0x1800000: 'PowerShot SD630 / Digital IXUS 65 / IXY Digital 80', 78 | 0x1810000: 'PowerShot SD450 / Digital IXUS 55 / IXY Digital 60', 79 | 0x1820000: 'PowerShot TX1', 80 | 0x1870000: 'PowerShot SD400 / Digital IXUS 50 / IXY Digital 55', 81 | 0x1880000: 'PowerShot A420', 82 | 0x1890000: 'PowerShot SD900 / Digital IXUS 900 Ti / IXY Digital 1000', 83 | 0x1900000: 'PowerShot SD550 / Digital IXUS 750 / IXY Digital 700', 84 | 0x1920000: 'PowerShot A700', 85 | 0x1940000: 86 | 'PowerShot SD700 IS / Digital IXUS 800 IS / IXY Digital 800 IS', 87 | 0x1950000: 'PowerShot S3 IS', 88 | 0x1960000: 'PowerShot A540', 89 | 0x1970000: 'PowerShot SD600 / Digital IXUS 60 / IXY Digital 70', 90 | 0x1980000: 'PowerShot G7', 91 | 0x1990000: 'PowerShot A530', 92 | 0x2000000: 93 | 'PowerShot SD800 IS / Digital IXUS 850 IS / IXY Digital 900 IS', 94 | 0x2010000: 'PowerShot SD40 / Digital IXUS i7 / IXY Digital L4', 95 | 0x2020000: 'PowerShot A710 IS', 96 | 0x2030000: 'PowerShot A640', 97 | 0x2040000: 'PowerShot A630', 98 | 0x2090000: 'PowerShot S5 IS', 99 | 0x2100000: 'PowerShot A460', 100 | 0x2120000: 101 | 'PowerShot SD850 IS / Digital IXUS 950 IS / IXY Digital 810 IS', 102 | 0x2130000: 'PowerShot A570 IS', 103 | 0x2140000: 'PowerShot A560', 104 | 0x2150000: 'PowerShot SD750 / Digital IXUS 75 / IXY Digital 90', 105 | 0x2160000: 'PowerShot SD1000 / Digital IXUS 70 / IXY Digital 10', 106 | 0x2180000: 'PowerShot A550', 107 | 0x2190000: 'PowerShot A450', 108 | 0x2230000: 'PowerShot G9', 109 | 0x2240000: 'PowerShot A650 IS', 110 | 0x2260000: 'PowerShot A720 IS', 111 | 0x2290000: 'PowerShot SX100 IS', 112 | 0x2300000: 113 | 'PowerShot SD950 IS / Digital IXUS 960 IS / IXY Digital 2000 IS', 114 | 0x2310000: 115 | 'PowerShot SD870 IS / Digital IXUS 860 IS / IXY Digital 910 IS', 116 | 0x2320000: 117 | 'PowerShot SD890 IS / Digital IXUS 970 IS / IXY Digital 820 IS', 118 | 0x2360000: 'PowerShot SD790 IS / Digital IXUS 90 IS / IXY Digital 95 IS', 119 | 0x2370000: 'PowerShot SD770 IS / Digital IXUS 85 IS / IXY Digital 25 IS', 120 | 0x2380000: 'PowerShot A590 IS', 121 | 0x2390000: 'PowerShot A580', 122 | 0x2420000: 'PowerShot A470', 123 | 0x2430000: 'PowerShot SD1100 IS / Digital IXUS 80 IS / IXY Digital 20 IS', 124 | 0x2460000: 'PowerShot SX1 IS', 125 | 0x2470000: 'PowerShot SX10 IS', 126 | 0x2480000: 'PowerShot A1000 IS', 127 | 0x2490000: 'PowerShot G10', 128 | 0x2510000: 'PowerShot A2000 IS', 129 | 0x2520000: 'PowerShot SX110 IS', 130 | 0x2530000: 131 | 'PowerShot SD990 IS / Digital IXUS 980 IS / IXY Digital 3000 IS', 132 | 0x2540000: 133 | 'PowerShot SD880 IS / Digital IXUS 870 IS / IXY Digital 920 IS', 134 | 0x2550000: 'PowerShot E1', 135 | 0x2560000: 'PowerShot D10', 136 | 0x2570000: 137 | 'PowerShot SD960 IS / Digital IXUS 110 IS / IXY Digital 510 IS', 138 | 0x2580000: 'PowerShot A2100 IS', 139 | 0x2590000: 'PowerShot A480', 140 | 0x2600000: 'PowerShot SX200 IS', 141 | 0x2610000: 142 | 'PowerShot SD970 IS / Digital IXUS 990 IS / IXY Digital 830 IS', 143 | 0x2620000: 144 | 'PowerShot SD780 IS / Digital IXUS 100 IS / IXY Digital 210 IS', 145 | 0x2630000: 'PowerShot A1100 IS', 146 | 0x2640000: 147 | 'PowerShot SD1200 IS / Digital IXUS 95 IS / IXY Digital 110 IS', 148 | 0x2700000: 'PowerShot G11', 149 | 0x2710000: 'PowerShot SX120 IS', 150 | 0x2720000: 'PowerShot S90', 151 | 0x2750000: 'PowerShot SX20 IS', 152 | 0x2760000: 153 | 'PowerShot SD980 IS / Digital IXUS 200 IS / IXY Digital 930 IS', 154 | 0x2770000: 155 | 'PowerShot SD940 IS / Digital IXUS 120 IS / IXY Digital 220 IS', 156 | 0x2800000: 'PowerShot A495', 157 | 0x2810000: 'PowerShot A490', 158 | 0x2820000: 'PowerShot A3100 IS / A3150 IS', 159 | 0x2830000: 'PowerShot A3000 IS', 160 | 0x2840000: 'PowerShot SD1400 IS / IXUS 130 / IXY 400F', 161 | 0x2850000: 'PowerShot SD1300 IS / IXUS 105 / IXY 200F', 162 | 0x2860000: 'PowerShot SD3500 IS / IXUS 210 / IXY 10S', 163 | 0x2870000: 'PowerShot SX210 IS', 164 | 0x2880000: 'PowerShot SD4000 IS / IXUS 300 HS / IXY 30S', 165 | 0x2890000: 'PowerShot SD4500 IS / IXUS 1000 HS / IXY 50S', 166 | 0x2920000: 'PowerShot G12', 167 | 0x2930000: 'PowerShot SX30 IS', 168 | 0x2940000: 'PowerShot SX130 IS', 169 | 0x2950000: 'PowerShot S95', 170 | 0x2980000: 'PowerShot A3300 IS', 171 | 0x2990000: 'PowerShot A3200 IS', 172 | 0x3000000: 'PowerShot ELPH 500 HS / IXUS 310 HS / IXY 31S', 173 | 0x3010000: 'PowerShot Pro90 IS', 174 | 0x3010001: 'PowerShot A800', 175 | 0x3020000: 'PowerShot ELPH 100 HS / IXUS 115 HS / IXY 210F', 176 | 0x3030000: 'PowerShot SX230 HS', 177 | 0x3040000: 'PowerShot ELPH 300 HS / IXUS 220 HS / IXY 410F', 178 | 0x3050000: 'PowerShot A2200', 179 | 0x3060000: 'PowerShot A1200', 180 | 0x3070000: 'PowerShot SX220 HS', 181 | 0x3080000: 'PowerShot G1 X', 182 | 0x3090000: 'PowerShot SX150 IS', 183 | 0x3100000: 'PowerShot ELPH 510 HS / IXUS 1100 HS / IXY 51S', 184 | 0x3110000: 'PowerShot S100 (new)', 185 | 0x3130000: 'PowerShot SX40 HS', 186 | 0x3120000: 'PowerShot ELPH 310 HS / IXUS 230 HS / IXY 600F', 187 | 0x3160000: 'PowerShot A1300', 188 | 0x3170000: 'PowerShot A810', 189 | 0x3180000: 'PowerShot ELPH 320 HS / IXUS 240 HS / IXY 420F', 190 | 0x3190000: 'PowerShot ELPH 110 HS / IXUS 125 HS / IXY 220F', 191 | 0x3200000: 'PowerShot D20', 192 | 0x3210000: 'PowerShot A4000 IS', 193 | 0x3220000: 'PowerShot SX260 HS', 194 | 0x3230000: 'PowerShot SX240 HS', 195 | 0x3240000: 'PowerShot ELPH 530 HS / IXUS 510 HS / IXY 1', 196 | 0x3250000: 'PowerShot ELPH 520 HS / IXUS 500 HS / IXY 3', 197 | 0x3260000: 'PowerShot A3400 IS', 198 | 0x3270000: 'PowerShot A2400 IS', 199 | 0x3280000: 'PowerShot A2300', 200 | 0x3330000: 'PowerShot G15', 201 | 0x3340000: 'PowerShot SX50', 202 | 0x3350000: 'PowerShot SX160 IS', 203 | 0x3360000: 'PowerShot S110 (new)', 204 | 0x3370000: 'PowerShot SX500 IS', 205 | 0x3380000: 'PowerShot N', 206 | 0x3390000: 'IXUS 245 HS / IXY 430F', 207 | 0x3400000: 'PowerShot SX280 HS', 208 | 0x3410000: 'PowerShot SX270 HS', 209 | 0x3420000: 'PowerShot A3500 IS', 210 | 0x3430000: 'PowerShot A2600', 211 | 0x3450000: 'PowerShot A1400', 212 | 0x3460000: 'PowerShot ELPH 130 IS / IXUS 140 / IXY 110F', 213 | 0x3470000: 'PowerShot ELPH 115/120 IS / IXUS 132/135 / IXY 90F/100F', 214 | 0x3490000: 'PowerShot ELPH 330 HS / IXUS 255 HS / IXY 610F', 215 | 0x3510000: 'PowerShot A2500', 216 | 0x3540000: 'PowerShot G16', 217 | 0x3550000: 'PowerShot S120', 218 | 0x3560000: 'PowerShot SX170 IS', 219 | 0x3580000: 'PowerShot SX510 HS', 220 | 0x3590000: 'PowerShot S200 (new)', 221 | 0x3600000: 'IXY 620F', 222 | 0x3610000: 'PowerShot N100', 223 | 0x3640000: 'PowerShot G1 X Mark II', 224 | 0x3650000: 'PowerShot D30', 225 | 0x3660000: 'PowerShot SX700 HS', 226 | 0x3670000: 'PowerShot SX600 HS', 227 | 0x3680000: 'PowerShot ELPH 140 IS / IXUS 150 / IXY 130', 228 | 0x3690000: 'PowerShot ELPH 135 / IXUS 145 / IXY 120', 229 | 0x3700000: 'PowerShot ELPH 340 HS / IXUS 265 HS / IXY 630', 230 | 0x3710000: 'PowerShot ELPH 150 IS / IXUS 155 / IXY 140', 231 | 0x3740000: 'EOS M3', 232 | 0x3750000: 'PowerShot SX60 HS', 233 | 0x3760000: 'PowerShot SX520 HS', 234 | 0x3770000: 'PowerShot SX400 IS', 235 | 0x3780000: 'PowerShot G7 X', 236 | 0x3790000: 'PowerShot N2', 237 | 0x3800000: 'PowerShot SX530 HS', 238 | 0x3820000: 'PowerShot SX710 HS', 239 | 0x3830000: 'PowerShot SX610 HS', 240 | 0x3870000: 'PowerShot ELPH 160 / IXUS 160', 241 | 0x3890000: 'PowerShot ELPH 170 IS / IXUS 170', 242 | 0x3910000: 'PowerShot SX410 IS', 243 | 0x4040000: 'PowerShot G1', 244 | 0x6040000: 'PowerShot S100 / Digital IXUS / IXY Digital', 245 | 0x4007d673: 'DC19/DC21/DC22', 246 | 0x4007d674: 'XH A1', 247 | 0x4007d675: 'HV10', 248 | 0x4007d676: 'MD130/MD140/MD150/MD160/ZR850', 249 | 0x4007d777: 'DC50', 250 | 0x4007d778: 'HV20', 251 | 0x4007d779: 'DC211', 252 | 0x4007d77a: 'HG10', 253 | 0x4007d77b: 'HR10', 254 | 0x4007d77d: 'MD255/ZR950', 255 | 0x4007d81c: 'HF11', 256 | 0x4007d878: 'HV30', 257 | 0x4007d87c: 'XH A1S', 258 | 0x4007d87e: 'DC301/DC310/DC311/DC320/DC330', 259 | 0x4007d87f: 'FS100', 260 | 0x4007d880: 'HF10', 261 | 0x4007d882: 'HG20/HG21', 262 | 0x4007d925: 'HF21', 263 | 0x4007d926: 'HF S11', 264 | 0x4007d978: 'HV40', 265 | 0x4007d987: 'DC410/DC411/DC420', 266 | 0x4007d988: 'FS19/FS20/FS21/FS22/FS200', 267 | 0x4007d989: 'HF20/HF200', 268 | 0x4007d98a: 'HF S10/S100', 269 | 0x4007da8e: 'HF R10/R16/R17/R18/R100/R106', 270 | 0x4007da8f: 'HF M30/M31/M36/M300/M306', 271 | 0x4007da90: 'HF S20/S21/S200', 272 | 0x4007da92: 'FS31/FS36/FS37/FS300/FS305/FS306/FS307', 273 | 0x4007dda9: 'HF G25', 274 | 0x80000001: 'EOS-1D', 275 | 0x80000167: 'EOS-1DS', 276 | 0x80000168: 'EOS 10D', 277 | 0x80000169: 'EOS-1D Mark III', 278 | 0x80000170: 'EOS Digital Rebel / 300D / Kiss Digital', 279 | 0x80000174: 'EOS-1D Mark II', 280 | 0x80000175: 'EOS 20D', 281 | 0x80000176: 'EOS Digital Rebel XSi / 450D / Kiss X2', 282 | 0x80000188: 'EOS-1Ds Mark II', 283 | 0x80000189: 'EOS Digital Rebel XT / 350D / Kiss Digital N', 284 | 0x80000190: 'EOS 40D', 285 | 0x80000213: 'EOS 5D', 286 | 0x80000215: 'EOS-1Ds Mark III', 287 | 0x80000218: 'EOS 5D Mark II', 288 | 0x80000219: 'WFT-E1', 289 | 0x80000232: 'EOS-1D Mark II N', 290 | 0x80000234: 'EOS 30D', 291 | 0x80000236: 'EOS Digital Rebel XTi / 400D / Kiss Digital X', 292 | 0x80000241: 'WFT-E2', 293 | 0x80000246: 'WFT-E3', 294 | 0x80000250: 'EOS 7D', 295 | 0x80000252: 'EOS Rebel T1i / 500D / Kiss X3', 296 | 0x80000254: 'EOS Rebel XS / 1000D / Kiss F', 297 | 0x80000261: 'EOS 50D', 298 | 0x80000269: 'EOS-1D X', 299 | 0x80000270: 'EOS Rebel T2i / 550D / Kiss X4', 300 | 0x80000271: 'WFT-E4', 301 | 0x80000273: 'WFT-E5', 302 | 0x80000281: 'EOS-1D Mark IV', 303 | 0x80000285: 'EOS 5D Mark III', 304 | 0x80000286: 'EOS Rebel T3i / 600D / Kiss X5', 305 | 0x80000287: 'EOS 60D', 306 | 0x80000288: 'EOS Rebel T3 / 1100D / Kiss X50', 307 | 0x80000289: 'EOS 7D Mark II', 308 | 0x80000297: 'WFT-E2 II', 309 | 0x80000298: 'WFT-E4 II', 310 | 0x80000301: 'EOS Rebel T4i / 650D / Kiss X6i', 311 | 0x80000302: 'EOS 6D', 312 | 0x80000324: 'EOS-1D C', 313 | 0x80000325: 'EOS 70D', 314 | 0x80000326: 'EOS Rebel T5i / 700D / Kiss X7i', 315 | 0x80000327: 'EOS Rebel T5 / 1200D / Kiss X70', 316 | 0x80000331: 'EOS M', 317 | 0x80000355: 'EOS M2', 318 | 0x80000346: 'EOS Rebel SL1 / 100D / Kiss X7', 319 | 0x80000347: 'EOS Rebel T6s / 760D / 8000D', 320 | 0x80000382: 'EOS 5DS', 321 | 0x80000393: 'EOS Rebel T6i / 750D / Kiss X8i', 322 | 0x80000401: 'EOS 5DS R', 323 | }), 324 | 0x0013: _make('ThumbnailImageValidArea'), 325 | 0x0015: _withMap( 326 | 'SerialNumberFormat', {0x90000000: 'Format 1', 0xA0000000: 'Format 2'}), 327 | 0x001a: 328 | _withMap('SuperMacro', {0: 'Off', 1: 'On const ()', 2: 'On const ()'}), 329 | 0x001c: _withMap('DateStampMode', { 330 | 0: 'Off', 331 | 1: 'Date', 332 | 2: 'Date & Time', 333 | }), 334 | 0x001e: _make('FirmwareRevision'), 335 | 0x0028: _make('ImageUniqueID'), 336 | 0x0095: _make('LensModel'), 337 | 0x0096: _make('InternalSerialNumber '), 338 | 0x0097: _make('DustRemovalData '), 339 | 0x0098: _make('CropInfo '), 340 | 0x009a: _make('AspectInfo'), 341 | 0x00b4: _withMap('ColorSpace', {1: 'sRGB', 2: 'Adobe RGB'}), 342 | }; 343 | 344 | static final tagsXxx = { 345 | 'MakerNote Tag 0x0001': cameraSettings, 346 | 'MakerNote Tag 0x0002': focalLength, 347 | 'MakerNote Tag 0x0004': shotInfo, 348 | 'MakerNote Tag 0x0026': afInfo2, 349 | 'MakerNote Tag 0x0093': fileInfo, 350 | }; 351 | 352 | // this is in element offset, name, optional value dictionary format 353 | // 0x0001 354 | static Map cameraSettings = { 355 | 1: _withMap('Macromode', {1: 'Macro', 2: 'Normal'}), 356 | 2: _make('SelfTimer'), 357 | 3: _withMap( 358 | 'Quality', {1: 'Economy', 2: 'Normal', 3: 'Fine', 5: 'Superfine'}), 359 | 4: _withMap('FlashMode', { 360 | 0: 'Flash Not Fired', 361 | 1: 'Auto', 362 | 2: 'On', 363 | 3: 'Red-Eye Reduction', 364 | 4: 'Slow Synchro', 365 | 5: 'Auto + Red-Eye Reduction', 366 | 6: 'On + Red-Eye Reduction', 367 | 16: 'external flash' 368 | }), 369 | 5: _withMap('ContinuousDriveMode', { 370 | 0: 'Single Or Timer', 371 | 1: 'Continuous', 372 | 2: 'Movie', 373 | }), 374 | 7: _withMap('FocusMode', { 375 | 0: 'One-Shot', 376 | 1: 'AI Servo', 377 | 2: 'AI Focus', 378 | 3: 'MF', 379 | 4: 'Single', 380 | 5: 'Continuous', 381 | 6: 'MF' 382 | }), 383 | 9: _withMap('RecordMode', { 384 | 1: 'JPEG', 385 | 2: 'CRW+THM', 386 | 3: 'AVI+THM', 387 | 4: 'TIF', 388 | 5: 'TIF+JPEG', 389 | 6: 'CR2', 390 | 7: 'CR2+JPEG', 391 | 9: 'Video' 392 | }), 393 | 10: _withMap('ImageSize', {0: 'Large', 1: 'Medium', 2: 'Small'}), 394 | 11: _withMap('EasyShootingMode', { 395 | 0: 'Full Auto', 396 | 1: 'Manual', 397 | 2: 'Landscape', 398 | 3: 'Fast Shutter', 399 | 4: 'Slow Shutter', 400 | 5: 'Night', 401 | 6: 'B&W', 402 | 7: 'Sepia', 403 | 8: 'Portrait', 404 | 9: 'Sports', 405 | 10: 'Macro/Close-Up', 406 | 11: 'Pan Focus', 407 | 51: 'High Dynamic Range', 408 | }), 409 | 12: _withMap('DigitalZoom', {0: 'None', 1: '2x', 2: '4x', 3: 'Other'}), 410 | 13: _withMap('Contrast', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}), 411 | 14: _withMap('Saturation', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}), 412 | 15: _withMap('Sharpness', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}), 413 | 16: _withMap('ISO', { 414 | 0: 'See ISOSpeedRatings Tag', 415 | 15: 'Auto', 416 | 16: '50', 417 | 17: '100', 418 | 18: '200', 419 | 19: '400' 420 | }), 421 | 17: _withMap('MeteringMode', { 422 | 0: 'Default', 423 | 1: 'Spot', 424 | 2: 'Average', 425 | 3: 'Evaluative', 426 | 4: 'Partial', 427 | 5: 'Center-weighted' 428 | }), 429 | 18: _withMap('FocusType', { 430 | 0: 'Manual', 431 | 1: 'Auto', 432 | 3: 'Close-Up (Macro)', 433 | 8: 'Locked (Pan Mode)' 434 | }), 435 | 19: _withMap('AFPointSelected', { 436 | 0x3000: 'None (MF)', 437 | 0x3001: 'Auto-Selected', 438 | 0x3002: 'Right', 439 | 0x3003: 'Center', 440 | 0x3004: 'Left' 441 | }), 442 | 20: _withMap('ExposureMode', { 443 | 0: 'Easy Shooting', 444 | 1: 'Program', 445 | 2: 'Tv-priority', 446 | 3: 'Av-priority', 447 | 4: 'Manual', 448 | 5: 'A-DEP' 449 | }), 450 | 22: _make('LensType'), 451 | 23: _make('LongFocalLengthOfLensInFocalUnits'), 452 | 24: _make('ShortFocalLengthOfLensInFocalUnits'), 453 | 25: _make('FocalUnitsPerMM'), 454 | 28: _withMap('FlashActivity', {0: 'Did Not Fire', 1: 'Fired'}), 455 | 29: _withMap('FlashDetails', { 456 | 0: 'Manual', 457 | 1: 'TTL', 458 | 2: 'A-TTL', 459 | 3: 'E-TTL', 460 | 4: 'FP Sync Enabled', 461 | 7: '2nd("Rear")-Curtain Sync Used', 462 | 11: 'FP Sync Used', 463 | 13: 'Internal Flash', 464 | 14: 'External E-TTL' 465 | }), 466 | 32: _withMap('FocusMode', {0: 'Single', 1: 'Continuous', 8: 'Manual'}), 467 | 33: _withMap('AESetting', { 468 | 0: 'Normal AE', 469 | 1: 'Exposure Compensation', 470 | 2: 'AE Lock', 471 | 3: 'AE Lock + Exposure Comp.', 472 | 4: 'No AE' 473 | }), 474 | 34: _withMap('ImageStabilization', { 475 | 0: 'Off', 476 | 1: 'On', 477 | 2: 'Shoot Only', 478 | 3: 'Panning', 479 | 4: 'Dynamic', 480 | 256: 'Off', 481 | 257: 'On', 482 | 258: 'Shoot Only', 483 | 259: 'Panning', 484 | 260: 'Dynamic' 485 | }), 486 | 39: _withMap('SpotMeteringMode', {0: 'Center', 1: 'AF Point'}), 487 | 41: _withMap('ManualFlashOutput', { 488 | 0x0: 'n/a', 489 | 0x500: 'Full', 490 | 0x502: 'Medium', 491 | 0x504: 'Low', 492 | 0x7fff: 'n/a' 493 | }), 494 | }; 495 | 496 | // 0x0002 497 | static Map focalLength = { 498 | 1: _withMap('FocalType', { 499 | 1: 'Fixed', 500 | 2: 'Zoom', 501 | }), 502 | 2: _make('FocalLength'), 503 | }; 504 | 505 | // 0x0004 506 | static Map shotInfo = { 507 | 7: _withMap('WhiteBalance', { 508 | 0: 'Auto', 509 | 1: 'Sunny', 510 | 2: 'Cloudy', 511 | 3: 'Tungsten', 512 | 4: 'Fluorescent', 513 | 5: 'Flash', 514 | 6: 'Custom' 515 | }), 516 | 8: _withMap('SlowShutter', 517 | {-1: 'n/a', 0: 'Off', 1: 'Night Scene', 2: 'On', 3: 'None'}), 518 | 9: _make('SequenceNumber'), 519 | 14: _make('AFPointUsed'), 520 | 15: _withMap('FlashBias', { 521 | 0xFFC0: '-2 EV', 522 | 0xFFCC: '-1.67 EV', 523 | 0xFFD0: '-1.50 EV', 524 | 0xFFD4: '-1.33 EV', 525 | 0xFFE0: '-1 EV', 526 | 0xFFEC: '-0.67 EV', 527 | 0xFFF0: '-0.50 EV', 528 | 0xFFF4: '-0.33 EV', 529 | 0x0000: '0 EV', 530 | 0x000c: '0.33 EV', 531 | 0x0010: '0.50 EV', 532 | 0x0014: '0.67 EV', 533 | 0x0020: '1 EV', 534 | 0x002c: '1.33 EV', 535 | 0x0030: '1.50 EV', 536 | 0x0034: '1.67 EV', 537 | 0x0040: '2 EV' 538 | }), 539 | 19: _make('SubjectDistance'), 540 | }; 541 | 542 | // 0x0026 543 | static Map afInfo2 = { 544 | 2: _withMap('AFAreaMode', { 545 | 0: 'Off (Manual Focus)', 546 | 2: 'Single-point AF', 547 | 4: 'Multi-point AF or AI AF', 548 | 5: 'Face Detect AF', 549 | 6: 'Face + Tracking', 550 | 7: 'Zone AF', 551 | 8: 'AF Point Expansion', 552 | 9: 'Spot AF', 553 | 11: 'Flexizone Multi', 554 | 13: 'Flexizone Single', 555 | }), 556 | 3: _make('NumAFPoints'), 557 | 4: _make('ValidAFPoints'), 558 | 5: _make('CanonImageWidth'), 559 | }; 560 | 561 | // 0x0093 562 | static Map fileInfo = { 563 | 1: _make('FileNumber'), 564 | 3: _withMap('BracketMode', { 565 | 0: 'Off', 566 | 1: 'AEB', 567 | 2: 'FEB', 568 | 3: 'ISO', 569 | 4: 'WB', 570 | }), 571 | 4: _make('BracketValue'), 572 | 5: _make('BracketShotNumber'), 573 | 6: _withMap('RawJpgQuality', { 574 | 0xFFFF: 'n/a', 575 | 1: 'Economy', 576 | 2: 'Normal', 577 | 3: 'Fine', 578 | 4: 'RAW', 579 | 5: 'Superfine', 580 | 130: 'Normal Movie' 581 | }), 582 | 7: _withMap('RawJpgSize', { 583 | 0: 'Large', 584 | 1: 'Medium', 585 | 2: 'Small', 586 | 5: 'Medium 1', 587 | 6: 'Medium 2', 588 | 7: 'Medium 3', 589 | 8: 'Postcard', 590 | 9: 'Widescreen', 591 | 10: 'Medium Widescreen', 592 | 14: 'Small 1', 593 | 15: 'Small 2', 594 | 16: 'Small 3', 595 | 128: '640x480 Movie', 596 | 129: 'Medium Movie', 597 | 130: 'Small Movie', 598 | 137: '1280x720 Movie', 599 | 142: '1920x1080 Movie', 600 | }), 601 | 8: _withMap('LongExposureNoiseReduction2', 602 | {0: 'Off', 1: 'On (1D)', 2: 'On', 3: 'Auto'}), 603 | 9: _withMap( 604 | 'WBBracketMode', {0: 'Off', 1: 'On (shift AB)', 2: 'On (shift GM)'}), 605 | 12: _make('WBBracketValueAB'), 606 | 13: _make('WBBracketValueGM'), 607 | 14: _withMap('FilterEffect', 608 | {0: 'None', 1: 'Yellow', 2: 'Orange', 3: 'Red', 4: 'Green'}), 609 | 15: _withMap('ToningEffect', { 610 | 0: 'None', 611 | 1: 'Sepia', 612 | 2: 'Blue', 613 | 3: 'Purple', 614 | 4: 'Green', 615 | }), 616 | 16: _make('MacroMagnification'), 617 | 19: _withMap('LiveViewShooting', {0: 'Off', 1: 'On'}), 618 | 25: _withMap('FlashExposureLock', {0: 'Off', 1: 'On'}) 619 | }; 620 | 621 | static String addOneFunc(int value) { 622 | return "${value + 1}"; 623 | } 624 | 625 | static String subtractOneFunc(int value) { 626 | return "${value - 1}"; 627 | } 628 | 629 | static String convertTempFunc(int value) { 630 | return sprintf('%d C', [value - 128]); 631 | } 632 | 633 | // CameraInfo data structures have variable sized members. Each entry here is: 634 | // byte offset: (item name, data item type, decoding map). 635 | // Note that the data item type is fed directly to struct.unpack at the 636 | // specified offset. 637 | static const cameraInfoTagName = 'MakerNote Tag 0x000D'; 638 | 639 | // A map of regular expressions on 'Image Model' to the CameraInfo spec 640 | static Map> cameraInfoModelMap = { 641 | r'EOS 5D$': { 642 | 23: const CameraInfo('CameraTemperature', 1, convertTempFunc), 643 | 204: const CameraInfo('DirectoryIndex', 4, subtractOneFunc), 644 | 208: const CameraInfo('FileIndex', 2, addOneFunc), 645 | }, 646 | r'EOS 5D Mark II$': { 647 | 25: const CameraInfo('CameraTemperature', 1, convertTempFunc), 648 | 443: const CameraInfo('FileIndex', 4, addOneFunc), 649 | 455: const CameraInfo('DirectoryIndex', 4, subtractOneFunc), 650 | }, 651 | r'EOS 5D Mark III$': { 652 | 27: const CameraInfo('CameraTemperature', 1, convertTempFunc), 653 | 652: const CameraInfo('FileIndex', 4, addOneFunc), 654 | 656: const CameraInfo('FileIndex2', 4, addOneFunc), 655 | 664: const CameraInfo('DirectoryIndex', 4, subtractOneFunc), 656 | 668: const CameraInfo('DirectoryIndex2', 4, subtractOneFunc), 657 | }, 658 | r'\b(600D|REBEL T3i|Kiss X5)\b': { 659 | 25: const CameraInfo('CameraTemperature', 1, convertTempFunc), 660 | 475: const CameraInfo('FileIndex', 4, addOneFunc), 661 | 487: const CameraInfo('DirectoryIndex', 4, subtractOneFunc), 662 | }, 663 | }; 664 | } 665 | 666 | class CameraInfo { 667 | final String tagName; 668 | final int tagSize; 669 | final String Function(int) function; 670 | 671 | const CameraInfo( 672 | this.tagName, 673 | this.tagSize, 674 | this.function, 675 | ); 676 | } 677 | --------------------------------------------------------------------------------