exif(): read HEIF/HEIC originals with exifreader (closes #145) #147
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user