@photivra/engine 2.21.0 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +118 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +11 -0
  6. package/dist/capture/c2pa-embed.d.ts +116 -0
  7. package/dist/capture/c2pa-embed.js +493 -0
  8. package/dist/capture/c2pa-embed.js.map +1 -0
  9. package/dist/capture/dng-import.d.ts +73 -8
  10. package/dist/capture/dng-import.js +315 -80
  11. package/dist/capture/dng-import.js.map +1 -1
  12. package/dist/capture/dng-opcodes.d.ts +96 -0
  13. package/dist/capture/dng-opcodes.js +391 -0
  14. package/dist/capture/dng-opcodes.js.map +1 -0
  15. package/dist/capture/dng-profile-maps.d.ts +99 -0
  16. package/dist/capture/dng-profile-maps.js +381 -0
  17. package/dist/capture/dng-profile-maps.js.map +1 -0
  18. package/dist/capture/dng-stage-image.d.ts +23 -0
  19. package/dist/capture/dng-stage-image.js +65 -0
  20. package/dist/capture/dng-stage-image.js.map +1 -0
  21. package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
  22. package/dist/capture/jpeg-xl-tiles.js +151 -0
  23. package/dist/capture/jpeg-xl-tiles.js.map +1 -0
  24. package/dist/capture/lossy-jpeg.d.ts +17 -0
  25. package/dist/capture/lossy-jpeg.js +369 -0
  26. package/dist/capture/lossy-jpeg.js.map +1 -0
  27. package/dist/capture/native-raw-development.d.ts +2 -1
  28. package/dist/capture/native-raw-development.js.map +1 -1
  29. package/dist/color/neighbor-highlight-reconstruction.d.ts +66 -0
  30. package/dist/color/neighbor-highlight-reconstruction.js +234 -0
  31. package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
  32. package/dist/color/sensor-color-development.d.ts +5 -0
  33. package/dist/color/sensor-color-development.js +7 -1
  34. package/dist/color/sensor-color-development.js.map +1 -1
  35. package/dist/color/spectral-spikiness.d.ts +11 -0
  36. package/dist/color/spectral-spikiness.js +23 -0
  37. package/dist/color/spectral-spikiness.js.map +1 -0
  38. package/dist/core/execution-error.d.ts +21 -0
  39. package/dist/core/execution-error.js +12 -0
  40. package/dist/core/execution-error.js.map +1 -0
  41. package/dist/core/version.d.ts +1 -1
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/version.js.map +1 -1
  44. package/dist/facade/catalogue-lens-signal.d.ts +26 -1
  45. package/dist/facade/catalogue-lens-signal.js +25 -7
  46. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  47. package/dist/facade/diffraction-provider.d.ts +1 -1
  48. package/dist/facade/diffraction-provider.js +4 -4
  49. package/dist/facade/diffraction-provider.js.map +1 -1
  50. package/dist/facade/display-view-gpu-session.js +38 -6
  51. package/dist/facade/display-view-gpu-session.js.map +1 -1
  52. package/dist/facade/display-view-gpu.d.ts +13 -5
  53. package/dist/facade/display-view-gpu.js.map +1 -1
  54. package/dist/facade/display-view-normal.d.ts +15 -0
  55. package/dist/facade/display-view-normal.js +44 -0
  56. package/dist/facade/display-view-normal.js.map +1 -0
  57. package/dist/facade/display-view-rendering.d.ts +97 -0
  58. package/dist/facade/display-view-rendering.js +598 -0
  59. package/dist/facade/display-view-rendering.js.map +1 -0
  60. package/dist/facade/display-view.d.ts +37 -65
  61. package/dist/facade/display-view.js +147 -476
  62. package/dist/facade/display-view.js.map +1 -1
  63. package/dist/facade/expected-signal-setup.js +2 -2
  64. package/dist/facade/expected-signal-setup.js.map +1 -1
  65. package/dist/facade/fast-methods.d.ts +1 -1
  66. package/dist/facade/fast-methods.js +37 -8
  67. package/dist/facade/fast-methods.js.map +1 -1
  68. package/dist/facade/generic-lens-capture.d.ts +1 -1
  69. package/dist/facade/generic-lens-capture.js +1 -1
  70. package/dist/facade/generic-lens-capture.js.map +1 -1
  71. package/dist/facade/image-plane.js +41 -10
  72. package/dist/facade/image-plane.js.map +1 -1
  73. package/dist/facade/on-demand-kits.d.ts +14 -0
  74. package/dist/facade/on-demand-kits.js +36 -0
  75. package/dist/facade/on-demand-kits.js.map +1 -0
  76. package/dist/facade/opt-in-kit.d.ts +9 -0
  77. package/dist/facade/opt-in-kit.js +11 -0
  78. package/dist/facade/opt-in-kit.js.map +1 -0
  79. package/dist/facade/quality-kit.d.ts +21 -0
  80. package/dist/facade/quality-kit.js +23 -0
  81. package/dist/facade/quality-kit.js.map +1 -0
  82. package/dist/facade/quality-paths.d.ts +22 -0
  83. package/dist/facade/quality-paths.js +44 -0
  84. package/dist/facade/quality-paths.js.map +1 -0
  85. package/dist/facade/scene-radiance.d.ts +18 -2
  86. package/dist/facade/scene-radiance.js +6 -4
  87. package/dist/facade/scene-radiance.js.map +1 -1
  88. package/dist/facade/simulate-capture.d.ts +32 -13
  89. package/dist/facade/simulate-capture.js +162 -69
  90. package/dist/facade/simulate-capture.js.map +1 -1
  91. package/dist/facade/simulate-display-view.d.ts +10 -12
  92. package/dist/facade/simulate-display-view.js +57 -48
  93. package/dist/facade/simulate-display-view.js.map +1 -1
  94. package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
  95. package/dist/gpu/display-view-gpu-renderer.js +134 -0
  96. package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
  97. package/dist/gpu/display-view-image-material-module.d.ts +3 -2
  98. package/dist/gpu/display-view-image-material-module.js +25 -3
  99. package/dist/gpu/display-view-image-material-module.js.map +1 -1
  100. package/dist/gpu/display-view-image-quality.d.ts +43 -1
  101. package/dist/gpu/display-view-image-quality.js +256 -11
  102. package/dist/gpu/display-view-image-quality.js.map +1 -1
  103. package/dist/gpu/display-view-quality-gpu.d.ts +13 -64
  104. package/dist/gpu/display-view-quality-gpu.js +60 -249
  105. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  106. package/dist/gpu/display-view-speed-gpu.d.ts +16 -16
  107. package/dist/gpu/display-view-speed-gpu.js +50 -12
  108. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  109. package/dist/gpu/image-kernel-accelerator.js +3 -0
  110. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  111. package/dist/gpu/image-kernel-execution.d.ts +2 -13
  112. package/dist/gpu/image-kernel-execution.js +2 -10
  113. package/dist/gpu/image-kernel-execution.js.map +1 -1
  114. package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +8 -0
  115. package/dist/gpu/wgsl-display-view-coupled-stub.js +21 -0
  116. package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
  117. package/dist/gpu/wgsl-display-view-coupled.d.ts +88 -0
  118. package/dist/gpu/wgsl-display-view-coupled.js +353 -0
  119. package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
  120. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +4 -9
  121. package/dist/gpu/wgsl-display-view-quality-kernel.js +109 -111
  122. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  123. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +1 -41
  124. package/dist/gpu/wgsl-display-view-speed-kernel.js +71 -8
  125. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  126. package/dist/index.d.ts +11 -3
  127. package/dist/index.js +9 -1
  128. package/dist/index.js.map +1 -1
  129. package/dist/output/camera-channel-response.d.ts +50 -0
  130. package/dist/output/camera-channel-response.js +69 -0
  131. package/dist/output/camera-channel-response.js.map +1 -0
  132. package/dist/output/native-capture-sdr.js +5 -2
  133. package/dist/output/native-capture-sdr.js.map +1 -1
  134. package/dist/output/sdr-rendering.d.ts +18 -1
  135. package/dist/output/sdr-rendering.js +39 -2
  136. package/dist/output/sdr-rendering.js.map +1 -1
  137. package/dist/sensor/native-sensor-report.d.ts +10 -0
  138. package/dist/sensor/native-sensor-report.js +16 -0
  139. package/dist/sensor/native-sensor-report.js.map +1 -0
  140. package/dist/sensor/sensor-characteristics.d.ts +81 -13
  141. package/dist/sensor/sensor-characteristics.js +10 -6
  142. package/dist/sensor/sensor-characteristics.js.map +1 -1
  143. package/docs/API_REFERENCE.md +117 -4
  144. package/docs/QUICK_START.md +37 -8
  145. package/docs/USAGE.md +104 -22
  146. package/docs/api/capture-c2pa-embed.md +177 -0
  147. package/docs/api/capture-dng-import.md +86 -10
  148. package/docs/api/capture-dng-opcodes.md +93 -0
  149. package/docs/api/capture-dng-profile-maps.md +141 -0
  150. package/docs/api/capture-dng-stage-image.md +43 -0
  151. package/docs/api/capture-jpeg-xl-tiles.md +33 -0
  152. package/docs/api/capture-lossy-jpeg.md +41 -0
  153. package/docs/api/capture-native-raw-development.md +2 -1
  154. package/docs/api/core-execution-error.md +43 -0
  155. package/docs/api/core-version.md +1 -1
  156. package/docs/api/exports.json +255 -5
  157. package/docs/api/facade-display-view-gpu.md +5 -2
  158. package/docs/api/facade-display-view.md +11 -15
  159. package/docs/api/facade-fast-methods.md +38 -9
  160. package/docs/api/facade-quality-paths.md +24 -0
  161. package/docs/api/facade-scene-radiance.md +1 -1
  162. package/docs/api/facade-simulate-capture.md +24 -7
  163. package/docs/api/facade-simulate-display-view.md +10 -12
  164. package/docs/api/gpu-image-kernel-execution.md +0 -36
  165. package/docs/api/output-camera-channel-response.md +55 -0
  166. package/docs/api/output-sdr-rendering.md +6 -1
  167. package/docs/api/sensor-sensor-characteristics.md +54 -2
  168. package/llms.txt +1 -0
  169. package/package.json +6 -5
package/CHANGELOG.md CHANGED
@@ -1,6 +1,123 @@
1
1
  # Changelog
2
2
 
3
- ## 2.21.0 — unreleased (prepared November 10, 2026)
3
+ ## 2.22.0 — unreleased (prepared November 11, 2026)
4
+
5
+ A minor release: phone DNG import with JPEG XL and lossy JPEG raw data, the DNG stage image and opcodes (#598, #600) and the channel-response helper (#591); native-photosite sensor figures (#595), read noise per photosite on the tiers (#599), mixed lighting over an image-plane print (#597), sample counts that follow the light and the output (#590) and the catalogue's fast grid (#603); the display view's luminance tone curves, highlight reconstruction and desaturation on the CPU and GPU (#593, #601, #602); the removal of the display view's AHD low-signal correction; Content Credentials writers (#165, engine half); and the full-quality and opt-in code loaded on demand (#594). See the [2.22.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_22_0.md).
6
+
7
+ - **New:** `importDngRawImage`, `parseLossyJpeg`, `createJxlOxideDngDecoder`, `parseDngOpcodeList`, `applyDngOpcodes`, `developDngStageImage`, `parseDngProfileGainTableMap`, `parseDngRgbTables`, `applyDngProfileGainTableMap`, `applyDngRgbTables` (#598, #600); `calculateCameraChannelResponse` (#591); `result.sensor.native` (#595); `output.gamutHandling: "desaturate-to-luminance"` (#601) and `output.highlightMode: "reconstruct-neighbors"` (#602); the luminance tone curves and `highlightMode: "reconstruct"` in the display view (#593); `bindC2paManifestStore`, `calculateC2paDataHash`, `setC2paManifestStore`, `parseC2paManifestStore` (#165); `prepareQualityPaths()` (#594).
8
+ - **Changed (see "Default changes"):** the tiers' read noise is per photosite (#599); the default spectral step under a spiky light on exact requests (#590); the catalogue's fast grid (#603); mixed lighting over a print is rendered (#597); the display view's AHD has no low-signal correction; two `raw.expectedSignal` method names; the GPU Quality self-check runs at the first Quality render; the full-quality and opt-in code loads on demand (#594).
9
+ - **Root exports:** 50 new, 1,931 → 1,981 (runtime exports 654 → 672): 29 from #598 and #600 (11 runtime), 3 from #591 (1 runtime), 2 from #595 (types), 15 from #165 (6 runtime), 1 from #594 (runtime). Nothing is removed; the AHD correction-state API is deprecated (no-ops until 3.0.0).
10
+ - **Why minor:** new root exports and options; every default change below keeps the old behavior one option away or is a model correction or a removal the owner decided, with its deprecated API kept.
11
+
12
+ ### Default changes (migration)
13
+
14
+ | What changed | Before (2.21.0) | Now (2.22.0) | Keep the old behavior |
15
+ | --- | --- | --- | --- |
16
+ | Generic tiers' (and caller presets') read noise above 6 µm, and `sensor.readNoise` on a tier (#599; owner decision 2026-11-11) | once per rendered site, whatever the pitch; `sensor.readNoise` was the site's value | the read noise of one physical photosite (as a datasheet gives it, as for a sensor profile, #554): a site that sums N = pitch²/36 µm² photosites takes it ×√N; `native.readNoiseElectrons` reports it as given; at 6 µm and below unchanged | set `sensor.readNoise` to the 2.21.0 site value divided by √N = pitch / 6 µm |
17
+ | Default spectral step under a spiky light (#590) | 20 nm | 5 nm on exact requests (DNG, JPEG, PNG, the ideal reference, `fast: false`) when R(20) = 20²·κ/24 > 0.01: the four #557 representative sources (fluorescent 0.84, LEDs 0.017–0.038); fast requests keep 20 nm with the assumption `sensor.spectralSpikiness` and `diagnostics.fast.applied` reporting it; Planck, CIE A and other smooth lights unchanged | `sensor.spectralStepNanometers: 20` |
18
+ | Catalogue field grid on fast requests (#603) | at least 10 cells per field interval | at least 4 (2.20.0's grid) on fast requests (on-screen sRGB captures), except the measured lenses of `CATALOGUE_FAST_GRID_FULL_ON_SCREEN` (the PV 15 2.8 Fisheye Pro), which keep 10; exact requests keep 10 | `fast: false` with `lens.catalogue: { kind, fast: true }` |
19
+ | Mixed lighting over an image-plane print (#597) | refused | rendered: every site integrates the lit radiance per node on the CPU; the GPU display view lights each lattice point in the print's Quality passes (Speed requests too) | — (a refusal lifted) |
20
+ | Display view with `output.demosaic: "ahd"` (owner decision 2026-11-11) | a low-signal correction from precomputed values on a grid of the three channel signal levels | no correction on any path; `view.demosaic` labels the remaining bias an approximation with measured numbers (largest: about 4 red codes at 1.4 µm, ISO 6400) | no option (the correction-state API is kept as checked no-ops until 3.0.0) |
21
+ | `raw.expectedSignal` method names (labels only) | `tabulated-expected-electrons`, `tabulated-expected-electrons-catalogue-psf-field-grid` | `per-site-integrated-electrons`, `per-site-integrated-electrons-catalogue-psf-field-grid` (what the paths have done since 2.21.0); samples and RAW codes unchanged | match the new names |
22
+ | GPU Quality self-check (#594; owner decision 2026-11-10) | before a view's first GPU render | before its first render that takes the Quality passes (a Speed-only view lists only the Speed case) | no option (diagnostics timing; results unchanged) |
23
+ | Full-quality and opt-in code (#594) | in a bundle's first load | loaded on demand, once, by the first request that uses it (a bundler that splits dynamic imports puts it in its own chunks); a request whose code cannot load fails with `ImageKernelExecutionError` reason `quality-code-unavailable` (the GPU falls back to the CPU) | `await prepareQualityPaths()` at start-up; results are unchanged either way |
24
+
25
+ ### Integration (this release)
26
+
27
+ - **Parts:** six part branches merged `--no-ff` (docs/RELEASE_2_22_0.md, "Scope"); every part's behavior is kept. The AHD correction's removal and #593's coupled rendering met in the display view's CPU and GPU code: the coupled steps keep their constants after the aux table, and the pixel pass's bindings are the five without the correction's state table, bitmap and threshold.
28
+ - **Cross-part fixes (tests and inputs, no result change):** the DNG import helpers reject malformed input with domain errors (no raw `TypeError`); `import`, `apply` and `develop` are documented verb families (docs/API_STYLE.md); a bound DNG's import names the C2PA store tag as present and not applied, now checked; the GPU renderer's `supports()` keeps refusing unknown tone curves and accepts the two luminance curves; hash locks re-recorded only where two parts changed the same output, each checked against the part that changed its bytes. #597's image-plane light shader declared a local named `common`, a WGSL reserved word that Chrome 153 refuses on hardware adapters; renamed (results unchanged), and a test now rejects reserved words in shipped WGSL.
29
+ - **The GPU coupled steps on SwiftShader (#593, #601):** the Speed kernel and the Quality pixel pass with the coupled steps did not compile on SwiftShader (the GPU process ended after about 44 s; the same on the part alone). Bisected to the colour-matrix lookups (`m_of`, a `switch`) inside the luminance curve's nested loops; those loops now read the matrix rows from one local array (the same values), and the kernels compile in about 2 s. Every coupled case passes on SwiftShader, the hardware adapter and the float32 emulation.
30
+ - **CI capacity (owner decisions):** coverage runs as eight shards in `ci.yml` and `publish.yml` (six in 2.21.0; the coverage gate still merges every shard and enforces the thresholds on the whole suite), and the SwiftShader WebGPU suite as two parallel jobs, `webgpu conformance (SwiftShader, 1/2)` and `(2/2)` (`test/webgpu/display-view-quality.webgpu.ts`, split from `display-view.webgpu.ts`, runs in part 2). The slowest test files are split with the same tests: the Quality cases after the charts into `test/display-view-quality-gpu-cases.test.ts`, the coupled `reconstruct` case into `test/display-view-coupled-gpu-reconstruct.test.ts`, and the extra defocus cases into `test/display-view-defocus-gpu-extra.test.ts`. No job time limit was raised and no test was removed; `test/shard-weights.json` comes from this pull request's CI times.
31
+ - **#594 CPU split:** see "#594 On-demand Quality code" below.
32
+
33
+ ### #598, #600, #591 Phone DNG import and the channel-response helper
34
+
35
+ - **Phone RAW files (#598).** `parseDngRawImage` reads 8-bit lossy JPEG raw data (Compression 34892) with the engine's own sequential-DCT decoder (`parseLossyJpeg`, ITU-T T.81), and the new `importDngRawImage` also reads JPEG XL raw data (52546) through a caller-supplied decoder: `createJxlOxideDngDecoder` adapts the prebuilt jxl-oxide WebAssembly package, whose 16-bit PNG output is read with the platform's `DecompressionStream`. Lossless 8–16-bit tiles round-trip bit-exactly (synthetic fixtures). Results carry `lossySource`, the opcode lists, ForwardMatrix1/2, BaselineExposure, the profile gain table map, RGBTables, decoded semantic masks and an `assumptions` list naming every rendering tag that is present and not applied; no phone tag fails an import. Row/ColumnInterleaveFactor are undone (order inferred from the DNG SDK, reported). New `maximumBytes` memory pre-check (default 1 GiB) and `size: "half"` LinearRaw preview; the 64 MP default stays. Schema 1.1.0. Verification against the owner's phone files is pending.
36
+ - **DNG stage image and opcodes (#598, #600).** `developDngStageImage` applies OpcodeList1, linearization, black and white levels and OpcodeList2/3 (LinearRaw); `parseDngOpcodeList` and `applyDngOpcodes` apply MapTable, MapPolynomial, GainMap, the per-row and per-column opcodes, FixVignetteRadial, WarpRectilinear (cubic convolution at full quality, bilinear with `fast`: new registry entry `dngImport.warpRectilinear`; fast by default, full quality when `format` is an export) and FixBadPixelsConstant/List; WarpFisheye, TrimBounds and WarpRectilinear2 are reported.
37
+ - **Profile gain table map and RGBTables (#598).** `applyDngProfileGainTableMap` and `applyDngRgbTables` apply ProfileGainTableMap(2) and masked RGBTables (weighted sum or sequential) at their places in a development.
38
+ - **Channel response helper (#591).** `calculateCameraChannelResponse({ sensorProfile?, peakQuantumEfficiency?, spectralStepNanometers?, illuminant? })` returns the capture's own channel response (electrons per joule at its spectral nodes) and, with an illuminant, the `channelDispersion` for `calculateLateralColorFringe`; full quality (5 nm) by default; agrees with a capture's reported dispersion to 1e-9. Not part of the capture worker.
39
+ - Dev dependency jxl-oxide-wasm 0.12.6 (MIT OR Apache-2.0; chosen licence Apache-2.0) for tests; the licence check accepts SPDX `OR` expressions with an allowed choice. THIRD_PARTY.md has a JPEG XL note.
40
+
41
+ ### #595, #599, #597, #590, #603 Sensor figures, read noise, mixed light over prints, sample counts
42
+
43
+ - **#595 New: `result.sensor.native`**, the sensor figures of one native photosite (a tier's declared 6 µm reference, a sensor profile's own pitch) beside the as-rendered ones: full well, read noise, engineering dynamic range (and per ISO), `photositesPerSite`, the mid-gray figure on exact requests, the EMVA 1288 Release 4.0 dynamic range (§2.7, Eq. 26–28: μ_e.min = √(σ_d² + σ_q² + 1/4) + 1/2, in stops and dB, with the saturation capacity per µm²), and `normalized8Megapixel`, a convention (not EMVA): DR + ½·log₂(frame photosites / 8 000 000). New root types `SimulateCaptureNativeSensorReport` and `SimulateCaptureMidGreyFigure`. Model figures, exact arithmetic. Professional tier, ISO 100/800/6400: native 14.67/12.72/9.73 stops engineering, 14.36/12.09/9.10 EMVA, at every pitch.
44
+ - **#599 Changed:** the tiers' as-rendered read noise scales by √N above 6 µm ("Default changes"). Rendered dynamic range at 46.9 µm, ISO 100/800/6400: 15.70/15.69/15.18 → 15.66/15.20/12.69 stops (now the binned value N·S₀ / √(N·σ_read² + σ_q,N²)); 187.5 µm: 15.70 → 15.70/15.66/14.54. The mid-gray SNR changes only where read noise matters (46.9 µm, ISO 100: 688.6). Tests: `test/sensor-native-report.test.ts` (new; a dark-frame photon-transfer check at 60 µm).
45
+ - **#597 Mixed lighting over a print.** The 2.21.0 refusal is lifted. **CPU** (capture, display view, ideal reference, moving subject, exports): the print's reflectance spectrum at each point times the blended irradiance, per spectral node, integrated per site at use; no per-pixel signal is cached under a blend (owner rule), only the pixels' reflectance spectra (by row, the same values). Measured (320 × 240 photo on a 384 × 256 DNG, 2 × 2 box): one light 73 ms; a two-source gradient 760–780 ms, a point source 590–625 ms (about 1,240 and 1,070 ms without the spectra cache). **GPU** (owner decision): the print's Quality passes take a light table (the blend's geometry and stops, the regions, the point sources, and each light's spectrum at the nodes; no channel value) and pass 1 lights each lattice point: the blend in double-float32, weights rounded once, hard edges and region edges flagged, point sources' h/d³ as bounded values, flagged when the computed bound exceeds 39·E; the site-sum bound grows by 40·E. Speed requests take these passes too (a print under a blend has no per-pixel signals). Registered as `illumination/image-plane-gpu` (fast: `gpu: "auto"`, float32; exact: the CPU reference). The self-check adds a lit case. Emulation: every case inside its bounds, samples within one code, ΔE2000 ≤ 1.9e-5, no flags on the gradient, radial-with-hard-edge and regions-plus-point cases; a regions edge exactly on the lattice is flagged (40 points) or resolved on the host (140). Charts under a blend keep the CPU. Tests: `test/image-plane-mixed-lighting.test.ts` (new), `test/display-view-image-gpu.test.ts`, `test/fast-methods-registry.test.ts`. Not measured on a hardware adapter.
46
+ - **#603 Catalogue fast grid.** `CATALOGUE_FAST_GRID_SPEED_SUBDIVISIONS` (4) on fast requests, `CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS` (10) on exact ones, resolved from the request's `fast`; registered as `lens.catalogue/field-grid-cells`, reported as `minimumSubdivisions` in `fast.lens.catalogue`. Measurements (frame time, fallbacks and lateral color for both grids) in docs/LENS_CATALOGUE.md: lateral color 96–101 % of the model with either grid. Tests: `test/simulate-capture-lens-catalogue-fast.test.ts`.
47
+ - **#590 Sample counts follow the output.** New `src/color/spectral-spikiness.ts`: κ = Σ|Δ²S| / ΣS on a 1 nm grid and R(h) = h²·κ/24 (the composite midpoint rule's error term, DLMF §3.5(i), normalized; the 0.01 threshold is a design choice). Measured κ reproduces #590's table (fluorescent R(20) 0.835; LEDs 0.0384, 0.0306, 0.0167; CIE A, Planck 2700 and 6500 K ≤ 3.0e-4). The default step is resolved after the format and the request's `fast` (the parse order changed: `fast` is now resolved before the sensor options; the light is read early for the measure). Registered as `sensor.spectralStepNanometers`, which joins `SIMULATE_CAPTURE_FAST_OPTIONS` and `SIMULATE_CAPTURE_COST_OPTIONS` (owner decision 2026-11-11): under a spiky light with the default step `diagnostics.fast.applied` reports it (true on fast requests, with `assurance: "approximation-with-a-priori-bound"`; false on exact ones), so a DNG join refuses fast parts; otherwise no key is added. White under the fluorescent source, exact: 5 nm within 0.34 % of 1 nm per channel; 20 nm 70 % off in one. Cost of 5 nm against 20 nm on a 384 × 256 DNG (tabulated path): 44 against 40 ms. **Measured, not changed (owner: decide from the numbers):** `sensor.pixelAperture.samplesPerAxis` (default 4) against 16, noise-free expected values on 96 × 64 frames: RMS error 1.75e-2 / 4.1e-3 / 1.25e-2 of the peak (Siemens star / slanted edge / zone plate) at 4, 6.1e-3 / 1.1e-3 / 2.1e-3 at 8; cost on a 384 × 256 DNG 43 / 53 / 90 / 242 ms at 1 / 2 / 4 / 8. `sensor.temporalSampleCount` (default 8 when moving) against 64: RMS error 0.125 (subject, a long streak) and 0.18 (rotation) at 8, 0.074 and 0.096 at 16; a 384 × 256 DNG with a moving subject 6.8 / 13.2 / 25.9 / 51.3 s at 4 / 8 / 16 / 32. The error follows the streak length, so a fixed count cannot bound it; both defaults are left as they are and the choice is put to the owner. Diffraction radius and bokeh zones stay exact-only. Tests: `test/spectral-step-default.test.ts` (new).
48
+
49
+ ### #593, #601, #602 Display view: luminance tone curves, highlight reconstruction, desaturation
50
+
51
+ - **New (#593):** `simulateDisplayView` and `prepareDisplayView` render the luminance tone curves (`extended-reinhard-luminance`, `naka-rushton-luminance` with `toneCurveParameters`) and `output.highlightMode: "reconstruct"`, which they refused through 2.21.0. Photivra's own looks for the app: Standard (Naka–Rushton n = 1.5, Lw = 8, σ = 0.5009) and Flat (n = 1, Lw = 32, σ = 0.8465), both keeping 18 % gray at 0.18; request parameters, not engine presets. A tone curve's moments come from conditioning each developed component on luminance; `reconstruct` (and #601's desaturation), where a pixel reaches it, from a 3-D Gauss–Hermite rule over the whole development chain (`src/facade/display-view-rendering.ts`; docs/DISPLAY_VIEW.md, "Coupled rendering"). Reported in the assumption `view.rendering`.
52
+ - **New (#601):** `output.gamutHandling: "desaturate-to-luminance"` (opt-in; default `"clip-components"`) and the same value of `SdrRenderingProfile.gamutHandling`: after the tone curve, a pixel whose largest component exceeds 1 moves toward the gray of its own luminance until that component is 1, keeping luminance and hue. sRGB rendering only; captures (CPU), the display view (CPU and GPU). The image-kernel accelerator's WebGPU SDR kernels refuse it (`unsupported-on-backend`; their fallback runs the CPU reference).
53
+ - **New (#602):** `output.highlightMode: "reconstruct-neighbors"` (opt-in, an estimate): clipped channels rebuilt from the color ratios of the unclipped pixels around each clipped region (widened by 2 pixels, whose demosaic reads clipped sites): the ring's mean ratios (fast) or their harmonic interpolation inward (full). Captures, the ideal reference and the CPU display view; not with `output.region`; not on the GPU in 2.22.0 (`unsupported-on-backend`). `RawHighlightDiagnostics.mode` gains the value; `SensorColorDevelopment` gains `developWhiteBalancedInto`.
54
+ - **GPU (#593, #601):** the Speed kernel and the Quality pixel pass run the luminance tone curves (every exponent), `reconstruct` and the desaturation, with the fast and the full rules, from one on-demand WGSL module (`src/gpu/wgsl-display-view-coupled.ts`) with first-order running bounds (docs/WEBGPU.md, "Coupled rendering steps"); undecided threshold decisions are recomputed on the host. Measured on the float32 emulation and on a hardware adapter (Apple, Metal 3): every component inside its bound, at most one code.
55
+ - **Speed versus accuracy:** registry entries `output.toneCurve/view-luminance-moments` (fast: 8-point Gauss–Hermite in luminance; full: composite 4 × 16-point Gauss–Legendre), `output.highlightMode.reconstruct/view-moments` (fast: 6 points per axis; full: 16), `prepareDisplayView.gpu/view-coupled-moments` (the GPU kernels with either rule) and `output.highlightMode.reconstruct-neighbors/region-ratios` (fast: ring mean; full: harmonic fill). Fast on screen, full for exports and `fast: false`. Measured fast-versus-full gaps: tone 6.7·10⁻⁶; chain 1.5·10⁻² linear at channel σ up to 0.076 (2.4·10⁻⁴ at σ ≤ 0.01).
56
+ - **On demand:** the coupled steps and the neighbor reconstruction load with the CPU Quality kit (#594), and the GPU coupled steps (`src/gpu/wgsl-display-view-coupled.ts`) on their own, only for requests that ask for them; the default path imports their types and a 0.9 KB WGSL stub only.
57
+ - **Unchanged:** requests without these options are byte-identical (the display-view hash locks pass unchanged).
58
+ - **Size (owner decision: no trimming; the code moves on demand with #594):** measured on the part alone, the display view's added entry size was 49,573 B minified (budget 46,500 B); after the #594 split it is 38,370 B (see "#594 On-demand Quality code").
59
+ - **Tests:** `test/display-view-coupled-rendering.test.ts`, `test/display-view-coupled-gpu.test.ts`, `test/neighbor-highlight-reconstruction.test.ts` (new); coupled cases in `test/display-view-speed-gpu.test.ts`, `test/webgpu/display-view.webgpu.ts` and `test/fast-methods-registry.test.ts`.
60
+
61
+ ### The display view's AHD low-signal correction removed; per-site wording
62
+
63
+ The owner decided (2026-11-11) to drop the display view's AHD low-signal correction, which was kept as precomputed values on a grid of the three channel signal levels. Computing it per site at use instead was measured at about 240–1300× the view's time (one evaluation 18 ms with 2048 draws, 74 ms with 8192), so the correction is removed on every path rather than replaced. The view's signals stay integrated per site at use.
64
+
65
+ - **Changed (default change, display view with `output.demosaic: "ahd"`):** no low-signal correction on the CPU (fast and `fast: false`) or in the GPU Quality kernel. `view.demosaic` is now `{ capture: "ahd", view: "ahd-candidates-with-noise-free-choice", lowSignalNoiseChoice: "not-modelled", scientificStatus: "approximation", measuredBias8BitCodes }` (before: `ahd-candidates-with-low-signal-correction` with `correctedPixels`, `correctionDrawsPerState` and `correctionThresholdLinearStd`). Measured shift of the view against 2.21.0, mean over a centred 32 × 32 block at f/8: 1.4 µm at ISO 6400, red +3.8 to +4.6 codes (8-bit; at most 6.7 in a pixel); 6 µm at ISO 6400, within 0.3 code; the app's 6 µm ISO 400 Quality request, within 0.01 code. A view without AHD, or whose pixels' noise stayed under the old 0.01 threshold, is byte-identical.
66
+ - **Changed (fast methods):** the registry entry `output.demosaic.ahd/view-low-signal-correction` is removed; AHD has no fast method in the view either, so `diagnostics.fast.applied["output.demosaic.ahd"]` is false and an AHD view on its own reports `assurance: "engine-reference"` (the approximation is labelled in `view.demosaic`). `fast` and `fast: false` give the same AHD view.
67
+ - **Deprecated (kept until 3.0.0, documented in docs/DISPLAY_VIEW.md, "AHD at low signal"):** `serializeDisplayViewCorrectionStates()` returns `[]`; `bindDisplayViewCorrectionStates(entries)` checks its entries as before and returns 0; `prepared.correctionKey()` returns null; `prepared.collectCorrectionStates(view)` and `prepared.computeCorrectionStates(gridKeys)` check their arguments as before and return no keys or states; `diagnostics.ahdCorrectedPixelCount` is always 0. The types `DisplayViewCorrectionStateSet` and `DisplayViewCorrectionStateKeys` stay. No root export is removed.
68
+ - **Changed (label only):** the capture's per-site expected-signal path (#394) reports `raw.expectedSignal` `method: "per-site-integrated-electrons"` (before: `tabulated-expected-electrons`); its explanation says each site's electrons are integrated from its materials' spectra when the site is read, as since 2.21.0. RAW codes and samples are unchanged.
69
+ - **Tests:** the display view's uniform AHD hash lock (the one case where the correction applied) is re-recorded on x64 and arm64; the low-signal AHD comparison against the capture takes a 5-code limit for the 1.4 µm ISO 6400 case (4.25 measured); the state-exchange tests become one deprecation test; the GPU Quality tests drop the state-store test.
70
+
71
+ - **Changed (label only):** `lens.catalogue`'s fast method is described as what it has done since 2.21.0: each site's expected electrons are integrated from its taps' material spectra when the site is read. The `raw.expectedSignal` assumption's `method` of a capture that took this fast method is now `per-site-integrated-electrons-catalogue-psf-field-grid` (before: `tabulated-expected-electrons-catalogue-psf-field-grid`); its explanation and the fast-methods registry entry `lens.catalogue/field-grid` say the same. Samples, RAW codes and every other assumption field are unchanged. Code that matched the old `method` string must match the new one.
72
+
73
+ ### #165 Content Credentials writers (engine half)
74
+
75
+ - **New (#165, engine half):** `bindC2paManifestStore`, `calculateC2paDataHash`, `setC2paManifestStore` and `parseC2paManifestStore` embed a caller-built C2PA Manifest Store in a JPEG (APP11 segments right after APP0, as the reference SDK writes them, C2PA 2.4 §A.3.1), PNG (a `caBX` chunk right after IHDR, §A.3.2) or DNG/TIFF (tag 52545 in the main IFD, store at the end of the file, §A.3.6); return the `c2pa.hash.data` exclusions (§18.5.3: every C2PA APP11 segment; §18.5.4: the whole caBX chunk; §18.5.5: the entry's count field and the store); compute the SHA-256 over the bound bytes; write the signed store in place at exactly the placeholder's length (§10.4), so no offset moves; and find an existing store and its binding without verifying signatures. `C2PA_ALGORITHMIC_MEDIA_SOURCE_TYPE` (owner decision: IPTC `algorithmicMedia`) and `C2PA_MANIFEST_STORE_MAXIMUM_BYTES` (32 MiB). No keys, signing, manifest building or network calls in the engine. See [Content Credentials](https://github.com/Photivra/photivra/blob/main/docs/CONTENT_CREDENTIALS.md).
76
+ - **Root exports:** 15 new (6 runtime: 4 functions, 2 constants; 9 types). Nothing is removed and no existing output changes.
77
+ - **Sizes:** the app's capture-worker shape is unchanged by this part (the module is imported by nothing in the capture path; `test/capture-bundle.test.ts` asserts it stays out). An application importing the four functions adds 4,883 B gzip (11,978 B minified; rolldown 1.2.9).
78
+ - **Pending:** validation of signed files with the C2PA reference tooling (no validator offline, and the engine cannot sign).
79
+
80
+ ### #594 On-demand Quality code
81
+
82
+ - **Changed (bundling, #594):** the display view's GPU Quality host and its WGSL (`display-view-quality-gpu.ts`,
83
+ `wgsl-display-view-quality-kernel.ts`, `wgsl-exact-sum.ts`) load on demand, at the first GPU render that takes the Quality
84
+ passes (diffraction, AHD, more query offsets than the Speed kernel holds, a defocused view, an image-plane print). The combined
85
+ renderer moves to `src/gpu/display-view-gpu-renderer.ts`; the Speed path no longer imports the Quality host. Results are
86
+ unchanged: GPU renders through `prepareDisplayView` on the float32 emulation (two Quality, two defocus and one Speed request)
87
+ give the same sample and bound hashes before and after.
88
+ - **Changed (diagnostics timing, owner decision 2026-11-10):** the Quality kernels' two self-check cases run before a view's
89
+ first Quality render instead of before its first GPU render. A Speed-only view's `conformance.cases` lists the two built-in
90
+ cases and the Speed case; a view that takes the Quality passes appends the two Quality cases. A failure keeps that render and
91
+ every later one on the CPU, labelled `conformance-failed`. Internal: the renderer contract gains `onDemandConformanceCases`.
92
+ - **New:** `prepareQualityPaths()` (root export; owner decision 2026-11-10) loads the on-demand full-quality code ahead of time,
93
+ for example when the user opens Export. Optional, single flight, idempotent, no change to results; a failed load rejects with
94
+ `ImageKernelExecutionError` reason `quality-code-unavailable` and is retried by the next call or request. It loads the GPU
95
+ Quality chunk and the two CPU kits.
96
+ - **New fallback reason:** `quality-code-unavailable` (in `DisplayViewGpuFallbackReason` and `ImageKernelFailureReason`). If the
97
+ Quality chunk does not load, that render runs the CPU reference (the same full-quality result, slower), labelled; with
98
+ `gpu: "require"` it fails with that reason. Not sticky: the next render that needs the code loads it again. A failed
99
+ self-check stays sticky.
100
+ - **Changed (bundling, CPU, #594):** the CPU full-quality code is one on-demand chunk, the Quality kit (`src/facade/quality-kit.ts`:
101
+ diffraction, the capture's AHD demosaic, `lens.defocus` beyond the geometric disc (bokeh shapes, cat's eye, the large-disc
102
+ lattice) and the image-space provider (lens projection, distortion, vignetting, rotation, a moving subject), the display view's
103
+ coupled rendering steps and the neighbor highlight reconstruction), and the opt-in features another, the opt-in kit
104
+ (`src/facade/opt-in-kit.ts`: `sensor.profile`, `illumination`, `motion.subject`). Each loads once where the request is parsed
105
+ and is then called synchronously; every hash lock passes. The app's geometric-disc focus control stays on the fast path. Not
106
+ moved: the native-photosite report (#595; every capture reports it), the exposure build-up hooks (255 B, in the RAW
107
+ producers) and the display view's inline AHD candidates. `ImageKernelExecutionError` moves to `src/core/execution-error.ts`
108
+ (re-exported unchanged) so the loader adds no GPU code to the first load.
109
+ - **Size (app capture-worker shape, rolldown 1.2.9, gzip 9):** after the GPU split, fast worker 363,695 → 344,287 B and GPU
110
+ Quality chunk 20,026 B on demand (all chunks +618 B: two chunks compress separately; owner-accepted). With every part merged
111
+ the fast worker measured 358,680 B; after the CPU split it is **337,413 B** and on demand **46,977 B** (GPU Quality 19,401 B,
112
+ CPU Quality kit 18,249 B, GPU coupled steps 5,159 B, opt-in kit 4,168 B); all chunks 384,390 B. `test/capture-bundle.test.ts`
113
+ checks the owner's two rows, each its measurement rounded up to the next 100 B (owner answer, 2026-11-10): fast worker
114
+ ≤ 337,500 B, on demand ≤ 47,000 B, in place of 2.21.0's 363,700 B all-chunk limit. The bundler now gives the entry ten small
115
+ chunks it shares with the kits, so the display view's added size is measured on the entry and those chunks: 38,370 B minified
116
+ and 14,126 B gzip (budgets 46,500 B and 18,550 B, unchanged; 2.21.0: 46,270 B and 18,327 B). The app adopts the two rows
117
+ at app round 16.
118
+ - **Root exports:** 1 new (runtime): `prepareQualityPaths`.
119
+
120
+ ## 2.21.0 — published November 10, 2026
4
121
 
5
122
  A minor release that combines the 2.21.0 and 2.22.0 parts (owner decision 2026-11-10): the chart-signal design choice on every render path, sensor profiles and phone lenses (#554), the image-plane scene (#581), catalogue-lens boundary ties (#588) and lateral color (#589), exposure build-up (#534) with subject motion (#551) and mixed lighting (#550), the lateral color fringe width (#580), Photivra's own light-source spectra (#557) and the print planner's contain fit (#365). See the [2.21.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_21_0.md).
6
123
 
package/CITATION.cff CHANGED
@@ -9,7 +9,7 @@ abstract: "An open TypeScript engine for the science and mathematics of photogra
9
9
  repository-code: "https://github.com/photivra/photivra"
10
10
  url: "https://photivra.com"
11
11
  license: "Apache-2.0"
12
- version: "2.21.0"
12
+ version: "2.22.0"
13
13
  keywords:
14
14
  - photography
15
15
  - camera optics
package/README.md CHANGED
@@ -32,10 +32,10 @@ The source is licensed under Apache-2.0.
32
32
 
33
33
  ## Quick start
34
34
 
35
- Install the published ESM package below. The [2.21.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_21_0.md) and [changelog](https://github.com/Photivra/photivra/blob/main/CHANGELOG.md) describe the current release:
35
+ Install the published ESM package below. The [2.22.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_22_0.md) and [changelog](https://github.com/Photivra/photivra/blob/main/CHANGELOG.md) describe the current release:
36
36
 
37
37
  ```sh
38
- npm install @photivra/engine@2.21.0
38
+ npm install @photivra/engine@2.22.0
39
39
  ```
40
40
 
41
41
  Simulate a photograph of a test chart with a generic camera, then read the developed sRGB pixels:
@@ -75,7 +75,7 @@ console.log(horizontal.provenance);
75
75
 
76
76
  Start with the [Developer Guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Then use the [Usage Guide](https://github.com/Photivra/photivra/blob/main/docs/USAGE.md) and the [API reference](https://github.com/Photivra/photivra/blob/main/docs/API_REFERENCE.md), which is generated from the package exports.
77
77
 
78
- Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.21.0` tag.
78
+ Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.22.0` tag.
79
79
 
80
80
  ## Documentation
81
81
 
@@ -363,14 +363,14 @@ The application can then use those results, while the photography calculations t
363
363
 
364
364
  ## Status
365
365
 
366
- - Repository package version: `2.21.0` (release candidate prepared November 10, 2026; publication is separate)
367
- - Engine API contract: `2.21.0`
366
+ - Repository package version: `2.22.0` (release candidate prepared November 11, 2026; publication is separate)
367
+ - Engine API contract: `2.22.0`
368
368
  - Versioned generic tier reference assets: [GENERIC_TIER_PRESETS.md](https://github.com/Photivra/photivra/blob/main/docs/GENERIC_TIER_PRESETS.md).
369
369
  - Sensor profiles from published specifications (#554, unnamed): [SENSOR_PROFILES.md](https://github.com/Photivra/photivra/blob/main/docs/SENSOR_PROFILES.md).
370
370
  - Composed POC simulation API contract: `1.0.0`
371
371
  - Stability: 1.0 root-package compatibility policy; bounded scientific models and independent POC contract
372
372
 
373
- From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.21.0; existing archives keep their original creator identities. Schema, model and POC contract versions stay independent.
373
+ From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.22.0; existing archives keep their original creator identities. Schema, model and POC contract versions stay independent.
374
374
 
375
375
  A breaking change to the root contracts now needs a new package major version. Additive and corrective changes follow [compatibility and migration](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_1_3_0.md). A 1.0 package is not a claim of calibrated physical accuracy.
376
376
 
package/SECURITY.md CHANGED
@@ -4,7 +4,7 @@ Please do not open a public issue that contains vulnerability details.
4
4
 
5
5
  ## Supported versions
6
6
 
7
- The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.20.0**, published on November 10, 2026 (see [CHANGELOG.md](CHANGELOG.md)).
7
+ The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.21.0**, published on November 10, 2026 (see [CHANGELOG.md](CHANGELOG.md)).
8
8
 
9
9
  Security fixes target the current maintained development and release line. Do not assume that older 1.x releases, snapshots or pre-1.0 releases will receive fixes.
10
10
 
package/THIRD_PARTY.md CHANGED
@@ -14,11 +14,20 @@ This file records direct development and CI dependencies intentionally used by t
14
14
  | Vitest | 5.0.1 | Unit/integration testing | MIT |
15
15
  | @vitest/coverage-v8 | 5.0.1 | V8-backed test coverage | MIT |
16
16
  | rolldown | 1.2.9 | Bundling test of capture-only consumers (`test/capture-bundle.test.ts`) | MIT |
17
+ | jxl-oxide-wasm | 0.12.6 | JPEG XL tile decoding in the phone DNG import tests (`test/dng-import-phone.test.ts`); see [JPEG XL](#jpeg-xl-2220) | MIT OR Apache-2.0 |
17
18
  | actions/checkout | v7 / `3d3c42e5aac5ba805825da76410c181273ba90b1` | CI checkout | MIT |
18
19
  | actions/setup-node | v7 / `820762786026740c76f36085b0efc47a31fe5020` | CI Node setup | MIT |
19
20
  | actions/upload-artifact | v7.0.1 / `043fb46d1a93c77aae656e7c1c64a875d1fc6a0a` | Verified release artifact transfer | MIT |
20
21
  | actions/download-artifact | v8.0.1 / `3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c` | Verified release artifact retrieval | MIT |
21
22
 
23
+ ## JPEG XL (2.22.0)
24
+
25
+ DNG 1.7 phone files may store their raw data as JPEG XL (Compression 52546; #598). The owner chose the jxl-oxide decoder (owner, 2026-11-10), as its prebuilt WebAssembly package from npm, `jxl-oxide-wasm` 0.12.6 (<https://github.com/tirr-c/jxl-oxide>), dual-licensed MIT OR Apache-2.0. **Chosen licence: Apache-2.0** (owner-accepted 2026-11-11, with the licence check's rule that an SPDX `OR` expression passes when one choice is allowed). It is a **devDependency only**: the published package declares no runtime dependency and does not import it. `createJxlOxideDngDecoder` takes the module the application loaded (structurally typed), so an application that opens JPEG XL DNGs installs and lazily loads jxl-oxide-wasm itself, and its licence then applies to that application. No jxl-oxide source is vendored or copied. `scripts/check-license-policy.mjs` accepts an SPDX `OR` expression when one of its choices is on the allowlist (here Apache-2.0 and MIT both are).
26
+
27
+ Patents (rule 2; not legal advice): JPEG XL is ISO/IEC 18181. At the 2026-11-10 research (#598), royalty-free patent declarations on JPEG XL had been filed by Google and Cloudinary only; Google's patent grant text covers its own implementations (libjxl, jxl-rs), not jxl-oxide. The owner accepted jxl-oxide on that basis (2026-11-10). The engine itself contains no JPEG XL coding: its tests carry a small test-only lossless encoder (`test/helpers/phone-dng-fixtures.ts`, written from ISO/IEC 18181-1:2022, modular mode with the zero predictor and prefix codes) to build fixtures, never shipped.
28
+
29
+ The 2.22.0 DNG opcode, ProfileGainTableMap and RGBTables readers (`src/capture/dng-opcodes.ts`, `dng-profile-maps.ts`) were written from the DNG specification's tag descriptions; because the 1.7.1.0 PDF could not be downloaded (HTTP 403), their byte layouts and evaluation order were only checked by reading the Adobe DNG SDK 1.7.1 source. No code was copied from the DNG SDK (layouts and behaviour checked only; the engine's code is written independently); the PDF check is pending with the owner; the Adobe DNG notice in `NOTICE` covers the DNG technology.
30
+
22
31
  Node built-in modules are used only by development tooling and the repository-local POC API; that API is excluded from the published package and the built-ins are not third-party dependencies.
23
32
 
24
33
  `src/sensor/stochastic-seed.ts` independently implements the MurmurHash3_x86_32 algorithm and its `fmix32` finalizer (Austin Appleby). The upstream `MurmurHash3.cpp` header states the algorithm "is placed in the public domain" and disclaims copyright. The SMHasher test suite that contains it is a separate work: its README says "SMHasher is released under the MIT license". No SMHasher or MurmurHash3 source file is vendored or copied. `test/sensor-raw-noise-independence.test.ts` reimplements, in its own code, SMHasher's verification procedure (keys of 0–255 bytes hashed with seed 256 − i, then the hash of those hashes) and compares the result with the published MurmurHash3_x86_32 verification value 0xB0F57EE3, a published fact. Test vectors earlier copied from a Stack Overflow answer (CC BY-SA 3.0) were removed.
@@ -27,6 +36,8 @@ Node built-in modules are used only by development tooling and the repository-lo
27
36
 
28
37
  `src/optics/longitudinal-chromatic-aberration.ts` uses three physical constants: the Fraunhofer d (helium, 587.5618 nm), F (hydrogen, 486.1327 nm) and C (hydrogen, 656.2725 nm) spectral-line wavelengths used to define the Abbe number (as tabulated in SCHOTT Technical Information TIE-29, *Refractive Index and Dispersion*, 2023, free; also the reference wavelengths of ISO 7944). They are factual measured constants of nature, not a copied dataset or table; no glass catalogue data is included and refractive index and Abbe number are caller inputs. The 2.0 projection, distortion, Zernike/Seidel, vignetting, polarizer and haze models are independent implementations of published equations cited in `docs/PHYSICS_FOUNDATION.md` and `docs/PSF_FOUNDATION.md`; they contain no third-party code or numeric tables.
29
38
 
39
+ `src/capture/c2pa-embed.ts` (#165) is written from the C2PA Technical Specification 2.4 (free, published by the C2PA) and ISO 19566-5 box framing as that specification describes it. To produce the same JPEG and PNG bytes as the C2PA reference SDK, it uses interoperability facts read from c2pa-rs `sdk/src/asset_handlers/jpeg_io.rs` and `png_io.rs` (MIT OR Apache-2.0): the 64,000-byte split of the store across APP11 segments, the box instance number `En` = 0x0211 and the segments' place right after the last APP0 (or SOI); and the PNG `caBX` chunk's place right after IHDR with the whole chunk excluded. No c2pa-rs source is vendored or copied, and nothing is a runtime dependency. Checking a signed file with the C2PA reference validator is pending (docs/RELEASE_2_22_0.md, "Pending"); the engine signs nothing and makes no network request.
40
+
30
41
  This inventory does not replace a transitive dependency/SBOM review before a release. New runtime dependencies, scientific datasets, model weights, or calibration material require explicit provenance and license review.
31
42
 
32
43
  ## Pending licence and source reviews
@@ -0,0 +1,116 @@
1
+ /** IPTC digital source type for media created purely by an algorithm (https://cv.iptc.org/newscodes/digitalsourcetype/). */
2
+ export declare const C2PA_ALGORITHMIC_MEDIA_SOURCE_TYPE: "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia";
3
+ /** Largest manifest store the engine embeds or reads, bytes (an engine limit, not a C2PA one). */
4
+ export declare const C2PA_MANIFEST_STORE_MAXIMUM_BYTES = 33554432;
5
+ /** A byte range of a file: `start` is a zero-based offset, `length` a byte count. */
6
+ export interface C2paByteRange {
7
+ start: number;
8
+ length: number;
9
+ }
10
+ /**
11
+ * Where the manifest store sits in a file and which bytes the data hash excludes. Plain data: it can be kept, sent to a
12
+ * worker or serialized, and must be passed back unchanged to `setC2paManifestStore`.
13
+ */
14
+ export interface C2paHardBinding {
15
+ /** `image/jpeg`, `image/png`, or for TIFF files `image/dng` when the main IFD has DNGVersion (50706), otherwise `image/tiff`. */
16
+ mediaType: "image/jpeg" | "image/png" | "image/dng" | "image/tiff";
17
+ /** Length of the file with the store embedded, bytes. */
18
+ fileLength: number;
19
+ /** The `exclusions` of the `c2pa.hash.data` assertion, ascending and non-overlapping (§18.5). */
20
+ exclusions: readonly C2paByteRange[];
21
+ /** JPEG: every C2PA APP11 segment, markers and length fields included. PNG: the whole caBX chunk. TIFF/DNG: the store's own bytes. */
22
+ manifestRegion: C2paByteRange;
23
+ /** Length of the JUMBF manifest store itself, bytes. A replacement store must have exactly this length. */
24
+ manifestStoreLength: number;
25
+ }
26
+ /** A file with a manifest store embedded, and its binding. */
27
+ export interface C2paBoundFile {
28
+ bytes: Uint8Array;
29
+ binding: C2paHardBinding;
30
+ }
31
+ /** A manifest store found in a file (`parseC2paManifestStore`): its binding and a copy of the store's JUMBF bytes. */
32
+ export interface C2paFoundManifestStore {
33
+ binding: C2paHardBinding;
34
+ manifestStore: Uint8Array;
35
+ }
36
+ /** Input of `bindC2paManifestStore`. */
37
+ export interface BindC2paManifestStoreInput {
38
+ /** A JPEG, a PNG, or a TIFF-based file such as a DNG, as written by the engine's exporters. It is read, never changed. */
39
+ file: Uint8Array;
40
+ /**
41
+ * The placeholder store (§10.4): a JUMBF C2PA Manifest Store sized for the final signature. For JPEG it may instead be the
42
+ * same store already split into APP11 segments (what the C2PA reference SDK's placeholder call returns), as long as that
43
+ * split is the one described above.
44
+ */
45
+ manifestStore: Uint8Array;
46
+ }
47
+ /** Input of `setC2paManifestStore`. */
48
+ export interface SetC2paManifestStoreInput {
49
+ /** The bound file (`C2paBoundFile.bytes`). It is read, never changed. */
50
+ file: Uint8Array;
51
+ /** The binding `bindC2paManifestStore` (or `parseC2paManifestStore`) returned for it. */
52
+ binding: C2paHardBinding;
53
+ /** The final, signed store: exactly `binding.manifestStoreLength` bytes, as JUMBF or (JPEG) in the same APP11 split. */
54
+ manifestStore: Uint8Array;
55
+ }
56
+ /** Input of `parseC2paManifestStore`. */
57
+ export interface ParseC2paManifestStoreInput {
58
+ /** A JPEG, PNG or TIFF-based file such as a DNG. It is read, never changed. */
59
+ file: Uint8Array;
60
+ }
61
+ /** Input of `calculateC2paDataHash`. */
62
+ export interface CalculateC2paDataHashInput {
63
+ /** The bound file. */
64
+ file: Uint8Array;
65
+ /** The binding's exclusions (or any ascending, non-overlapping ranges inside the file). */
66
+ exclusions: readonly C2paByteRange[];
67
+ }
68
+ /** SHA-256 over every byte of the file outside the exclusions, in file order (§18.5): the `hash` of `c2pa.hash.data`. */
69
+ export interface C2paDataHash {
70
+ alg: "sha256";
71
+ hash: Uint8Array;
72
+ hashHex: string;
73
+ hashedByteCount: number;
74
+ }
75
+ /**
76
+ * Embeds a placeholder C2PA Manifest Store in a JPEG, PNG or TIFF/DNG export and returns the new file with its hard binding:
77
+ * the exclusions for `c2pa.hash.data` and where the store sits (C2PA 2.4 §A.3.1, §A.3.2, §A.3.6, §18.5, §10.4; see the module
78
+ * notes). The input file is not changed. Only the first store of a file can be added; a file that already has one is rejected.
79
+ * @param input - The file and the placeholder store.
80
+ * @returns The bound file (a new array) and its binding.
81
+ *
82
+ * @see docs/CONTENT_CREDENTIALS.md for the workflow and the byte layouts.
83
+ */
84
+ export declare function bindC2paManifestStore(input: BindC2paManifestStoreInput): C2paBoundFile;
85
+ /**
86
+ * Finds the C2PA Manifest Store embedded in a JPEG (APP11), PNG (caBX) or TIFF/DNG (tag 52545) and returns its location as a
87
+ * binding (the exclusions the engine's own writer uses for that format) and a copy of the store. It checks the file's structure
88
+ * and the store's outer framing only: it does not read the manifests, check the data hash or verify any signature. The
89
+ * manifest's own `c2pa.hash.data` assertion is what a validator uses; this binding is what `setC2paManifestStore` needs.
90
+ * @param input - The file.
91
+ * @returns The store and its binding, or null when the file has none.
92
+ *
93
+ * @see docs/CONTENT_CREDENTIALS.md for the byte layouts.
94
+ */
95
+ export declare function parseC2paManifestStore(input: ParseC2paManifestStoreInput): C2paFoundManifestStore | null;
96
+ /**
97
+ * Writes the final, signed manifest store over the placeholder of a bound file (C2PA 2.4 §10.4.4). The store must have
98
+ * exactly the placeholder's length, so the file's length, every offset and every byte outside the exclusions stay the same
99
+ * and the data hash computed before signing still holds. The binding is checked against the file first. The new store goes
100
+ * into the file's existing segments or chunk as they are (for PNG the chunk CRC is recomputed; it is inside the exclusion).
101
+ * @param input - The bound file, its binding and the signed store.
102
+ * @returns A new array: the file with the signed store.
103
+ *
104
+ * @see docs/CONTENT_CREDENTIALS.md for the workflow.
105
+ */
106
+ export declare function setC2paManifestStore(input: SetC2paManifestStoreInput): Uint8Array;
107
+ /**
108
+ * SHA-256 over every byte of a file outside the given exclusions, in file order: the value of `hash` in a `c2pa.hash.data`
109
+ * assertion with `alg` "sha256" (C2PA 2.4 §18.5). It uses Web Crypto (`crypto.subtle`, browsers in secure contexts and
110
+ * Node 22+). The bytes outside the exclusions are copied once into one array before hashing.
111
+ * @param input - The file and its exclusions (ascending, non-overlapping, inside the file).
112
+ * @returns The digest as bytes and hexadecimal, and how many bytes it covers.
113
+ *
114
+ * @see docs/CONTENT_CREDENTIALS.md for the workflow.
115
+ */
116
+ export declare function calculateC2paDataHash(input: CalculateC2paDataHashInput): Promise<C2paDataHash>;