Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
# 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.