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 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> readExifFromBytes(List 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> 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(''); 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 header) => header.length >= 4 && listContainedIn( header.sublist(0, 4), ['II*\x00'.codeUnits, 'MM\x00*'.codeUnits]); bool _isHeic(List header) => listRangeEqual(header, 4, 12, 'ftypheic'.codeUnits); bool _isAvif(List header) => listRangeEqual(header, 4, 12, 'ftypavif'.codeUnits); bool _isJpeg(List header) => listRangeEqual(header, 0, 2, '\xFF\xD8'.codeUnits); bool _isPng(List header) => listRangeEqual(header, 0, 8, '\x89PNG\r\n\x1a\n'.codeUnits); bool _isWebp(List 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; }