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