Initial Commit
This commit is contained in:
120
openspec/changes/exif-completion/proposal.md
Normal file
120
openspec/changes/exif-completion/proposal.md
Normal 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.
|
||||
165
openspec/changes/exif-completion/specs/exif/spec.md
Normal file
165
openspec/changes/exif-completion/specs/exif/spec.md
Normal file
@@ -0,0 +1,165 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: exif (exif-completion)
|
||||
|
||||
**Package**: `@suaveplan/exif`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` Section 13.)
|
||||
>
|
||||
> **Note on prior archive:** several requirements below were also listed in `openspec/archive/2026-04-19-enterprise-exif/specs/exif/spec.md` (lines 21, 24, 29-30) and ticked `[x]` in that archive's `tasks.md` (lines 37, 38, 43), but the supporting code never landed. This delta backs those claims with the actual implementation.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MakerNotes registry and dispatcher
|
||||
|
||||
The package SHALL expose a `MakerNoteDecoder` interface and a `decodeMakerNotes(buffer, byteOrder, makeTag, modelTag?)` dispatcher that selects the right vendor decoder by case-insensitive prefix match against the EXIF `Make` tag and falls through to a generic decoder when no vendor matches. The package SHALL ship one decoder per file under `src/makernotes/<vendor>/<vendor>.ts` for the 18 vendors covered by the legacy package: `apple`, `canon`, `casio`, `fujifilm`, `kodak`, `leica`, `minolta`, `nikon`, `olympus`, `panasonic`, `pentax`, `ricoh`, `samsung`, `sigma`, `sony`, plus the `dispatch` and `generic` shims.
|
||||
|
||||
#### Scenario: dispatcher selects the right vendor decoder
|
||||
|
||||
- **WHEN** `decodeMakerNotes(buffer, byteOrder, "NIKON CORPORATION")` is called against a Nikon MakerNote payload
|
||||
- **THEN** the Nikon decoder is invoked and the returned `MakerNoteData.vendor` is `"nikon"`
|
||||
|
||||
#### Scenario: unknown vendor falls through to generic decoder
|
||||
|
||||
- **WHEN** `decodeMakerNotes(buffer, byteOrder, "UNKNOWN-MAKE")` is called and no vendor's `canDecode` returns true
|
||||
- **THEN** the generic decoder is invoked and the returned `MakerNoteData.vendor` is `"generic"`
|
||||
|
||||
#### Scenario: strict mode raises a stable error code on unknown vendor
|
||||
|
||||
- **WHEN** `decodeMakerNotes(buffer, byteOrder, "UNKNOWN-MAKE", undefined, { strict: true })` is called
|
||||
- **THEN** it throws `SuaveplanError` with code `EXIF.MAKERNOTE_VENDOR_UNKNOWN`
|
||||
|
||||
**Verified by:** `src/makernotes/dispatch/dispatch.test.ts::"selects the Nikon decoder for Make='NIKON CORPORATION'"` AND `src/makernotes/dispatch/dispatch.test.ts::"falls through to generic decoder for unknown Make"` AND `src/makernotes/dispatch/dispatch.spec-conformance.test.ts::"raises EXIF.MAKERNOTE_VENDOR_UNKNOWN in strict mode"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nikon cipher decoder
|
||||
|
||||
The package SHALL expose `decryptNikonMakerNote(buffer, serial, count)` that decrypts the encrypted block of a Nikon MakerNote per the legacy `nikon-cipher.ts` algorithm: an XOR + permutation table keyed by the camera serial number and the shutter count. Output bytes match the legacy implementation byte-for-byte against KAT vectors.
|
||||
|
||||
#### Scenario: KAT vector decrypts to expected plaintext
|
||||
|
||||
- **WHEN** `decryptNikonMakerNote(ciphertext, serial=0x12345678, count=0x9ABCDEF0)` is called for a fixture extracted from the legacy test corpus
|
||||
- **THEN** the returned bytes are byte-equal to the legacy plaintext
|
||||
|
||||
#### Scenario: malformed cipher input raises a stable error code
|
||||
|
||||
- **WHEN** `decryptNikonMakerNote` is called against a buffer too short to carry the encrypted block
|
||||
- **THEN** it throws `SuaveplanError` with code `EXIF.MAKERNOTE_DECRYPT_FAILED`
|
||||
|
||||
**Verified by:** `src/makernotes/nikon-cipher/nikon-cipher.test.ts::"matches legacy KAT vector for serial 0x12345678 / count 0x9ABCDEF0"` AND `src/makernotes/nikon-cipher/nikon-cipher.test.ts::"throws EXIF.MAKERNOTE_DECRYPT_FAILED on truncated input"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Sony cipher decoder
|
||||
|
||||
The package SHALL expose `decryptSonyMakerNote(buffer)` that decrypts the encrypted block of a Sony MakerNote per the legacy `sony-cipher.ts` algorithm. Output bytes match the legacy implementation byte-for-byte against KAT vectors.
|
||||
|
||||
#### Scenario: KAT vector decrypts to expected plaintext
|
||||
|
||||
- **WHEN** `decryptSonyMakerNote(ciphertext)` is called for a fixture extracted from the legacy test corpus
|
||||
- **THEN** the returned bytes are byte-equal to the legacy plaintext
|
||||
|
||||
#### Scenario: malformed cipher input raises a stable error code
|
||||
|
||||
- **WHEN** `decryptSonyMakerNote` is called against a buffer too short to carry the encrypted block
|
||||
- **THEN** it throws `SuaveplanError` with code `EXIF.MAKERNOTE_DECRYPT_FAILED`
|
||||
|
||||
**Verified by:** `src/makernotes/sony-cipher/sony-cipher.test.ts::"matches legacy KAT vector"` AND `src/makernotes/sony-cipher/sony-cipher.test.ts::"throws EXIF.MAKERNOTE_DECRYPT_FAILED on truncated input"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Apple binary property-list parser
|
||||
|
||||
The package SHALL expose `parseBplist(bytes)` that parses a binary property-list (`bplist00` v0 and v1 magic) into a typed `BplistValue` tree. `BplistValue` is a discriminated union over `dict | array | string | integer | real | boolean | date | data | uid | null` carrying its decoded value. The parser is used by the Apple MakerNote decoder for HEIC and iPhone-shot JPEG payloads.
|
||||
|
||||
#### Scenario: known fixture parses into the expected typed tree
|
||||
|
||||
- **WHEN** `parseBplist(bytes)` is called against a known iPhone JPEG MakerNote bplist fixture
|
||||
- **THEN** the returned `BplistValue` is a `dict` whose keys/values match the legacy implementation's output
|
||||
|
||||
#### Scenario: unsupported version raises a stable error code
|
||||
|
||||
- **WHEN** `parseBplist(bytes)` is called against a buffer whose magic is `bplist02` or anything other than `bplist00` v0/v1
|
||||
- **THEN** it throws `SuaveplanError` with code `EXIF.BPLIST_VERSION_UNSUPPORTED`
|
||||
|
||||
#### Scenario: truncated trailer raises a stable error code
|
||||
|
||||
- **WHEN** `parseBplist(bytes)` is called against a buffer whose 32-byte trailer is missing or malformed
|
||||
- **THEN** it throws `SuaveplanError` with code `EXIF.BPLIST_TRAILER_INVALID`
|
||||
|
||||
#### Scenario: out-of-range object reference raises a stable error code
|
||||
|
||||
- **WHEN** the bplist trailer claims more objects than the offset table can address
|
||||
- **THEN** `parseBplist` throws `SuaveplanError` with code `EXIF.BPLIST_OBJECT_REF_OUT_OF_RANGE`
|
||||
|
||||
**Verified by:** `src/makernotes/apple-bplist/apple-bplist.test.ts::"parses iPhone JPEG MakerNote bplist into expected typed tree"` AND `src/makernotes/apple-bplist/apple-bplist.spec-conformance.test.ts::"throws EXIF.BPLIST_VERSION_UNSUPPORTED on bplist02 magic"` AND `src/makernotes/apple-bplist/apple-bplist.spec-conformance.test.ts::"throws EXIF.BPLIST_TRAILER_INVALID on missing trailer"` AND `src/makernotes/apple-bplist/apple-bplist.spec-conformance.test.ts::"throws EXIF.BPLIST_OBJECT_REF_OUT_OF_RANGE when offset table is too short"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ICC profile module
|
||||
|
||||
The package SHALL expose `parseIccProfile(bytes)`, `writeIccProfile(profile)`, `validateIccProfile(profile)`, and the convert helpers `classifyProfile`, `isSRGB`, `isAdobeRGB`, `isDisplayP3`, `profileDescription`. The module SHALL ship type definitions for `IccProfile`, `IccHeader`, `IccTagEntry`, `IccProfileClass`, `IccColorSpace`, `IccRenderingIntent`, and the four-byte tag signatures (`IccTagSignature` enum). When `readMetadata(buffer, options)` is called with `options.includeIcc !== false` (the default), and the input image carries an ICC profile (JPEG `APP2 / ICC_PROFILE\0` segment or PNG `iCCP` chunk), the result's `metadata.icc` field SHALL carry the parsed `IccProfile`.
|
||||
|
||||
#### Scenario: known sRGB profile parses
|
||||
|
||||
- **WHEN** `parseIccProfile(bytes)` is called against the IEC sRGB v4.2 profile bytes
|
||||
- **THEN** `isSRGB(profile)` returns `true` and `profile.tags` contains the expected entries
|
||||
|
||||
#### Scenario: round-trip preserves byte-equal output
|
||||
|
||||
- **WHEN** `writeIccProfile(parseIccProfile(bytes))` is invoked
|
||||
- **THEN** the output bytes are byte-equal to the input bytes for every fixture in the test corpus
|
||||
|
||||
#### Scenario: truncated header raises a stable error code
|
||||
|
||||
- **WHEN** `parseIccProfile(bytes)` is called against a buffer shorter than the 128-byte header
|
||||
- **THEN** it throws `SuaveplanError` with code `EXIF.ICC_HEADER_TOO_SHORT`
|
||||
|
||||
#### Scenario: tag offset out of range raises a stable error code
|
||||
|
||||
- **WHEN** `parseIccProfile(bytes)` is called against a profile whose tag table claims an offset beyond the profile's declared size
|
||||
- **THEN** it throws `SuaveplanError` with code `EXIF.ICC_TAG_OFFSET_OUT_OF_RANGE`
|
||||
|
||||
#### Scenario: `readMetadata` populates `metadata.icc` when present
|
||||
|
||||
- **WHEN** `readMetadata(jpegWithIcc)` is called with default options
|
||||
- **THEN** `metadata.icc` is a populated `IccProfile`
|
||||
|
||||
**Verified by:** `src/icc/read/read.test.ts::"parses sRGB v4.2 profile and isSRGB returns true"` AND `src/icc/write/write.test.ts::"round-trips every fixture byte-equal"` AND `src/icc/header/header.spec-conformance.test.ts::"throws EXIF.ICC_HEADER_TOO_SHORT on short input"` AND `src/icc/validate/validate.test.ts::"throws EXIF.ICC_TAG_OFFSET_OUT_OF_RANGE on bad tag offset"` AND `src/core/read-metadata.test.ts::"populates metadata.icc for JPEG with APP2 ICC_PROFILE segment"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: GPS module
|
||||
|
||||
The package SHALL expose `gpsCoordinatesToDecimal({ lat, latRef, lon, lonRef })`, `decimalToGpsCoordinates(lat, lon)`, `parseGpsTimestamp(date, time)`, and `gpsAltitudeToMeters(value, ref)`. The conversions round-trip within 1e-9 degrees for `lat ∈ [-90, 90]` and `lon ∈ [-180, 180]`. `gpsAltitudeToMeters` honours the GPSAltitudeRef byte (0 = above sea level, 1 = below sea level → negative meters).
|
||||
|
||||
#### Scenario: round-trip preserves coordinates within 1e-9
|
||||
|
||||
- **WHEN** `gpsCoordinatesToDecimal(decimalToGpsCoordinates(lat, lon))` is invoked for any `(lat, lon)` in the valid range
|
||||
- **THEN** the returned `(lat', lon')` satisfies `|lat - lat'| < 1e-9 && |lon - lon'| < 1e-9`
|
||||
|
||||
#### Scenario: invalid ref raises a stable error code
|
||||
|
||||
- **WHEN** `gpsCoordinatesToDecimal({ lat, latRef: "X", lon, lonRef: "E" })` is called (latRef is not `"N"` or `"S"`)
|
||||
- **THEN** it throws `SuaveplanError` with code `EXIF.GPS_INVALID_REF`
|
||||
|
||||
#### Scenario: out-of-range coordinate raises a stable error code
|
||||
|
||||
- **WHEN** `gpsCoordinatesToDecimal({ lat, latRef: "N", lon, lonRef: "E" })` is called with `lat > 90`
|
||||
- **THEN** it throws `SuaveplanError` with code `EXIF.GPS_OUT_OF_RANGE`
|
||||
|
||||
#### Scenario: timestamp combines GPSDateStamp and GPSTimeStamp
|
||||
|
||||
- **WHEN** `parseGpsTimestamp("2026:05:02", [12, 34, 56])` is called
|
||||
- **THEN** the returned `Date` represents `2026-05-02T12:34:56Z`
|
||||
|
||||
#### Scenario: altitude with ref=1 returns negative meters
|
||||
|
||||
- **WHEN** `gpsAltitudeToMeters(100, 1)` is called
|
||||
- **THEN** the returned value is `-100`
|
||||
|
||||
**Verified by:** `src/gps/convert/convert.test.ts::"round-trips decimal ↔ DMS within 1e-9 over a 1000-point random sweep"` AND `src/gps/convert/convert.spec-conformance.test.ts::"throws EXIF.GPS_INVALID_REF when latRef is not N or S"` AND `src/gps/convert/convert.spec-conformance.test.ts::"throws EXIF.GPS_OUT_OF_RANGE when lat > 90"` AND `src/gps/timestamp/timestamp.test.ts::"combines date stamp and time stamp into Date"` AND `src/gps/convert/convert.test.ts::"gpsAltitudeToMeters returns negative for ref=1"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
157
openspec/changes/exif-completion/tasks.md
Normal file
157
openspec/changes/exif-completion/tasks.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Tasks — EXIF completion (MakerNotes / ICC / GPS / Apple bplist / vendor ciphers)
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was claimed but never landed. The MakerNotes / ICC / GPS gaps closed here were prematurely ticked in `openspec/archive/2026-04-19-enterprise-exif/tasks.md` lines 37, 38, 43; that's the failure mode this checklist guards against.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [ ] 1.1 Read `../suaveplan/packages/universal/exif/src/{makernotes,icc,gps}` end-to-end (no dedicated `enterprise/exif.md` exists)
|
||||
- [ ] 1.2 Re-read `openspec/archive/2026-04-19-enterprise-exif/tasks.md` and `specs/exif/spec.md` to confirm what was claimed but not delivered
|
||||
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 0a (parity), 13 (verification), 14 (archival), 14a (affected dependents)
|
||||
- [ ] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`; the **Why** section explicitly cites the archive discrepancy (lines 37, 38, 43 of the archived `tasks.md`)
|
||||
- [ ] 1.5 Fill in `## Source mapping` table — every requirement cites a line range OR appears in the "no source line" table with justification
|
||||
- [ ] 1.6 Draft `specs/exif/spec.md` from `openspec/templates/spec-delta.md` covering all six ADDED Requirements
|
||||
- [ ] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
|
||||
- [ ] 1.8 `openspec change validate exif-completion` exits with no errors
|
||||
|
||||
## 2. MakerNotes registry + decoders
|
||||
|
||||
- [ ] 2.1 Define `MakerNoteDecoder` interface in `packages/image/exif/src/makernotes/types.ts`
|
||||
- [ ] 2.2 Port the dispatcher `makernotes/dispatch/dispatch.ts` (L1-L78) into `packages/image/exif/src/makernotes/dispatch/dispatch.ts`
|
||||
- [ ] 2.3 Port the 18 vendor decoders (one file each):
|
||||
- `apple/apple.ts` (L1-L126)
|
||||
- `canon/canon.ts` (L1-L139)
|
||||
- `casio/casio.ts` (L1-L6) — vendor-detection stub
|
||||
- `fujifilm/fujifilm.ts` (L1-L130)
|
||||
- `kodak/kodak.ts` (L1-L6) — vendor-detection stub
|
||||
- `leica/leica.ts` (L1-L6) — vendor-detection stub
|
||||
- `minolta/minolta.ts` (L1-L6) — vendor-detection stub
|
||||
- `nikon/nikon.ts` (L1-L155)
|
||||
- `olympus/olympus.ts` (L1-L129)
|
||||
- `panasonic/panasonic.ts` (L1-L133)
|
||||
- `pentax/pentax.ts` (L1-L140)
|
||||
- `ricoh/ricoh.ts` (L1-L6) — vendor-detection stub
|
||||
- `samsung/samsung.ts` (L1-L9)
|
||||
- `sigma/sigma.ts` (L1-L6) — vendor-detection stub
|
||||
- `sony/sony.ts` (L1-L14)
|
||||
- `generic/generic.ts` (L1-L13)
|
||||
- [ ] 2.4 `decodeMakerNotes(buffer, byteOrder, makeTag, modelTag?)` dispatcher — case-insensitive Make-prefix match, falls through to `generic`
|
||||
- [ ] 2.5 Add `EXIF.MAKERNOTE_VENDOR_UNKNOWN`, `EXIF.MAKERNOTE_DECRYPT_FAILED`, `EXIF.MAKERNOTE_INVALID_HEADER` to `error-codes.ts`
|
||||
- [ ] 2.6 Sibling `.md` for each implementation file (≥200 words, basic + advanced examples)
|
||||
|
||||
## 3. Vendor ciphers
|
||||
|
||||
- [ ] 3.1 Port `decryptNikonMakerNote` from `makernotes/nikon-cipher/nikon-cipher.ts` (L1-L84) into `packages/image/exif/src/makernotes/nikon-cipher/nikon-cipher.ts`
|
||||
- [ ] 3.2 Port `decryptSonyMakerNote` from `makernotes/sony-cipher/sony-cipher.ts` (L1-L29) into `packages/image/exif/src/makernotes/sony-cipher/sony-cipher.ts`
|
||||
- [ ] 3.3 Wire ciphers into the Nikon and Sony decoders; both decoders accept an injectable cipher function (default = the bundled one) for testability
|
||||
- [ ] 3.4 KAT vector tests against legacy implementation
|
||||
- [ ] 3.5 Sibling `.md`
|
||||
|
||||
## 4. Apple bplist parser
|
||||
|
||||
- [ ] 4.1 Port `parseBplist` from `makernotes/apple-bplist/apple-bplist.ts` (L1-L170) into `packages/image/exif/src/makernotes/apple-bplist/apple-bplist.ts`
|
||||
- [ ] 4.2 Define typed value tree: `BplistValue = { type: 'dict' | 'array' | 'string' | 'integer' | 'real' | 'boolean' | 'date' | 'data' | 'uid' | 'null'; value: … }`
|
||||
- [ ] 4.3 Add `EXIF.BPLIST_VERSION_UNSUPPORTED`, `EXIF.BPLIST_TRAILER_INVALID`, `EXIF.BPLIST_OBJECT_REF_OUT_OF_RANGE` to `error-codes.ts`
|
||||
- [ ] 4.4 Wire `parseBplist` into the Apple decoder for HEIC + iPhone-shot JPEG
|
||||
- [ ] 4.5 Sibling `.md`
|
||||
|
||||
## 5. ICC profile module
|
||||
|
||||
- [ ] 5.1 Port `icc/header/header.ts` (L1-L121), `icc/read/read.ts` (L1-L165), `icc/write/write.ts` (L1-L187), `icc/validate/validate.ts` (L1-L37), `icc/convert/convert.ts` (L1-L51), `icc/tag-table/tag-table.ts` (L1-L63) into `packages/image/exif/src/icc/{header,read,write,validate,convert,tag-table}/`
|
||||
- [ ] 5.2 Define `IccProfile`, `IccHeader`, `IccTagEntry`, `IccProfileClass`, `IccColorSpace`, `IccRenderingIntent`, `IccTagSignature` types in `src/icc/types.ts`
|
||||
- [ ] 5.3 Public API: `parseIccProfile`, `writeIccProfile`, `validateIccProfile`, plus convert helpers `classifyProfile`, `isSRGB`, `isAdobeRGB`, `isDisplayP3`, `profileDescription`
|
||||
- [ ] 5.4 Add `EXIF.ICC_HEADER_TOO_SHORT`, `EXIF.ICC_PROFILE_TOO_SHORT`, `EXIF.ICC_TAG_OFFSET_OUT_OF_RANGE`, `EXIF.ICC_INVALID_SIGNATURE`, `EXIF.ICC_VALIDATION_FAILED` to `error-codes.ts`
|
||||
- [ ] 5.5 Wire ICC into `readMetadata`: when an ICC profile segment is present in JPEG (`APP2 / ICC_PROFILE\0`) or PNG (`iCCP` chunk), populate `metadata.icc`
|
||||
- [ ] 5.6 Add `includeIcc?: boolean` to `ReadOptions` (default `true`)
|
||||
- [ ] 5.7 Sibling `.md` for each implementation file
|
||||
|
||||
## 6. GPS module
|
||||
|
||||
- [ ] 6.1 Port `gps/convert/convert.ts` (L1-L41) into `packages/image/exif/src/gps/convert/convert.ts`
|
||||
- [ ] 6.2 Port `gps/timestamp/timestamp.ts` (L1-L38) into `packages/image/exif/src/gps/timestamp/timestamp.ts`
|
||||
- [ ] 6.3 Public API: `gpsCoordinatesToDecimal`, `decimalToGpsCoordinates`, `parseGpsTimestamp`, `gpsAltitudeToMeters`
|
||||
- [ ] 6.4 Add `EXIF.GPS_INVALID_REF`, `EXIF.GPS_OUT_OF_RANGE` to `error-codes.ts`
|
||||
- [ ] 6.5 Sibling `.md`
|
||||
|
||||
## 7. Public surface
|
||||
|
||||
- [ ] 7.1 `package.json` `exports` map gains `./makernotes`, `./icc`, `./gps`, `./bplist` subpaths
|
||||
- [ ] 7.2 Root `index.ts` re-exports `decodeMakerNotes`, `parseIccProfile`/`writeIccProfile`/`validateIccProfile`, GPS helpers, and `parseBplist`
|
||||
- [ ] 7.3 `ReadOptions` gains `includeMakerNote?: boolean` and `includeIcc?: boolean` (both default `true`)
|
||||
- [ ] 7.4 `ImageMetadata` gains optional `icc?: IccProfile`
|
||||
- [ ] 7.5 `readMetadata` decodes MakerNote when `Make` tag present; falls through to raw bytes when `includeMakerNote: false` (back-compat)
|
||||
|
||||
## 8. Tests
|
||||
|
||||
- [ ] 8.1 `src/makernotes/dispatch/dispatch.test.ts` — vendor selection by Make tag; fall-through to generic
|
||||
- [ ] 8.2 `src/makernotes/dispatch/dispatch.spec-conformance.test.ts` — `MakerNoteDecoder` interface pinned for every vendor; `MAKERNOTE_VENDOR_UNKNOWN` raised in strict mode
|
||||
- [ ] 8.3 `src/makernotes/<vendor>/<vendor>.test.ts` for all 18 vendors — happy path against fixture
|
||||
- [ ] 8.4 `src/makernotes/nikon-cipher/nikon-cipher.test.ts` — KAT vectors
|
||||
- [ ] 8.5 `src/makernotes/sony-cipher/sony-cipher.test.ts` — KAT vectors
|
||||
- [ ] 8.6 `src/makernotes/apple-bplist/apple-bplist.test.ts` — bplist00 v0/v1 fixtures
|
||||
- [ ] 8.7 `src/makernotes/apple-bplist/apple-bplist.spec-conformance.test.ts` — `BPLIST_VERSION_UNSUPPORTED`, `BPLIST_TRAILER_INVALID`, `BPLIST_OBJECT_REF_OUT_OF_RANGE` paths
|
||||
- [ ] 8.8 `src/icc/read/read.test.ts` — sRGB and Display-P3 fixtures
|
||||
- [ ] 8.9 `src/icc/write/write.test.ts` — round-trip byte-equal
|
||||
- [ ] 8.10 `src/icc/validate/validate.test.ts` — valid + 3 truncation variants
|
||||
- [ ] 8.11 `src/icc/convert/convert.test.ts` — `classifyProfile`, `isSRGB`, `isAdobeRGB`, `isDisplayP3`
|
||||
- [ ] 8.12 `src/icc/header/header.spec-conformance.test.ts` — `ICC_HEADER_TOO_SHORT`
|
||||
- [ ] 8.13 `src/gps/convert/convert.test.ts` — round-trip decimal ↔ DMS within 1e-9; altitude positive vs negative ref
|
||||
- [ ] 8.14 `src/gps/convert/convert.spec-conformance.test.ts` — `GPS_INVALID_REF`, `GPS_OUT_OF_RANGE`
|
||||
- [ ] 8.15 `src/gps/timestamp/timestamp.test.ts` — date+time → Date; midnight/midday boundaries
|
||||
- [ ] 8.16 `tests/dep-graph.test.ts` — subpath exports `./makernotes`, `./icc`, `./gps`, `./bplist` resolve
|
||||
- [ ] 8.17 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 9. Documentation
|
||||
|
||||
- [ ] 9.1 Sibling `.md` for every new implementation `.ts` (≥200 words, basic + advanced examples, full API reference)
|
||||
- [ ] 9.2 README links every co-located `.md` under `## Modules`
|
||||
- [ ] 9.3 Update `CLAUDE.md` `exif` package listing to reflect the restored surface (MakerNotes for 18 vendors, ICC read/write/validate/convert, GPS, Apple bplist, Nikon + Sony ciphers)
|
||||
|
||||
## 10. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [ ] 10.1 `bun run check` — Biome 0/0/0 across the monorepo
|
||||
- [ ] 10.2 `bunx turbo lint typecheck test --filter=@suaveplan/exif` — green
|
||||
- [ ] 10.3 `bunx turbo test:coverage --filter=@suaveplan/exif` — 100/100/100/100
|
||||
- [ ] 10.4 `bun scripts/openspec-conformance.ts @suaveplan/exif` — exit 0, no `error` findings
|
||||
- [ ] 10.5 `bunx turbo build --filter=@suaveplan/exif` — green
|
||||
|
||||
## 11. Pre-archive 7-row checklist (AGENTS.md Section 13)
|
||||
|
||||
- [ ] 11.1 Generics threaded; `expectTypeOf` assertions on `MakerNoteData`, `IccProfile`, `GpsCoordinate`, `BplistValue` discriminated unions
|
||||
- [ ] 11.2 Zod schemas wired at every declared boundary (`MakerNoteData`, `IccProfile`, GPS inputs, `ReadOptions`)
|
||||
- [ ] 11.3 Every `EXIF.*` code added in §2.5 / 3 / 4.3 / 5.4 / 6.4 thrown by at least one production path; no orphans
|
||||
- [ ] 11.4 Strict-mode behaviors tested both on and off (e.g. `includeMakerNote: true` vs `false`; `includeIcc: true` vs `false`)
|
||||
- [ ] 11.5 OTel histograms emitted for `exif.makernote.decode_ms`, `exif.icc.parse_ms`, `exif.gps.convert_ms`, asserted with a real in-memory `MeterProvider`
|
||||
- [ ] 11.6 Lifecycle dedup tested: same image decoded twice produces identical output (deterministic; ciphers seeded from input only, no module-scope state)
|
||||
- [ ] 11.7 No retroactive tick — every box above reflects work done in this change
|
||||
|
||||
## 12. Archive
|
||||
|
||||
- [ ] 12.1 Fold spec delta into `openspec/specs/exif/spec.md` (creating any missing requirements; existing requirements claimed in 2026-04-19 archive but never delivered are now backed by code)
|
||||
- [ ] 12.2 Move `openspec/changes/exif-completion/` → `openspec/archive/<yyyy-mm-dd>-exif-completion/`
|
||||
- [ ] 12.3 Add a note to `openspec/archive/2026-04-19-enterprise-exif/tasks.md` (or to the archive's `proposal.md`) pointing at this change as the actual delivery of §7.1, §7.2, §8.1
|
||||
- [ ] 12.4 Refresh `MANIFEST.md` if the package's exports list materially changed
|
||||
- [ ] 12.5 Update `STATUS.md` if the package was previously listed as Pending
|
||||
|
||||
## Audit Note (2026-05-02)
|
||||
|
||||
Audit verdict: **DEFERRED — 0/78 tasks evidenced in code**.
|
||||
|
||||
Evidence reviewed:
|
||||
|
||||
- `packages/image/exif/src/` has `binary/`, `core/`, `exif/`, `format/`, `ifd/`, `iptc/`, `jpeg/`, `xmp/`, `errors/`, `types.ts`. No `makernotes/`, no `icc/`, no `gps/`, no `bplist/`.
|
||||
- The 18 vendor MakerNote decoders, the dispatcher, the Nikon + Sony ciphers, the Apple bplist parser, the ICC profile module (header/read/write/validate/convert/tag-table), and the GPS module are all absent.
|
||||
- None of the new `EXIF.*` error codes (MAKERNOTE_*, BPLIST_*, ICC_*, GPS_*) appear in `errors/exif-error.ts`.
|
||||
|
||||
Missing pieces:
|
||||
|
||||
1. `makernotes/` directory with dispatcher + 18 vendor decoders + Nikon/Sony cipher pair + Apple bplist parser.
|
||||
2. `icc/` module (six files: header, read, write, validate, convert, tag-table) wired into `readMetadata` for JPEG `APP2` and PNG `iCCP`.
|
||||
3. `gps/` module (DMS↔decimal conversion + timestamp parsing) plus `ReadOptions` flag additions and the corresponding error codes.
|
||||
|
||||
Decision: leave change open. The MakerNotes block alone is 18+ vendor files with KAT cipher tests, plus the ICC and GPS modules — substantial implementation work flagged by the prior audit (entries 37/38/43 of `2026-04-19-enterprise-exif/tasks.md` were ticked but never delivered).
|
||||
Reference in New Issue
Block a user