exif(): read HEIF/HEIC originals, not only JPEG #145

Closed
opened 2026-10-01 20:06:01 +02:00 by clawbot · 2 comments
Collaborator

Owner's words, on the milestone PR (#134 (comment), 2026-10-01 17:56 UTC):

this work never should have landed on next. HEIF support for exif is table stakes.

"This work" is photo.exif(), which landed on next from #142 and reads EXIF from JPEG only. This also answers question 3 of #141 (comment): HEIF is required now, not as a follow-up.

Definition of done:

  • photo.exif() returns the same common fields for a HEIC/HEIF original as for a JPEG, a live photo's HEIC image included.
  • quak backup-metadata --exif also reads EXIF from HEIC/HEIF originals.
  • EXIF is read with one widely used, maintained library, not a hand-written parser per format.
  • Tests use a real HEIC file carrying EXIF (small, committed as a fixture) as well as JPEG. README states the formats that are read.
  • The check gate is green on next. One PR to next, independently reviewed and squash-merged.

Model: opus-5-5

Owner's words, on the milestone PR (https://git.eeqj.de/sneak/quak/pulls/134#issuecomment-107590, 2026-10-01 17:56 UTC): > this work never should have landed on next. HEIF support for exif is table stakes. "This work" is `photo.exif()`, which landed on `next` from https://git.eeqj.de/sneak/quak/pulls/142 and reads EXIF from JPEG only. This also answers question 3 of https://git.eeqj.de/sneak/quak/issues/141#issuecomment-107489: HEIF is required now, not as a follow-up. Definition of done: - `photo.exif()` returns the same common fields for a HEIC/HEIF original as for a JPEG, a live photo's HEIC image included. - `quak backup-metadata --exif` also reads EXIF from HEIC/HEIF originals. - EXIF is read with one widely used, maintained library, not a hand-written parser per format. - Tests use a real HEIC file carrying EXIF (small, committed as a fixture) as well as JPEG. README states the formats that are read. - The check gate is green on `next`. One PR to `next`, independently reviewed and squash-merged. Model: opus-5-5
clawbot self-assigned this 2026-10-01 20:06:01 +02:00
Author
Collaborator

Implementer's brief. Move all EXIF reading to exifreader (https://github.com/mattiasw/ExifReader). It is actively maintained, widely used, and reads JPEG, HEIC/HEIF, AVIF, PNG and WebP. exifr is used more but has not been published since 2022. exifreader is MPL-2.0, which permits using it unmodified from WTFPL code; say so in one line in the PR.

src/exif.ts.

  • readPhotoExif(bytes) reads the same 13 fields through exifreader, for every format it supports.
  • Keep PhotoExif's shape and value types exactly as they are:
    • dateTimeOriginal keeps today's meaning: the camera's clock in the Date's UTC fields;
    • GPS values stay signed decimal degrees and metres;
    • iso keeps its current rule.
  • A file with no EXIF, or with EXIF that cannot be read, still returns {}.
  • A video still returns {} without downloading.
  • Confirm each tag name and value type against exifreader's own types.

quak backup-metadata --exif (src/metadata-backup.ts) reads EXIF through exifreader too, so HEIC/HEIF originals get it.

  • The exif object in its JSON becomes exifreader's tag output. Say that format change in one line in the PR and in the README.
  • Keep exifError for a block that cannot be read.
  • Keep the jpeg-js dimensions and the XMP scan unchanged.

Removals. Remove extractExifFromJpeg and the exif-reader dependency once nothing uses them. Pin exifreader exactly, as package.json pins everything else.

Tests.

  • Commit one small real HEIC file that carries EXIF, kept under 100 KB, as a fixture.
    • Make it in a throwaway Docker container (for example libheif and exiftool on Alpine), and remove the container afterwards.
    • Say in a comment beside the fixture how it was made.
  • exif() on that HEIC returns the expected fields, and so does a live photo whose image is that HEIC.
  • The existing JPEG cases still pass unchanged.
  • quak backup-metadata --exif records EXIF for the HEIC.
  • Every behaviour the deleted extractExifFromJpeg tests guarded keeps a test against the new code: a non-image has no EXIF, and malformed EXIF yields {} or exifError. Never weaken an assertion.

Docs. README "Read surface" lists the formats exif() reads, and the CLI section covers --exif.

Process

  • Branch from current next, with one PR whose base is next. Title: exif(): read HEIF/HEIC originals with exifreader (closes #145). Label it needs-review and assign clawbot.
  • #143 is being built at the same time and touches src/library/read.ts. Stay out of that file except for exif(), and rebase onto next before pushing.
  • Gate only with make check, and format only with make fmt. Both run in Docker. Run Docker builds under flock -w 1800 /srv/code/tmp/.docker-build.lock. A timeout there exits 1 without running anything; retry, and never read it as red. Remove every container you start.
  • Edit files by hand. No sed -i, perl -pi, awk or scripted replacements.
  • Keep the change small and plain enough for a newcomer to follow in one reading. Coin no new terms.
  • Ask no interactive questions.
  • Commits and the PR body end with a Model: line naming your model id. Never name the company.
  • Stay under 2 GiB of RAM.

Model: opus-5-5

**Implementer's brief.** Move all EXIF reading to `exifreader` (https://github.com/mattiasw/ExifReader). It is actively maintained, widely used, and reads JPEG, HEIC/HEIF, AVIF, PNG and WebP. `exifr` is used more but has not been published since 2022. `exifreader` is MPL-2.0, which permits using it unmodified from WTFPL code; say so in one line in the PR. **`src/exif.ts`.** - `readPhotoExif(bytes)` reads the same 13 fields through `exifreader`, for every format it supports. - Keep `PhotoExif`'s shape and value types exactly as they are: - `dateTimeOriginal` keeps today's meaning: the camera's clock in the `Date`'s UTC fields; - GPS values stay signed decimal degrees and metres; - `iso` keeps its current rule. - A file with no EXIF, or with EXIF that cannot be read, still returns `{}`. - A video still returns `{}` without downloading. - Confirm each tag name and value type against `exifreader`'s own types. **`quak backup-metadata --exif`** (`src/metadata-backup.ts`) reads EXIF through `exifreader` too, so HEIC/HEIF originals get it. - The `exif` object in its JSON becomes `exifreader`'s tag output. Say that format change in one line in the PR and in the README. - Keep `exifError` for a block that cannot be read. - Keep the `jpeg-js` dimensions and the XMP scan unchanged. **Removals.** Remove `extractExifFromJpeg` and the `exif-reader` dependency once nothing uses them. Pin `exifreader` exactly, as `package.json` pins everything else. **Tests.** - Commit one small real HEIC file that carries EXIF, kept under 100 KB, as a fixture. - Make it in a throwaway Docker container (for example `libheif` and `exiftool` on Alpine), and remove the container afterwards. - Say in a comment beside the fixture how it was made. - `exif()` on that HEIC returns the expected fields, and so does a live photo whose image is that HEIC. - The existing JPEG cases still pass unchanged. - `quak backup-metadata --exif` records EXIF for the HEIC. - Every behaviour the deleted `extractExifFromJpeg` tests guarded keeps a test against the new code: a non-image has no EXIF, and malformed EXIF yields `{}` or `exifError`. Never weaken an assertion. **Docs.** README "Read surface" lists the formats `exif()` reads, and the CLI section covers `--exif`. **Process** - Branch from current `next`, with one PR whose base is `next`. Title: `exif(): read HEIF/HEIC originals with exifreader (closes #145)`. Label it `needs-review` and assign `clawbot`. - https://git.eeqj.de/sneak/quak/issues/143 is being built at the same time and touches `src/library/read.ts`. Stay out of that file except for `exif()`, and rebase onto `next` before pushing. - Gate only with `make check`, and format only with `make fmt`. Both run in Docker. Run Docker builds under `flock -w 1800 /srv/code/tmp/.docker-build.lock`. A timeout there exits 1 without running anything; retry, and never read it as red. Remove every container you start. - Edit files by hand. No `sed -i`, `perl -pi`, `awk` or scripted replacements. - Keep the change small and plain enough for a newcomer to follow in one reading. Coin no new terms. - Ask no interactive questions. - Commits and the PR body end with a `Model:` line naming your model id. Never name the company. - Stay under 2 GiB of RAM. Model: opus-5-5
Author
Collaborator

Owner ruling on the EXIF shape (chat, 2026-10-01 ~19:05 UTC): both. exif() returns everything, and one async accessor per field calls exif() and returns that value. Filed as #148, to land after this one or together with it. Keep this issue's field list in one place that both can use.

model: opus-5-5

Owner ruling on the EXIF shape (chat, 2026-10-01 ~19:05 UTC): both. `exif()` returns everything, and one async accessor per field calls `exif()` and returns that value. Filed as https://git.eeqj.de/sneak/quak/issues/148, to land after this one or together with it. Keep this issue's field list in one place that both can use. model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/quak#145