exif(): read HEIF/HEIC originals with exifreader (closes #145) #147

Merged
clawbot merged 5 commits from issue-145-heif-exif into next 2026-10-01 22:34:09 +02:00
2 changed files with 30 additions and 6 deletions
Showing only changes of commit 9c0c7c9489 - Show all commits
+4 -6
View File
@@ -500,12 +500,10 @@ its XMP metadata, its EXIF metadata and, for a JPEG, its dimensions. EXIF is
read with [exifreader](https://github.com/mattiasw/ExifReader) from any image read with [exifreader](https://github.com/mattiasw/ExifReader) from any image
format it reads, JPEG, HEIC/HEIF, AVIF, PNG and WebP among them. The record's format it reads, JPEG, HEIC/HEIF, AVIF, PNG and WebP among them. The record's
`exif` field is exifreader's EXIF tag output: each tag by name, with its `exif` field is exifreader's EXIF tag output: each tag by name, with its
`value`, `description` and `computed` value. Dumps written before 2026-10-01 `value`, `description` and `computed` value. An EXIF block exifreader finds but
hold `exif-reader`'s `Image`, `Photo` and `GPSInfo` groups there instead. An reads no tag from is recorded, base64, as `exifRaw`, with the reason in
EXIF block exifreader finds but reads no tag from is recorded, base64, as `exifError`. `collections`, `files`, `backup`, `helper list-missing-thumbnails`
`exifRaw`, with the reason in `exifError`. `collections`, `files`, `backup`, and `helper fix-missing-thumbnails` take `--json` for machine-readable output.
`helper list-missing-thumbnails` and `helper fix-missing-thumbnails` take
`--json` for machine-readable output.
`backup-metadata` fetches ML data in requests of up to 200 files. When a request `backup-metadata` fetches ML data in requests of up to 200 files. When a request
fails, the error is logged, each of its files is written with the reason in an fails, the error is logged, each of its files is written with the reason in an
+26
View File
@@ -27,6 +27,25 @@ const TIFF_ORIENTATION_6 = [
0x00, 0x00, 0x00, 0x00,
]; ];
// A big-endian TIFF block holding Orientation 6 and DateTimeOriginal
// "0000:00:00 00:00:00", which a camera with an unset clock writes.
const TIFF_UNSET_DATE = [
...[0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08], // the first IFD at 8
// The first IFD, at 8: two entries, then no next IFD.
...[0x00, 0x02],
// Orientation (0x0112), SHORT, 6.
...[0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00],
// The Exif IFD's offset (0x8769), LONG, 38.
...[0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x26],
...[0x00, 0x00, 0x00, 0x00],
// The Exif IFD, at 38: one entry, then no next IFD.
...[0x00, 0x01],
// DateTimeOriginal (0x9003), 20 ASCII bytes at 56.
...[0x90, 0x03, 0x00, 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x38],
...[0x00, 0x00, 0x00, 0x00],
...new TextEncoder().encode("0000:00:00 00:00:00\0"), // at 56
];
// An APP1 segment whose length field matches its data. // An APP1 segment whose length field matches its data.
const app1 = (data: number[]): number[] => { const app1 = (data: number[]): number[] => {
const len = data.length + 2; const len = data.length + 2;
@@ -44,6 +63,13 @@ describe("readPhotoExif", () => {
}); });
}); });
it("gives no dateTimeOriginal for a DateTimeOriginal of 0000:00:00 00:00:00", () => {
const data = [...EXIF_HEADER, ...TIFF_UNSET_DATE];
expect(readPhotoExif(bytes(SOI, app1(data), SOS))).toStrictEqual({
orientation: 6,
});
});
it.each([ it.each([
[ [
"a file that is not an image", "a file that is not an image",