Files
2026-05-27 23:29:53 +01:00

14 KiB
Raw Permalink Blame History

EXIF completion — MakerNotes / ICC / GPS / Apple bplist / vendor ciphers

Package: @suaveplan/exif Status: PROPOSED Type: spec-conformance + feature

Why

The genesis port of @suaveplan/exif ships only the JPEG/PNG container readers, the IFD engine, IPTC, XMP, and basic EXIF tag extraction (packages/image/exif/src directory listing on 2026-05-02 returned: binary/, core/, errors/, exif/, format/, ifd/, iptc/, jpeg/, xmp/, types.ts, index.ts). The legacy package at ../suaveplan/packages/universal/exif/src ships an additional ~2 088 lines of MakerNotes parsers (18 vendor decoders), ICC profile read/write/validate (~624 lines), GPS conversion + timestamp helpers (~79 lines), and the Apple bplist parser (170 lines) plus Nikon (84 lines) and Sony (29 lines) cipher decoders. None of those modules are present in genesis.

Archive discrepancy (must be flagged in the proposal): openspec/archive/2026-04-19-enterprise-exif/tasks.md line 37 (- [x] 7.1 GPS: convert + timestamp), line 38 (- [x] 7.2 ICC: read / write / validate / convert (classifyProfile, isSRGB, isAdobeRGB, isDisplayP3, profileDescription)), and line 43 (- [x] 8.1 Dispatch + Apple bplist + Apple + Canon + Nikon (+ cipher) + Sony (+ cipher)) are all ticked. The archived spec at openspec/archive/2026-04-19-enterprise-exif/specs/exif/spec.md lines 21, 24, 29-30 lists corresponding requirements as accepted. The code never landed. This change closes that gap honestly and also extends the surface beyond Apple/Canon/Nikon/Sony to cover the full 18-vendor matrix shipped by the legacy package (Olympus, Fujifilm, Panasonic, Pentax, Casio, Kodak, Leica, Minolta, Ricoh, Samsung, Sigma, plus the dispatch shim and the generic decoder).

Genesis policy is "no half-baked or mediocre libraries" (CLAUDE.md §Workflow rule 3) and "Preserve the full public surface … never trim on 'probably unused' grounds" (CLAUDE.md §Workflow rule 5). Retroactive ticks on completed work are the failure mode AGENTS.md §13 explicitly guards against; this change exists because that gate failed.

What changes

@suaveplan/exif

  • ADDED: MakerNoteDecoder interface ({ vendor: string; canDecode(make: string, model?: string): boolean; decode(buffer: Uint8Array, byteOrder: ByteOrder, ifd: IfdEntry[]): MakerNoteData }) — pluggable contract for vendor decoders.
  • ADDED: 18 vendor decoders, one per file under src/makernotes/<vendor>/<vendor>.ts: apple, canon, casio, fujifilm, kodak, leica, minolta, nikon, olympus, panasonic, pentax, ricoh, samsung, sigma, sony, plus the dispatch and generic shims.
  • ADDED: decodeMakerNotes(buffer, byteOrder, makeTag, modelTag?) dispatcher — selects the right decoder by Make tag (case-insensitive prefix match) and falls back to the generic decoder when no vendor matches; returns MakerNoteData.
  • ADDED: decryptNikonMakerNote(buffer, serial, count) — Nikon MakerNote decipher per the legacy nikon-cipher.ts implementation (XOR + permutation table keyed by serial number and shutter count).
  • ADDED: decryptSonyMakerNote(buffer) — Sony MakerNote decipher per the legacy sony-cipher.ts implementation.
  • ADDED: parseBplist(bytes) — Apple binary property-list parser; returns a typed value tree ({ type: 'dict' | 'array' | 'string' | 'integer' | 'real' | 'boolean' | 'date' | 'data' | 'uid' | 'null'; value: … }); used by the Apple MakerNote decoder for HEIC and iPhone-shot JPEG.
  • ADDED: ICC profile module under src/icc/: parseIccProfile(bytes), writeIccProfile(profile), validateIccProfile(profile), plus the supporting parseIccHeader, parseIccTagTable, convertIccProfile (ports of icc/header/header.ts, icc/read/read.ts, icc/write/write.ts, icc/validate/validate.ts, icc/convert/convert.ts, icc/tag-table/tag-table.ts).
  • ADDED: ICC type definitions: IccProfile, IccHeader, IccTagEntry, IccProfileClass, IccColorSpace, IccRenderingIntent, plus the four-byte tag signatures (IccTagSignature enum).
  • ADDED: GPS module under src/gps/: gpsCoordinatesToDecimal({ lat, latRef, lon, lonRef }), decimalToGpsCoordinates(lat, lon), parseGpsTimestamp(date, time), gpsAltitudeToMeters(value, ref).
  • ADDED: error codes registered on @suaveplan/error under EXIF.*: MAKERNOTE_VENDOR_UNKNOWN, MAKERNOTE_DECRYPT_FAILED, MAKERNOTE_INVALID_HEADER, BPLIST_VERSION_UNSUPPORTED, BPLIST_TRAILER_INVALID, BPLIST_OBJECT_REF_OUT_OF_RANGE, ICC_HEADER_TOO_SHORT, ICC_PROFILE_TOO_SHORT, ICC_TAG_OFFSET_OUT_OF_RANGE, ICC_INVALID_SIGNATURE, ICC_VALIDATION_FAILED, GPS_INVALID_REF, GPS_OUT_OF_RANGE.
  • ADDED: subpath exports ./makernotes, ./icc, ./gps, ./bplist.
  • MODIFIED: readMetadata(buffer, options) — when the parsed EXIF IFD contains a MakerNote tag and an Make tag, the result's exif.makerNote field carries the decoded MakerNoteData instead of the raw bytes; existing callers that passed options.includeMakerNote: false keep the raw-bytes behaviour for back-compat.
  • MODIFIED: ReadOptions — adds includeMakerNote?: boolean (default true), includeIcc?: boolean (default true).
  • MODIFIED: ImageMetadata — adds optional icc?: IccProfile; populated when an ICC profile segment is present.

Out of scope

  • Writing MakerNotes — read-only is sufficient for the v1 surface. Round-tripping vendor MakerNotes requires verified per-vendor schemas and is its own change. Legacy upstream is also read-only.
  • RAW container support (CR2, NEF, ARW, RAF, RW2, ORF, DNG bodies beyond TIFF) — only JPEG and PNG are in scope. RAW handling lives behind the legacy formats/raw/dispatch module which is its own large surface; deferred to a separate change.
  • HEIC container parsing — the genesis package does not yet expose ISOBMFF/HEIC. Apple bplist support lands here because it is a MakerNote dependency (used inside JPEG MakerNote payloads on iPhone-shot JPEGs), but full HEIC container support is a separate change.
  • Adobe XMP-side ICC — XMP can carry ICC by reference; this change ships the binary ICC parser only. Cross-linking XMP xmp:Thumbnails references to ICC profiles is its own change.
  • Differential fuzz against exifr — the legacy archive's §9 differential harness is out of scope; the spec-conformance tests pin the implementation against KAT vectors instead.

Source mapping

This change derives from ../suaveplan/packages/universal/exif/src directly. There is no dedicated enterprise/exif.md audit doc upstream.

Requirement Source file Source lines Notes
MakerNoteDecoder interface and decodeMakerNotes dispatcher makernotes/dispatch/dispatch.ts L1-L78 Verbatim
Apple MakerNote decoder makernotes/apple/apple.ts L1-L126 Verbatim
Canon MakerNote decoder makernotes/canon/canon.ts L1-L139 Verbatim
Casio MakerNote decoder makernotes/casio/casio.ts L1-L6 Stub-with-vendor-detection (per legacy)
Fujifilm MakerNote decoder makernotes/fujifilm/fujifilm.ts L1-L130 Verbatim
Kodak MakerNote decoder makernotes/kodak/kodak.ts L1-L6 Stub-with-vendor-detection
Leica MakerNote decoder makernotes/leica/leica.ts L1-L6 Stub-with-vendor-detection
Minolta MakerNote decoder makernotes/minolta/minolta.ts L1-L6 Stub-with-vendor-detection
Nikon MakerNote decoder makernotes/nikon/nikon.ts L1-L155 Verbatim
Olympus MakerNote decoder makernotes/olympus/olympus.ts L1-L129 Verbatim
Panasonic MakerNote decoder makernotes/panasonic/panasonic.ts L1-L133 Verbatim
Pentax MakerNote decoder makernotes/pentax/pentax.ts L1-L140 Verbatim
Ricoh MakerNote decoder makernotes/ricoh/ricoh.ts L1-L6 Stub-with-vendor-detection
Samsung MakerNote decoder makernotes/samsung/samsung.ts L1-L9 Verbatim
Sigma MakerNote decoder makernotes/sigma/sigma.ts L1-L6 Stub-with-vendor-detection
Sony MakerNote decoder makernotes/sony/sony.ts L1-L14 Verbatim
Generic MakerNote decoder makernotes/generic/generic.ts L1-L13 Verbatim
decryptNikonMakerNote makernotes/nikon-cipher/nikon-cipher.ts L1-L84 Verbatim
decryptSonyMakerNote makernotes/sony-cipher/sony-cipher.ts L1-L29 Verbatim
parseBplist makernotes/apple-bplist/apple-bplist.ts L1-L170 Verbatim
parseIccHeader icc/header/header.ts L1-L121 Verbatim
parseIccProfile icc/read/read.ts L1-L165 Verbatim
writeIccProfile icc/write/write.ts L1-L187 Verbatim
validateIccProfile icc/validate/validate.ts L1-L37 Verbatim
convertIccProfile (classifyProfile, isSRGB, isAdobeRGB, isDisplayP3, profileDescription) icc/convert/convert.ts L1-L51 Verbatim
ICC tag-table signatures icc/tag-table/tag-table.ts L1-L63 Verbatim
gpsCoordinatesToDecimal, decimalToGpsCoordinates, gpsAltitudeToMeters gps/convert/convert.ts L1-L41 Verbatim
parseGpsTimestamp gps/timestamp/timestamp.ts L1-L38 Verbatim

Requirements with no source line

Requirement Justification
EXIF.* error codes registered on @suaveplan/error Genesis-wide policy that every thrown error route through @suaveplan/error (CLAUDE.md §Workflow rule 2). Upstream throws plain Error.
./makernotes, ./icc, ./gps, ./bplist subpath exports Genesis-wide subpath-per-feature rule (AGENTS.md §6). Upstream uses a single barrel; tree-shaking parity demands subpath splits.
includeMakerNote / includeIcc ReadOptions flags Back-compat: legacy upstream behaviour is "always decode if present"; genesis ships flags so callers who don't need MakerNotes can skip the decode work. Default-on preserves legacy behaviour.

Verification approach

Each spec delta requirement carries a **Verified by:** line pointing at concrete it(...) titles. The test files that will exist after this change ships:

  • packages/image/exif/src/makernotes/dispatch/dispatch.test.ts — vendor selection by Make tag; fall-through to generic decoder
  • packages/image/exif/src/makernotes/dispatch/dispatch.spec-conformance.test.ts — MakerNoteDecoder interface contract pinned for every vendor; MAKERNOTE_VENDOR_UNKNOWN raised when caller demands strict mode
  • packages/image/exif/src/makernotes/apple/apple.test.ts — known iPhone JPEG fixture decodes; bplist payload extracted
  • packages/image/exif/src/makernotes/canon/canon.test.ts — known 5D Mark IV fixture decodes; lens info / shooting modes extracted
  • packages/image/exif/src/makernotes/nikon/nikon.test.ts — known D850 fixture decodes; encrypted block decoded via injected cipher
  • packages/image/exif/src/makernotes/sony/sony.test.ts — known α7 III fixture decodes; encrypted block decoded via injected cipher
  • packages/image/exif/src/makernotes/olympus/olympus.test.ts, fujifilm/fujifilm.test.ts, panasonic/panasonic.test.ts, pentax/pentax.test.ts, samsung/samsung.test.ts — one happy-path fixture each
  • packages/image/exif/src/makernotes/casio/casio.test.ts, kodak/kodak.test.ts, leica/leica.test.ts, minolta/minolta.test.ts, ricoh/ricoh.test.ts, sigma/sigma.test.ts — vendor-detection-only assertions (matches legacy stub coverage)
  • packages/image/exif/src/makernotes/generic/generic.test.ts — fall-through path emits raw IFD entries
  • packages/image/exif/src/makernotes/nikon-cipher/nikon-cipher.test.ts — KAT vectors against legacy implementation
  • packages/image/exif/src/makernotes/sony-cipher/sony-cipher.test.ts — KAT vectors against legacy implementation
  • packages/image/exif/src/makernotes/apple-bplist/apple-bplist.test.ts — bplist00 v0/v1 fixture; parseBplist returns the expected typed value tree
  • packages/image/exif/src/makernotes/apple-bplist/apple-bplist.spec-conformance.test.ts — BPLIST_VERSION_UNSUPPORTED, BPLIST_TRAILER_INVALID, BPLIST_OBJECT_REF_OUT_OF_RANGE all raised on crafted inputs
  • packages/image/exif/src/icc/read/read.test.ts — sRGB and Display-P3 fixtures parse; IccProfile.tags populated
  • packages/image/exif/src/icc/write/write.test.ts — round-trip: writeIccProfile(parseIccProfile(bytes)) is byte-equal
  • packages/image/exif/src/icc/validate/validate.test.ts — valid profiles pass; truncated profiles emit ICC_HEADER_TOO_SHORT / ICC_PROFILE_TOO_SHORT; bad tag offsets emit ICC_TAG_OFFSET_OUT_OF_RANGE
  • packages/image/exif/src/icc/convert/convert.test.ts — classifyProfile, isSRGB, isAdobeRGB, isDisplayP3 against known fixtures
  • packages/image/exif/src/icc/header/header.spec-conformance.test.ts — parseIccHeader boundary checks; ICC_HEADER_TOO_SHORT raised on short input
  • packages/image/exif/src/gps/convert/convert.test.ts — round-trip: decimal → DMS → decimal within 1e-9; altitude positive vs negative ref
  • packages/image/exif/src/gps/convert/convert.spec-conformance.test.ts — GPS_INVALID_REF and GPS_OUT_OF_RANGE raised on crafted inputs
  • packages/image/exif/src/gps/timestamp/timestamp.test.ts — date+time → Date round-trip; midnight/midday boundaries
  • tests/dep-graph.test.ts — subpath exports ./makernotes, ./icc, ./gps, ./bplist resolve

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/exif to enumerate. Result of grep -rl "@suaveplan/exif" /home/ubuntu/web/genesis/packages/ --include="package.json" on 2026-05-02 returned only the package itself (packages/image/exif/package.json). No in-repo dependents.

  • No dependents (verified by grep -rl "@suaveplan/exif" /home/ubuntu/web/genesis/packages/ --include="package.json" returning only the package's own package.json).

This change is additive on its public surface — every existing export is preserved and the readMetadata return shape gains optional fields (exif.makerNote decoded payload, icc block) without breaking existing callers. Callers that currently treat exif.makerNote as raw bytes need to either set options.includeMakerNote: false (gets old behaviour) or update their type expectations to the new MakerNoteData discriminated union — flagged as a behaviour change in the migration table for any future consumer that lands.