check / check (push) Successful in 2m14s
Each file's JSON gains imageMetadata, what extractImageMetadata finds in
the stored original (for a live photo, its image), or the reason the
read failed in imageMetadataError; a failed read fails neither the file
nor the run. A video is not read. An original is read when the run
stores it or when its JSON has neither field; otherwise the field is
carried over from that JSON, so a run does not read every original
again. The hand-built JPEG fixtures move to test/exif-jpeg.ts so the
backup tests can use them.
Judgement call: an original with nothing to record gets imageMetadata
{} instead of no field, so it is not read again on every run.
Model: opus-5-5
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
/**
|
|
* Hand-built JPEGs for the tests of `exif()` and of the image metadata
|
|
* `quak backup` records: one whose EXIF holds the same values as `exif.heic`
|
|
* (see `exif-heic.ts`), and one whose EXIF cannot be parsed.
|
|
*/
|
|
|
|
// Big-endian bytes for the hand-built JPEG below.
|
|
const u16 = (n: number): number[] => [n >> 8, n & 0xff];
|
|
const u32 = (n: number): number[] => [...u16(n >>> 16), ...u16(n & 0xffff)];
|
|
const ascii = (s: string): number[] => [...new TextEncoder().encode(s), 0];
|
|
const rational = (num: number, den: number): number[] => [
|
|
...u32(num),
|
|
...u32(den),
|
|
];
|
|
// One IFD entry: tag, type (1 BYTE, 2 ASCII, 3 SHORT, 4 LONG, 5 RATIONAL),
|
|
// count, then the value when it fits in 4 bytes, else its offset.
|
|
const entry = (
|
|
tag: number,
|
|
type: number,
|
|
count: number,
|
|
value: number[],
|
|
): number[] => [...u16(tag), ...u16(type), ...u32(count), ...value];
|
|
|
|
// The TIFF block of a JPEG's EXIF segment, holding every field `Photo`'s typed
|
|
// methods return: the camera in the first IFD, the exposure in the Exif IFD,
|
|
// and a GPS position of 40°26'46" N, 79°58'56" W, 12.5 m below sea level.
|
|
// Offsets count from the start of this block.
|
|
const TIFF = [
|
|
...[0x4d, 0x4d, 0x00, 0x2a], // big-endian TIFF
|
|
...u32(8), // the first IFD's offset
|
|
// The first IFD, at 8: five entries, then no next IFD.
|
|
...u16(5),
|
|
...entry(0x010f, 2, 6, u32(74)), // Make
|
|
...entry(0x0110, 2, 7, u32(80)), // Model
|
|
...entry(0x0112, 3, 1, [...u16(6), 0, 0]), // Orientation
|
|
...entry(0x8769, 4, 1, u32(88)), // the Exif IFD's offset
|
|
...entry(0x8825, 4, 1, u32(246)), // the GPS IFD's offset
|
|
...u32(0),
|
|
...ascii("Canon"), // at 74
|
|
...ascii("EOS R5"), // at 80
|
|
0, // a pad byte
|
|
// The Exif IFD, at 88: seven entries, then no next IFD.
|
|
...u16(7),
|
|
...entry(0x829a, 5, 1, u32(178)), // ExposureTime
|
|
...entry(0x829d, 5, 1, u32(186)), // FNumber
|
|
...entry(0x8827, 3, 1, [...u16(400), 0, 0]), // ISOSpeedRatings
|
|
...entry(0x9003, 2, 20, u32(194)), // DateTimeOriginal
|
|
...entry(0x9011, 2, 7, u32(214)), // OffsetTimeOriginal
|
|
...entry(0x920a, 5, 1, u32(222)), // FocalLength
|
|
...entry(0xa434, 2, 16, u32(230)), // LensModel
|
|
...u32(0),
|
|
...rational(1, 250), // at 178
|
|
...rational(28, 10), // at 186
|
|
...ascii("2021:07:15 14:30:00"), // at 194
|
|
...ascii("+02:00"), // at 214
|
|
0, // a pad byte
|
|
...rational(50, 1), // at 222
|
|
...ascii("RF50mm F1.8 STM"), // at 230
|
|
// The GPS IFD, at 246: six entries, then no next IFD.
|
|
...u16(6),
|
|
...entry(0x0001, 2, 2, [...ascii("N"), 0, 0]), // GPSLatitudeRef
|
|
...entry(0x0002, 5, 3, u32(324)), // GPSLatitude
|
|
...entry(0x0003, 2, 2, [...ascii("W"), 0, 0]), // GPSLongitudeRef
|
|
...entry(0x0004, 5, 3, u32(348)), // GPSLongitude
|
|
...entry(0x0005, 1, 1, [1, 0, 0, 0]), // GPSAltitudeRef: below sea level
|
|
...entry(0x0006, 5, 1, u32(372)), // GPSAltitude
|
|
...u32(0),
|
|
...[...rational(40, 1), ...rational(26, 1), ...rational(46, 1)], // at 324
|
|
...[...rational(79, 1), ...rational(58, 1), ...rational(56, 1)], // at 348
|
|
...rational(25, 2), // at 372
|
|
];
|
|
|
|
export const JPEG_WITH_EXIF = new Uint8Array([
|
|
...[0xff, 0xd8], // start of image
|
|
...[0xff, 0xe1, ...u16(2 + 6 + TIFF.length)], // APP1 and its length
|
|
...[...ascii("Exif"), 0], // "Exif\0\0"
|
|
...TIFF,
|
|
...[0xff, 0xda, 0x00, 0x02], // start of scan
|
|
]);
|
|
|
|
// A JPEG whose EXIF segment is laid out correctly but holds "XX" where the TIFF
|
|
// byte order belongs, so exifreader cannot parse it.
|
|
export const JPEG_WITH_BAD_EXIF = new Uint8Array([
|
|
...[0xff, 0xd8], // start of image
|
|
...[0xff, 0xe1, ...u16(2 + 6 + 2)], // APP1 and its length
|
|
...[...ascii("Exif"), 0], // "Exif\0\0"
|
|
...[0x58, 0x58], // "XX"
|
|
...[0xff, 0xda, 0x00, 0x02], // start of scan
|
|
]);
|