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
Collaborator

Implements #145.

photo.exif() and quak backup-metadata --exif now read EXIF with exifreader, so HEIC/HEIF originals get it, a live photo's image included. exifreader replaces exif-reader and extractExifFromJpeg. PhotoExif is unchanged.

Both callers use readExifTags (src/exif.ts), which asks exifreader for EXIF tags only, unnamed ones included, so a broken XMP or ICC block cannot cost a file its EXIF. A file exifreader cannot read, such as a video, has no EXIF. A text tag whose value lies outside the file gives no field.

The fixture test/exif.heic holds the same values as the library tests' hand-built JPEG; test/exif-heic.ts says how it was made. Each input the deleted scan tests used is now checked against readPhotoExif.

  • Licence: exifreader is MPL-2.0, which permits unmodified use from WTFPL code.
  • Format change: exifRaw holds the whole EXIF block exifreader found (for a JPEG, the APP1 segment, marker and length included). A tag exifreader has no name for is keyed undefined- plus its number; the thumbnail's tags are not recorded.
  • Judgement call: a latitude or longitude without its hemisphere reference tag is left out.
  • Judgement call: exifError is set when exifreader finds an EXIF block but reads no tag from it; a JPEG broken before any EXIF block gets none, where the old scan named the bad segment.
  • Judgement call: a date the JavaScript date parser rejects, such as 0000:00:00 00:00:00 or a month above 12, gives no dateTimeOriginal. One it rolls over, such as 2021:02:30 or hour 24, gives the rolled-over date.

Model: opus-5-5

Implements https://git.eeqj.de/sneak/quak/issues/145. `photo.exif()` and `quak backup-metadata --exif` now read EXIF with `exifreader`, so HEIC/HEIF originals get it, a live photo's image included. `exifreader` replaces `exif-reader` and `extractExifFromJpeg`. `PhotoExif` is unchanged. Both callers use `readExifTags` (`src/exif.ts`), which asks `exifreader` for EXIF tags only, unnamed ones included, so a broken XMP or ICC block cannot cost a file its EXIF. A file `exifreader` cannot read, such as a video, has no EXIF. A text tag whose value lies outside the file gives no field. The fixture `test/exif.heic` holds the same values as the library tests' hand-built JPEG; `test/exif-heic.ts` says how it was made. Each input the deleted scan tests used is now checked against `readPhotoExif`. - Licence: `exifreader` is MPL-2.0, which permits unmodified use from WTFPL code. - Format change: `exifRaw` holds the whole EXIF block `exifreader` found (for a JPEG, the APP1 segment, marker and length included). A tag `exifreader` has no name for is keyed `undefined-` plus its number; the thumbnail's tags are not recorded. - Judgement call: a latitude or longitude without its hemisphere reference tag is left out. - Judgement call: `exifError` is set when `exifreader` finds an EXIF block but reads no tag from it; a JPEG broken before any EXIF block gets none, where the old scan named the bad segment. - Judgement call: a date the JavaScript date parser rejects, such as `0000:00:00 00:00:00` or a month above 12, gives no `dateTimeOriginal`. One it rolls over, such as `2021:02:30` or hour 24, gives the rolled-over date. Model: opus-5-5
clawbot added the needs-review label 2026-10-01 20:31:56 +02:00
clawbot self-assigned this 2026-10-01 20:31:56 +02:00
clawbot added 1 commit 2026-10-01 20:31:57 +02:00
photo.exif() and backup-metadata --exif read EXIF with exifreader,
which reads JPEG, HEIC/HEIF, AVIF, PNG and WebP, so a HEIC original or
a live photo's HEIC image gets its EXIF. exifreader replaces
exif-reader and the hand-written JPEG segment scan. PhotoExif is
unchanged. The exif field of backup-metadata --exif is now
exifreader's tag output, and exifRaw holds the whole EXIF block it
could not read. Tests use a real 933-byte HEIC fixture, test/exif.heic,
whose recipe is in test/exif-heic.ts.

Model: opus-5-5
Author
Collaborator

Review: fail.

  1. README.md:503-504: "Dumps written before 2026-10-01 hold exif-reader's Image, Photo and GPSInfo groups there instead." describes an old dump layout. quak is pre-1.0 and has no dumps to describe, so docs about an old layout are a defect. Acceptable: the README describes only the current exif format, which the sentence before it already does. Drop this sentence.

  2. src/exif.ts:71: the PR's third judgement call is that a date that is not a real calendar date, such as 0000:00:00 00:00:00 (what cameras with an unset clock write), gives no dateTimeOriginal. That check is new code, and no test covers it. Without it, exif() would return an Invalid Date. Acceptable: a test that EXIF with DateTimeOriginal 0000:00:00 00:00:00 gives no dateTimeOriginal.

Judgement call: the brief asked for the exif format change to be stated in the README. I read that as asking for the current format to be stated, not the old one.

Model: opus-5-5

Review: fail. 1. `README.md:503-504`: "Dumps written before 2026-10-01 hold `exif-reader`'s `Image`, `Photo` and `GPSInfo` groups there instead." describes an old dump layout. quak is pre-1.0 and has no dumps to describe, so docs about an old layout are a defect. Acceptable: the README describes only the current `exif` format, which the sentence before it already does. Drop this sentence. 2. `src/exif.ts:71`: the PR's third judgement call is that a date that is not a real calendar date, such as `0000:00:00 00:00:00` (what cameras with an unset clock write), gives no `dateTimeOriginal`. That check is new code, and no test covers it. Without it, `exif()` would return an Invalid Date. Acceptable: a test that EXIF with `DateTimeOriginal` `0000:00:00 00:00:00` gives no `dateTimeOriginal`. Judgement call: the brief asked for the `exif` format change to be stated in the README. I read that as asking for the current format to be stated, not the old one. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-01 20:51:29 +02:00
clawbot added 1 commit 2026-10-01 21:15:02 +02:00
Add a readPhotoExif test: EXIF whose DateTimeOriginal is
0000:00:00 00:00:00, as a camera with an unset clock writes it, gives no
dateTimeOriginal while its other fields are still read. Remove the README
sentence describing the backup's exif field in dumps from an earlier
version.

Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-01 21:22:24 +02:00
Author
Collaborator

Review: fail.

  1. src/exif.ts:94: gpsAltitude now comes from exifreader's computed position, which exifreader sets only when GPSAltitudeRef is present. A file with GPSAltitude and no GPSAltitudeRef therefore loses its altitude. The code on next returned it as above sea level, which is the EXIF standard's default for that tag, and README.md:725 says each field is absent only when the file lacks it. Leaving out latitude and longitude that have no reference tag is fine, since the standard gives those no default. Acceptable: altitude is read from the GPSAltitude tag and negated only when GPSAltitudeRef is 1, with a test of a file that has GPSAltitude and no GPSAltitudeRef.

  2. src/exif.ts:3 says backup-metadata --exif records every EXIF tag it finds. readExifTags drops the tags exifreader has no name for, and the thumbnail IFD's tags, though the dump on next kept both. src/exif.ts:12 says each tag's computed is a plain string or number, but a tag with several values computes to an array: GPSLatitude gives [40, 26, 46]. Acceptable: both comments say what the code does. If the dump is meant to keep every tag, readExifTags passes includeUnknown: true instead.

  3. PR body, third judgement call: it says a date that is not a real calendar date now gives no dateTimeOriginal. 2021:02:30 10:00:00 still gives 2021-03-02, because asDate (src/exif.ts:64-72) leaves day overflow to the Date parser. Acceptable: the PR body names only the dates that are actually dropped, those with a zero or out-of-range field such as 0000:00:00 00:00:00.

Model: opus-5-5

Review: fail. 1. `src/exif.ts:94`: `gpsAltitude` now comes from exifreader's computed position, which exifreader sets only when `GPSAltitudeRef` is present. A file with `GPSAltitude` and no `GPSAltitudeRef` therefore loses its altitude. The code on `next` returned it as above sea level, which is the EXIF standard's default for that tag, and `README.md:725` says each field is absent only when the file lacks it. Leaving out latitude and longitude that have no reference tag is fine, since the standard gives those no default. Acceptable: altitude is read from the `GPSAltitude` tag and negated only when `GPSAltitudeRef` is 1, with a test of a file that has `GPSAltitude` and no `GPSAltitudeRef`. 2. `src/exif.ts:3` says `backup-metadata --exif` records every EXIF tag it finds. `readExifTags` drops the tags exifreader has no name for, and the thumbnail IFD's tags, though the dump on `next` kept both. `src/exif.ts:12` says each tag's `computed` is a plain string or number, but a tag with several values computes to an array: `GPSLatitude` gives `[40, 26, 46]`. Acceptable: both comments say what the code does. If the dump is meant to keep every tag, `readExifTags` passes `includeUnknown: true` instead. 3. PR body, third judgement call: it says a date that is not a real calendar date now gives no `dateTimeOriginal`. `2021:02:30 10:00:00` still gives 2021-03-02, because `asDate` (`src/exif.ts:64-72`) leaves day overflow to the `Date` parser. Acceptable: the PR body names only the dates that are actually dropped, those with a zero or out-of-range field such as `0000:00:00 00:00:00`. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-01 21:33:23 +02:00
Author
Collaborator

Rework decision on finding 2 of the review above: readExifTags passes includeUnknown: true, so the backup-metadata --exif dump keeps tags exifreader has no name for. Both comments in src/exif.ts then say exactly what the code does, including that the thumbnail's tags are not kept and that a tag with several values computes to an array. Findings 1 and 3 are fixed as their "Acceptable" sentences say.

Model: opus-5-5

Rework decision on finding 2 of the review above: `readExifTags` passes `includeUnknown: true`, so the `backup-metadata --exif` dump keeps tags `exifreader` has no name for. Both comments in `src/exif.ts` then say exactly what the code does, including that the thumbnail's tags are not kept and that a tag with several values computes to an array. Findings 1 and 3 are fixed as their "Acceptable" sentences say. Model: opus-5-5
clawbot added 1 commit 2026-10-01 21:35:54 +02:00
exif() takes the altitude from the GPSAltitude tag and negates it only when
GPSAltitudeRef is 1, so a file with GPSAltitude and no GPSAltitudeRef keeps its
altitude, above sea level. A test covers that file.

backup-metadata --exif keeps the EXIF tags exifreader has no name for. The
comments in src/exif.ts say the thumbnail's tags are not kept and that a tag
with several values computes to an array.

Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-01 21:40:28 +02:00
Author
Collaborator

Review: fail.

  1. src/exif.ts:87-91: when a text tag's value lies outside the file (a bad offset or count), exifreader returns the placeholder string "<faulty value>", and readPhotoExif passes it on as make, model, lensModel or offsetTimeOriginal. A JPEG whose Make offset points past the end of the file now gets that string as its make; on next the field was absent. Acceptable: a tag exifreader could not read gives no field, as the README's "absent when the file lacks it" says, with a test of such a file.

  2. src/exif.ts:25: includeUnknown: true, decided in #147 (comment) so that the backup-metadata --exif dump keeps the tags exifreader has no name for, is not covered by any test. Acceptable: a test in test/cli/metadata-exif.test.ts that extractImageMetadata records a tag exifreader has no name for, keyed undefined-<tag number>.

Model: opus-5-5

Review: fail. 1. `src/exif.ts:87-91`: when a text tag's value lies outside the file (a bad offset or count), exifreader returns the placeholder string "&lt;faulty value&gt;", and `readPhotoExif` passes it on as `make`, `model`, `lensModel` or `offsetTimeOriginal`. A JPEG whose `Make` offset points past the end of the file now gets that string as its make; on `next` the field was absent. Acceptable: a tag exifreader could not read gives no field, as the README's "absent when the file lacks it" says, with a test of such a file. 2. `src/exif.ts:25`: `includeUnknown: true`, decided in https://git.eeqj.de/sneak/quak/pulls/147#issuecomment-108406 so that the `backup-metadata --exif` dump keeps the tags exifreader has no name for, is not covered by any test. Acceptable: a test in `test/cli/metadata-exif.test.ts` that `extractImageMetadata` records a tag exifreader has no name for, keyed undefined-&lt;tag number&gt;. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-01 21:49:09 +02:00
clawbot added 1 commit 2026-10-01 21:59:41 +02:00
A text tag whose value lies outside the file, which exifreader reads as
"<faulty value>", no longer gives a make, model, lens model or offset
field. Tests cover such a JPEG and the backup-metadata dump keying a tag
exifreader has no name for as undefined-<tag number>.

Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-01 22:00:07 +02:00
Author
Collaborator

Review: fail.

  1. README.md:728-730: the read surface says EXIF is read from "any image format exifreader reads: JPEG, HEIC/HEIF, AVIF, PNG and WebP", and that any other original gives {}. exifreader also reads TIFF, and so the TIFF-based raw originals such as DNG, plus JPEG XL. exif() returns their fields: a TIFF original gives its orientation. The issue's definition of done asks the README to state the formats that are read. Acceptable: the README names every format exif() reads, or presents the list as examples, so that "any other original gives {}" is true.

  2. PR body, last judgement call: it counts an out-of-range hour among the readings that give no dateTimeOriginal, but 2021:07:15 24:00:00 gives 2021-07-16 00:00 UTC. Acceptable: either the body stops counting hour 24 as out of range, or the code also drops hour 24 and a test covers it.

Model: opus-5-5

Review: fail. 1. `README.md:728-730`: the read surface says EXIF is read from "any image format exifreader reads: JPEG, HEIC/HEIF, AVIF, PNG and WebP", and that any other original gives `{}`. exifreader also reads TIFF, and so the TIFF-based raw originals such as DNG, plus JPEG XL. `exif()` returns their fields: a TIFF original gives its orientation. The issue's definition of done asks the README to state the formats that are read. Acceptable: the README names every format `exif()` reads, or presents the list as examples, so that "any other original gives `{}`" is true. 2. PR body, last judgement call: it counts an out-of-range hour among the readings that give no `dateTimeOriginal`, but `2021:07:15 24:00:00` gives 2021-07-16 00:00 UTC. Acceptable: either the body stops counting hour 24 as out of range, or the code also drops hour 24 and a test covers it. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-01 22:12:14 +02:00
clawbot added 1 commit 2026-10-01 22:23:41 +02:00
The read surface listed JPEG, HEIC/HEIF, AVIF, PNG and WebP as every
format exif() reads, but exifreader reads more, so "any other original
gives {}" was false. The list is now examples.

Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-01 22:24:30 +02:00
Author
Collaborator

Review: pass.

Model: opus-5-5

Review: pass. Model: opus-5-5
clawbot merged commit 67d554fb46 into next 2026-10-01 22:34:09 +02:00
clawbot deleted branch issue-145-heif-exif 2026-10-01 22:34:10 +02:00
Sign in to join this conversation.