twonly-app-dependencies/exif/lib/src/read_exif.dart
2026-05-31 01:24:27 +02:00

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;
}