@photivra/engine 2.12.0 → 2.14.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 (148) hide show
  1. package/CHANGELOG.md +144 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -7
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +3 -0
  6. package/dist/camera/lens-projection.js +2 -1
  7. package/dist/camera/lens-projection.js.map +1 -1
  8. package/dist/capture/dng-import.js +1 -1
  9. package/dist/capture/dng-import.js.map +1 -1
  10. package/dist/capture/export-jpeg.d.ts +1 -1
  11. package/dist/capture/export-jpeg.js +6 -4
  12. package/dist/capture/export-jpeg.js.map +1 -1
  13. package/dist/capture/host-yield-cadence.js +1 -1
  14. package/dist/capture/host-yield-cadence.js.map +1 -1
  15. package/dist/capture/native-environment-optical-stage.js +1 -1
  16. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  17. package/dist/capture/native-raw.js +1 -1
  18. package/dist/capture/native-raw.js.map +1 -1
  19. package/dist/capture/packed-sensor-raw.js +3 -3
  20. package/dist/capture/packed-sensor-raw.js.map +1 -1
  21. package/dist/capture/sensor-raw-producer.js +1 -1
  22. package/dist/capture/sensor-raw-producer.js.map +1 -1
  23. package/dist/color/capture-color.js +3 -3
  24. package/dist/color/capture-color.js.map +1 -1
  25. package/dist/color/chromatic-adaptation.js +2 -2
  26. package/dist/color/chromatic-adaptation.js.map +1 -1
  27. package/dist/color/color-matching-functions.js +1 -1
  28. package/dist/color/color-matching-functions.js.map +1 -1
  29. package/dist/color/colorimetry-math.js +2 -2
  30. package/dist/color/colorimetry-math.js.map +1 -1
  31. package/dist/color/illuminants.js +3 -3
  32. package/dist/color/illuminants.js.map +1 -1
  33. package/dist/color/rgb-color-spaces.js +1 -1
  34. package/dist/color/rgb-color-spaces.js.map +1 -1
  35. package/dist/color/sensor-color-development.js +1 -1
  36. package/dist/color/sensor-color-development.js.map +1 -1
  37. package/dist/core/record-validation.js +3 -3
  38. package/dist/core/record-validation.js.map +1 -1
  39. package/dist/core/version.d.ts +1 -1
  40. package/dist/core/version.js +1 -1
  41. package/dist/core/version.js.map +1 -1
  42. package/dist/equipment/generic-prime-lenses.js +3 -3
  43. package/dist/equipment/generic-prime-lenses.js.map +1 -1
  44. package/dist/equipment/generic-tier-assets.d.ts +23 -1
  45. package/dist/equipment/generic-tier-assets.js +58 -30
  46. package/dist/equipment/generic-tier-assets.js.map +1 -1
  47. package/dist/equipment/generic-tier-presets.d.ts +6 -3
  48. package/dist/equipment/generic-tier-presets.js +26 -13
  49. package/dist/equipment/generic-tier-presets.js.map +1 -1
  50. package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
  51. package/dist/exposure/auto-iso-slowest-shutter.js +217 -0
  52. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
  53. package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
  54. package/dist/exposure/exposure-mode-resolver.js +238 -33
  55. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  56. package/dist/exposure/flash-ttl.js +1 -1
  57. package/dist/exposure/flash-ttl.js.map +1 -1
  58. package/dist/exposure/flash.js +1 -1
  59. package/dist/exposure/flash.js.map +1 -1
  60. package/dist/facade/chart-material-layers.js +2 -2
  61. package/dist/facade/chart-material-layers.js.map +1 -1
  62. package/dist/facade/chart-radiance-batch.js +1 -1
  63. package/dist/facade/chart-radiance-batch.js.map +1 -1
  64. package/dist/facade/chart-scenes.d.ts +45 -8
  65. package/dist/facade/chart-scenes.js +101 -44
  66. package/dist/facade/chart-scenes.js.map +1 -1
  67. package/dist/facade/color-patches.d.ts +129 -0
  68. package/dist/facade/color-patches.js +209 -0
  69. package/dist/facade/color-patches.js.map +1 -0
  70. package/dist/facade/diffraction-provider.js +12 -13
  71. package/dist/facade/diffraction-provider.js.map +1 -1
  72. package/dist/facade/display-view-gpu.js +1 -1
  73. package/dist/facade/display-view-gpu.js.map +1 -1
  74. package/dist/facade/display-view.d.ts +27 -0
  75. package/dist/facade/display-view.js +234 -65
  76. package/dist/facade/display-view.js.map +1 -1
  77. package/dist/facade/scene-radiance.js +2 -2
  78. package/dist/facade/scene-radiance.js.map +1 -1
  79. package/dist/facade/simulate-capture.d.ts +139 -12
  80. package/dist/facade/simulate-capture.js +209 -36
  81. package/dist/facade/simulate-capture.js.map +1 -1
  82. package/dist/facade/simulate-display-view.js +2 -2
  83. package/dist/facade/simulate-display-view.js.map +1 -1
  84. package/dist/gpu/chart-composite-gpu.js +2 -2
  85. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  86. package/dist/gpu/display-view-composite-material-module.js +1 -1
  87. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  88. package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
  89. package/dist/gpu/display-view-quality-gpu.js +24 -14
  90. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  91. package/dist/gpu/image-kernel-execution.js +2 -2
  92. package/dist/gpu/image-kernel-execution.js.map +1 -1
  93. package/dist/gpu/temporal-sum-gpu.js +23 -23
  94. package/dist/gpu/webgpu-runtime.js +1 -1
  95. package/dist/gpu/webgpu-runtime.js.map +1 -1
  96. package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
  97. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
  98. package/dist/gpu/wgsl-chart-kernel.js +117 -117
  99. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  100. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
  101. package/dist/gpu/wgsl-display-view-quality-kernel.js +522 -488
  102. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  103. package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
  104. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  105. package/dist/gpu/wgsl-kernels.js +203 -203
  106. package/dist/gpu/wgsl-kernels.js.map +1 -1
  107. package/dist/gpu/wgsl-sensor-kernel.js +302 -302
  108. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  109. package/dist/index.d.ts +8 -5
  110. package/dist/index.js +6 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/motion/temporal-exposure-sum.js +1 -1
  113. package/dist/motion/temporal-exposure-sum.js.map +1 -1
  114. package/dist/optics/diffraction-psf-profile.js +4 -4
  115. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  116. package/dist/optics/front-of-lens-filter.js +1 -1
  117. package/dist/optics/front-of-lens-filter.js.map +1 -1
  118. package/dist/optics/irradiance-convolution.d.ts +10 -0
  119. package/dist/optics/irradiance-convolution.js +49 -14
  120. package/dist/optics/irradiance-convolution.js.map +1 -1
  121. package/dist/output/native-capture-sdr.js +1 -1
  122. package/dist/output/native-capture-sdr.js.map +1 -1
  123. package/dist/output/png-encoder.js +2 -2
  124. package/dist/output/png-encoder.js.map +1 -1
  125. package/dist/output/print-jpeg.js +13 -10
  126. package/dist/output/print-jpeg.js.map +1 -1
  127. package/dist/output/text-metadata.d.ts +22 -2
  128. package/dist/output/text-metadata.js +58 -9
  129. package/dist/output/text-metadata.js.map +1 -1
  130. package/dist/sensor/fixed-pattern-noise.js +1 -1
  131. package/dist/sensor/fixed-pattern-noise.js.map +1 -1
  132. package/dist/sensor/readout-exposure-linkage.js +1 -1
  133. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  134. package/docs/API_REFERENCE.md +49 -1
  135. package/docs/QUICK_START.md +104 -9
  136. package/docs/USAGE.md +241 -2
  137. package/docs/api/core-version.md +1 -1
  138. package/docs/api/equipment-generic-tier-assets.md +41 -1
  139. package/docs/api/equipment-generic-tier-presets.md +11 -3
  140. package/docs/api/exports.json +172 -2
  141. package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
  142. package/docs/api/exposure-exposure-mode-resolver.md +68 -0
  143. package/docs/api/facade-chart-scenes.md +36 -5
  144. package/docs/api/facade-color-patches.md +150 -0
  145. package/docs/api/facade-simulate-capture.md +140 -9
  146. package/docs/api/output-text-metadata.md +34 -0
  147. package/llms.txt +1 -0
  148. package/package.json +2 -2
package/docs/USAGE.md CHANGED
@@ -5089,10 +5089,72 @@ The limiting constraint is reported as one of:
5089
5089
  - `none`;
5090
5090
  - `iso-minimum`;
5091
5091
  - `iso-maximum`;
5092
+ - `user-iso-minimum` and `user-iso-maximum` (#505, [photographer-set limits](#photographer-set-auto-iso-limits-505));
5092
5093
  - `iso-grid-quantization`.
5093
5094
 
5094
5095
  If Auto ISO capability is `unsupported` or `unknown`, the resolver returns a blocked result. Unknown is not treated as supported.
5095
5096
 
5097
+ ### Photographer-set Auto ISO limits (#505)
5098
+
5099
+ Cameras let the photographer cap Auto ISO, and some also raise its floor: the Nikon D850 has a maximum sensitivity
5100
+ ([Auto ISO control](https://imaging.nikon.com/support/digitutor/d850/functions/autoisocontrol.html)), Fujifilm a default and a
5101
+ maximum sensitivity per preset ([ISO Auto setting](https://fujifilm-dsc.com/en/manual/x-pro2/menu_shooting/iso_auto_setting/index.html)),
5102
+ and Panasonic separate upper and lower Auto ISO limits
5103
+ ([GH5 II manual](https://eww.pavc.panasonic.co.jp/dscoi/DC-GH5M2/html/DC-GH5M2_DVQP2464_eng/0062.html)). That is the
5104
+ photographer's choice, so the engine keeps it apart from what the body allows (`capabilities.iso.autoIsoMinimum` and
5105
+ `autoIsoMaximum`, or the ISO range when the body declares none).
5106
+
5107
+ Every Auto ISO entry point takes an optional `autoIsoLimits: { maximum, minimum? }` (`AutoIsoLimits`):
5108
+
5109
+ | Resolver | Where |
5110
+ | --- | --- |
5111
+ | `resolveManualExposureMode` | `isoControl.autoIsoLimits` (automatic ISO) |
5112
+ | `resolveAperturePriorityAutoIsoExposureMode` | `policy.autoIsoLimits` |
5113
+ | `resolveShutterPriorityExposureMode` | `isoControl.policy.autoIsoLimits` (automatic ISO) |
5114
+ | `resolveProgramAutoExposureMode` | `isoControl.autoIsoLimits` (automatic ISO) |
5115
+ | `resolveFullAutoExposureMode` | `policy.autoIsoLimits` |
5116
+
5117
+ Composition fragment: requires previously validated bindings `capabilities`, `target`. This is an integration sketch, not a standalone runnable program.
5118
+
5119
+ ```ts
5120
+ import {
5121
+ resolveManualExposureMode
5122
+ } from "@photivra/engine";
5123
+
5124
+ const capped =
5125
+ resolveManualExposureMode({
5126
+ target,
5127
+ capabilities,
5128
+ referenceExposure: {
5129
+ aperture: 4,
5130
+ shutterSeconds: 1 / 125,
5131
+ iso: 100
5132
+ },
5133
+ manualAperture: 4,
5134
+ manualShutterSeconds: 1 / 125,
5135
+ isoControl: {
5136
+ kind: "automatic",
5137
+ quantizationPolicy:
5138
+ "nearest-log2-lower-on-tie",
5139
+ autoIsoLimits: { maximum: 6400 }
5140
+ }
5141
+ });
5142
+ ```
5143
+
5144
+ The rules:
5145
+
5146
+ - Both limits are ISO settings: inside the body's Auto ISO range and, on a discrete ISO grid, one of its values, with
5147
+ `minimum` at most `maximum`. `minimum` defaults to the body's Auto ISO minimum. A limit outside the body's range or off
5148
+ the grid fails with `InvalidScientificInputError`, and a malformed record (not an object, an unknown field) with
5149
+ `InvalidConfigurationError`. The engine never clips a limit silently.
5150
+ - The ISO is chosen in the narrowed range, and the baseline of the priority, program and Full Auto policies (the lowest
5151
+ selectable ISO) is the first selectable ISO at or above `minimum`.
5152
+ - When the ISO is held at a limit strictly inside the body's range, the limiting constraint is `user-iso-maximum` or
5153
+ `user-iso-minimum`, with the residual to the target as usual. A limit equal to the body's bound adds nothing; the body's
5154
+ `iso-maximum` or `iso-minimum` is reported.
5155
+ - Without `autoIsoLimits` every result is unchanged.
5156
+ - A separate, lower cap for flash is not part of this: it belongs with flash-aware exposure.
5157
+
5096
5158
  A `no-signal` meter target also blocks Auto ISO, instead of selecting maximum ISO or infinity.
5097
5159
 
5098
5160
  ### Manual ISO
@@ -5258,6 +5320,12 @@ The policy is explicit:
5258
5320
  - `allow-slower-shutter`; or
5259
5321
  - `hold-preferred-shutter` and return residual underexposure.
5260
5322
 
5323
+ With `policy.autoIsoLimits` ([photographer-set limits](#photographer-set-auto-iso-limits-505), #505) the maximum in step 4
5324
+ is the photographer's `maximum`, and both choices behave there exactly as at the body's maximum: the same settings and
5325
+ residual, with `user-iso-maximum` in place of `iso-maximum` when the policy holds the shutter. Shutter Priority
5326
+ (`isoControl.policy.autoIsoLimits`), Program Auto (`isoControl.autoIsoLimits`) and Full Auto (`policy.autoIsoLimits`) take
5327
+ the same limits.
5328
+
5261
5329
  For discrete shutter grids, the shutter chosen before ISO is raised is never longer than the requested or preferred duration. Otherwise rounding to the grid could overexpose, and undoing that would need an ISO below the supported minimum.
5262
5330
 
5263
5331
  ### Shutter Priority + manual ISO
@@ -5320,6 +5388,107 @@ This policy:
5320
5388
 
5321
5389
  That direction is deliberate. Choosing an aperture wider than the ideal at minimum ISO could overexpose, with no lower ISO left to compensate.
5322
5390
 
5391
+ ### Automatic slowest shutter (#504)
5392
+
5393
+ Cameras let the photographer set the slowest shutter before Auto ISO raises the ISO to "Auto", chosen from the focal
5394
+ length, with a bias in 1-stop steps (Sony's ISO AUTO Min. SS: Faster, Fast, Standard, Slow and Slower, 1 EV apart:
5395
+ [help guide](https://helpguide.sony.net/ilc/2020/v1/en/contents/TP1000139958.html); Nikon and Fujifilm: see the manuals
5396
+ cited above). Those cameras base it on the old reciprocal rule, about 1/focal length. That is a rule of thumb, not physics,
5397
+ and the engine does not use it. `estimateAutoIsoSlowestShutter()` derives the threshold from the engine's own camera-shake
5398
+ model, `estimateCameraShakeBlur()` ([camera shake](https://github.com/Photivra/photivra/blob/main/docs/STABILIZATION.md#controlled-camera-shake)), the same physics the preview
5399
+ shows.
5400
+
5401
+ The camera turns at a constant angular velocity ω = (ω_yaw, ω_pitch), reduced by stabilization to r·ω with r = 2^−stops. A
5402
+ still point on the optical axis then moves
5403
+
5404
+ ```text
5405
+ b(t) = v · hypot(tan(r·ω_yaw·t), tan(r·ω_pitch·t))
5406
+ ```
5407
+
5408
+ on the sensor, where v is the focal length (or, with `focusDistanceM`, the thin-lens image distance). The derived shutter is
5409
+ the time t at which b(t) equals the blur budget. On one axis that is `t = atan(budget / v) / (r·|ω|)` exactly; on two axes
5410
+ the engine bisects, which works because b grows strictly with t. Then:
5411
+
5412
+ 1. the bias: `biased = derived · 2^−fasterByStops`, with `fasterByStops` a whole number from −2 to +2 (+1 is one stop faster);
5413
+ 2. the grid, optional: with `shutterCapability`, the biased time snaps to the body's shutter grid, never longer, held at its
5414
+ ends.
5415
+
5416
+ Composition fragment: requires previously validated bindings `capabilities`. This is an integration sketch, not a standalone runnable program.
5417
+
5418
+ ```ts
5419
+ import {
5420
+ estimateAutoIsoSlowestShutter,
5421
+ GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC
5422
+ } from "@photivra/engine";
5423
+
5424
+ const slowest =
5425
+ estimateAutoIsoSlowestShutter({
5426
+ focalLengthMm: 50,
5427
+ angularVelocityRadPerSec:
5428
+ GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC,
5429
+ stabilizationStopsEquivalent: 0,
5430
+ blurBudget: {
5431
+ kind: "pixels",
5432
+ pixels: 1,
5433
+ pixelPitchMicrometers: 6
5434
+ },
5435
+ fasterByStops: 0,
5436
+ shutterCapability:
5437
+ capabilities.shutter
5438
+ });
5439
+ // slowest.value.shutterSeconds: about 1/640 s for 50 mm, 6 µm pixels and the generic hand.
5440
+ ```
5441
+
5442
+ The result reports `derivedShutterSeconds`, `biasedShutterSeconds`, `shutterSeconds` (after the grid), the blur budget and
5443
+ the shake model's blur at the derived and the final shutter, the projection distance and the residual-motion factor, as an
5444
+ `approximation` with its assumptions. The blur budget is `{ kind: "pixels", pixels, pixelPitchMicrometers }` or
5445
+ `{ kind: "image-diagonal-fraction", fraction, imageWidthMm, imageHeightMm }`.
5446
+
5447
+ What the tests check: longer focal lengths give faster thresholds; each stabilization stop doubles the time exactly; the
5448
+ bias moves it by exact whole stops; the shake model's blur at the derived time equals the budget; nothing is NaN or
5449
+ infinite; invalid input fails with a domain error (no shake, a fractional or out-of-range bias, a non-positive budget, a
5450
+ quarter turn of motion).
5451
+
5452
+ **The hand-shake profile is an input.** No free source gives one angular velocity for every photographer and camera.
5453
+ `GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC` is a **labelled generic default**: 4.39 °/s (≈ 0.0766 rad/s), on yaw. It is the
5454
+ linear camera-motion rate F. Xiao, J. Farrell, P. Catrysse and B. Wandell use for an average user holding a typical 100 g
5455
+ mobile handset with two hands ("Mobile imaging: the big challenge of the small pixel", Proc. SPIE 7250, Digital
5456
+ Photography V, 2009, §2; [author copy](https://web.stanford.edu/~wandell/data/papers/2009-CameraShake-Xiao.pdf)). They
5457
+ take it from their earlier gyroscope measurements, F. Xiao et al., "Camera-motion and mobile imaging", Proc. SPIE 6502
5458
+ (2007), which report that the motion is close to linear for exposures under about 100 ms. It is one magnitude, so the
5459
+ engine puts it on yaw; for small angles the blur length does not depend on the direction. A heavier camera, another grip
5460
+ or another photographer moves differently, so pass your own profile when you have one.
5461
+
5462
+ **Approximations.** Constant angular velocity, blur on the optical axis only, stabilization as 2^−stops (the educational
5463
+ model of `estimateCameraShakeBlur()`, not a CIPA DC-011 rating), no roll, translation or subject motion. The blur budget is
5464
+ your choice of sharpness, not a measured threshold.
5465
+
5466
+ #### Through `simulateCapture`
5467
+
5468
+ `exposure.autoIso.slowestPreferredShutterSeconds: "auto"` (aperture priority; optional in program) calls
5469
+ `estimateAutoIsoSlowestShutter()` with the request's focal length, focus distance, pixel pitch and the body's shutter range.
5470
+ `exposure.autoIso.automaticSlowestShutter` sets `fasterByStops` (default 0), `blurBudgetPixels` (default 1, a generic
5471
+ pixel-level budget), `angularVelocityRadPerSec` (default the generic hand above) and `stabilizationStopsEquivalent` (default 0:
5472
+ the tiers declare none). Each default is recorded as a `default` assumption. The report's `exposure.autoIso.automaticSlowestShutter`
5473
+ gives the derived, biased and used times and the blur at the used time in pixels.
5474
+
5475
+ ### Program Auto with Auto ISO: a slowest preferred shutter (#504)
5476
+
5477
+ `resolveProgramAutoExposureMode()` optionally takes
5478
+ `isoControl.slowestPreferredShutter: { slowestPreferredShutterSeconds, afterMaximumIso }`
5479
+ (`ProgramSlowestPreferredShutterPolicy`), the same two-axis policy as Aperture Priority + Auto ISO:
5480
+
5481
+ 1. Follow the program line at the lowest selectable ISO.
5482
+ 2. If the line's shutter is longer than `slowestPreferredShutterSeconds`, hold the shutter there (snapped, never longer),
5483
+ keep the line's aperture, and raise the ISO.
5484
+ 3. At the Auto ISO maximum (the body's or the photographer's `autoIsoLimits`), `"allow-slower-shutter"` lengthens the shutter
5485
+ toward the target, but never past the line's own shutter. `"hold-preferred-shutter"` keeps the shutter there and reports
5486
+ the residual.
5487
+
5488
+ The result adds `slowestPreferredShutter: { ..., applied, afterMaximumIsoFallbackUsed }`; `programLineSelection` stays the
5489
+ line's own selection. Without the option the resolver behaves as before and reports no such field. Full Auto exposure does
5490
+ not take the option.
5491
+
5323
5492
  ### Shared boundaries
5324
5493
 
5325
5494
  These are control policies over the same relative exposure equation. They create no new optics or sensor physics.
@@ -6882,6 +7051,31 @@ try { parseDngRawImage({ bytes: png }); } catch { rejected = true; }
6882
7051
  if (!rejected) throw new Error('A PNG must not parse as a DNG');
6883
7052
  ```
6884
7053
 
7054
+ ## Text metadata on a file you encoded
7055
+
7056
+ `addJpegTextMetadata(jpeg, metadata)` and `addPngTextMetadata(png, metadata)` (#388) add the caller's text to a JPEG
7057
+ or PNG that was encoded without `output.metadata`: for example a full-size file joined from `output.region` parts
7058
+ and encoded with `composePrintJpegStrips` or `serializePngImage`. They are the functions that `output.metadata`
7059
+ calls, so for the same file and text the bytes are the same as a whole-frame `simulateCapture` with
7060
+ `output.metadata`. The text is checked as `output.metadata` is (see
7061
+ [Text metadata](https://github.com/Photivra/photivra/blob/main/docs/QUICK_START.md#text-metadata-24)); invalid text or a
7062
+ malformed file fails with `InvalidConfigurationError`. The input array is not changed.
7063
+
7064
+ ```ts
7065
+ import { addJpegTextMetadata, addPngTextMetadata, serializePngImage, simulateCapture } from '@photivra/engine';
7066
+
7067
+ const text = { description: 'Siemens star at f/8', software: 'My Viewer 1.0', comment: 'Test run 3' };
7068
+ // A PNG the caller encoded (here one sRGB pixel) gets the PNG text chunks.
7069
+ const png = addPngTextMetadata(serializePngImage({ width: 1, height: 1, bitDepth: 8, samples: Uint8Array.of(255, 128, 0),
7070
+ colourEncoding: { kind: 'srgb', renderingIntent: 'perceptual' } }), text);
7071
+ // A JPEG written without output.metadata and tagged afterwards has the bytes of one written with it.
7072
+ const plain = await simulateCapture({ scene: { preset: 'siemens-star' }, output: { width: 8, height: 8, format: 'jpeg' } });
7073
+ const direct = await simulateCapture({ scene: { preset: 'siemens-star' }, output: { width: 8, height: 8, format: 'jpeg', metadata: text } });
7074
+ const tagged = addJpegTextMetadata(plain.image.jpeg!, text);
7075
+ const same = tagged.length === direct.image.jpeg!.length && tagged.every((byte, i) => byte === direct.image.jpeg![i]);
7076
+ console.log(png.length, same);
7077
+ ```
7078
+
6885
7079
  ## Native-only Print planning
6886
7080
 
6887
7081
  - [Registered linear statistics](https://github.com/Photivra/photivra/blob/main/docs/PRINT_REGION_STATISTICS.md) tell apart changes caused by processing the same capture from variation between repeated captures at each site.
@@ -7502,17 +7696,56 @@ console.log(star.assumptions.filter((a) => a.kind === "default").map((a) => a.id
7502
7696
  console.log(star.request.siteInput(0).environment.optics.nominalFNumber);
7503
7697
  ```
7504
7698
 
7699
+ ### Color charts (2.14)
7700
+
7701
+ The engine's own [color patches](https://github.com/Photivra/photivra/blob/main/docs/COLOR_PATCHES.md) are reflectance spectra lit by the capture's illuminant. Draw the
7702
+ whole set with the `color-patches` preset, or use one patch wherever a chart takes a reflectance spectrum, as
7703
+ `{ kind: "color-patch", patchId }`. `getColorPatchSet()` gives each patch's code, name, reason for being on the chart and reference CIELAB and sRGB
7704
+ under D65.
7705
+
7706
+ ```ts
7707
+ import { getColorPatchSet, simulateCapture } from "@photivra/engine";
7708
+
7709
+ const tungsten = await simulateCapture({
7710
+ scene: { preset: "color-patches", patchIds: ["neutral-4", "red", "skin-medium", "sky-blue"], columns: 4 },
7711
+ illuminant: "cie-a",
7712
+ sensor: { spectralStepNanometers: 5 },
7713
+ output: { width: 64, height: 16 }
7714
+ });
7715
+ const sky = getColorPatchSet().patches.find((p) => p.patchId === "sky-blue");
7716
+ console.log(tungsten.scene.parameters.patchSet, sky?.code, sky?.name, sky?.reference.cielabD65); // "PV-M1", "Midday Sky"
7717
+ ```
7718
+
7505
7719
  In `aperture-priority` mode, the shutter comes from the meter (CIPA DC-004 relations S = 10/H and H = 0.65·L·t/N²) through
7506
7720
  `resolveAperturePriorityExposureMode()`.
7507
7721
  `exposure.standardExposureDeviationStops` is the exposure used minus the standard exposure for the metered luminance L, in stops,
7508
7722
  log₂(N²/t) − log₂(0.65·L·S/10) from the same relations: positive is darker (under), negative brighter (over). In an automatic
7509
7723
  mode it is minus the compensation plus any limit the resolver hit; it is `null` only for a scene with no metered luminance.
7510
- The display view's `exposure` carries the same value.
7724
+ `exposure.standardExposureIlluminanceLux` is the illuminance at which those settings give the standard exposure, E·2^Δ for the
7725
+ request's illuminance E (a reflective scene's metered luminance is proportional to E); `null` for a black scene or a
7726
+ `radiance-function` scene. `exposure.greyCardDeviationStops` (2.14) is the deviation from the standard exposure of an 18 % grey
7727
+ card under that illuminance, log₂(L_ref/(0.18·E/π)): incident metering, independent of the scene's reflectances, exactly 0 at
7728
+ the defaults. The automatic modes meter the scene by default; `exposure.metering: "grey-card"` makes them (and Auto ISO) meter
7729
+ that grey card instead, so they land on a grey-card deviation of 0. The display view's `exposure` carries the same values.
7730
+
7731
+ ```ts
7732
+ import { simulateCapture } from "@photivra/engine";
7733
+
7734
+ const chart = await simulateCapture({
7735
+ scene: { preset: "uniform", reflectance: 0.9 },
7736
+ exposure: { mode: "aperture-priority", metering: "grey-card" },
7737
+ output: { width: 8, height: 8 }
7738
+ });
7739
+ console.log(chart.exposure.metering, chart.exposure.settings.shutterSeconds, chart.exposure.greyCardDeviationStops);
7740
+ ```
7511
7741
 
7512
7742
  With `exposure.iso: "auto"` (#516) the ISO is chosen too, in every mode, by the resolvers' Auto ISO branches
7513
7743
  ([Priority modes with Auto ISO](#priority-modes-with-auto-iso)). Aperture priority then needs the slowest shutter
7514
7744
  before the ISO rises, and takes the policy at the maximum ISO; the result reports the ISO, that it was automatic and
7515
- what limited it. See [Auto ISO](QUICK_START.md#auto-iso-516).
7745
+ what limited it. `exposure.autoIso.limits: { maximum, minimum? }` (#505) sets the photographer's Auto ISO range in any
7746
+ mode ([photographer-set limits](#photographer-set-auto-iso-limits-505)). `slowestPreferredShutterSeconds: "auto"` (#504)
7747
+ derives the slowest shutter from the camera-shake model, in aperture priority and, optionally, program
7748
+ ([automatic slowest shutter](#automatic-slowest-shutter-504)). See [Auto ISO](QUICK_START.md#auto-iso-516).
7516
7749
 
7517
7750
  ```ts
7518
7751
  import { prepareDisplayView, simulateCapture, type SimulateCaptureOptions } from "@photivra/engine";
@@ -7575,6 +7808,12 @@ console.log(ran.backend, ran.fallbackReason, ran.assurance, ran.errorBound, ran.
7575
7808
  prepared.dispose(); // releases a device the view acquired; a shared accelerator's device stays its owner's
7576
7809
  ```
7577
7810
 
7811
+ The GPU code (the session, the renderers and their WGSL, about 52 kB gzip) is imported only when `gpu` is `"auto"` or
7812
+ `"require"`, so a bundler that splits dynamic imports leaves it out of a page's first load. A build that cannot split inlines
7813
+ it: Vite builds workers as `iife` by default, so a worker that calls `prepareDisplayView` carries it; build workers as ES
7814
+ modules (`worker: { format: "es" }`, with `new Worker(url, { type: "module" })`) to keep it out. Measured sizes:
7815
+ [Bundle size](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#bundle-size).
7816
+
7578
7817
  #### Sharing AHD correction states between workers
7579
7818
 
7580
7819
  With `output.demosaic: "ahd"`, low-signal display pixels read correction states that each realm (a page, a Web Worker)
@@ -11,5 +11,5 @@ version they were made with. Proof-of-concept (POC), schema and model contracts
11
11
  own versions. Release checks confirm it matches package.json and the packed package.
12
12
 
13
13
  ```ts
14
- ENGINE_API_VERSION = "2.12.0" as const
14
+ ENGINE_API_VERSION = "2.14.0" as const
15
15
  ```
@@ -2,6 +2,37 @@
2
2
 
3
3
  The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
4
 
5
+ ## GENERIC_THIRD_STOP_ISO_SETTINGS
6
+
7
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
8
+
9
+ ```ts
10
+ GENERIC_THIRD_STOP_ISO_SETTINGS = Object.freeze([100, 125, 160, 200, 250, 320, 400, 500, 640, 800, 1000, 1250, 1600,
11
+ 2000, 2500, 3200, 4000, 5000, 6400, 8000, 10000, 12800, 16000, 20000, 25600] as const)
12
+ ```
13
+
14
+ ## GENERIC_THIRD_STOP_SHUTTER_SECONDS
15
+
16
+ The standard third-stop shutter times, seconds, from 1/8000 s to 30 s: the conventional markings of t = 2^(k/3) s
17
+ (1/8000 for 2^−13, 1/6 for 2^−8/3, 0.3 for 2^−5/3, 30 for 2^5; each within 7 % of the exact value). The generic bodies'
18
+ shutters are continuous over 1/8000–30 s and take any time as exact, so these are the values to offer in a third-stop
19
+ dial, not a restriction.
20
+
21
+ ```ts
22
+ GENERIC_THIRD_STOP_SHUTTER_SECONDS = Object.freeze([1 / 8000, 1 / 6400, 1 / 5000, 1 / 4000, 1 / 3200, 1 / 2500, 1 / 2000,
23
+ 1 / 1600, 1 / 1250, 1 / 1000, 1 / 800, 1 / 640, 1 / 500, 1 / 400, 1 / 320, 1 / 250, 1 / 200, 1 / 160, 1 / 125, 1 / 100, 1 / 80, 1 / 60, 1 / 50, 1 / 40,
24
+ 1 / 30, 1 / 25, 1 / 20, 1 / 15, 1 / 13, 1 / 10, 1 / 8, 1 / 6, 1 / 5, 1 / 4, 0.3, 0.4, 0.5, 0.6, 0.8, 1, 1.3, 1.6, 2, 2.5, 3.2, 4, 5, 6, 8, 10, 13,
25
+ 15, 20, 25, 30] as const)
26
+ ```
27
+
28
+ ## GENERIC_WHOLE_STOP_ISO_SETTINGS
29
+
30
+ Whole-stop ISO settings of the 2.0.0 bodies (each body's list stops at its maximum).
31
+
32
+ ```ts
33
+ GENERIC_WHOLE_STOP_ISO_SETTINGS = Object.freeze([100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600] as const)
34
+ ```
35
+
5
36
  ## GenericEquipmentTier
6
37
 
7
38
  The exact contract is declared below; use the domain guide for assumptions and staged integration.
@@ -18,7 +49,7 @@ The exact contract is declared below; use the domain guide for assumptions and s
18
49
  export interface GenericEquipmentTierPreset {
19
50
  tier: GenericEquipmentTier;
20
51
  label: "Consumer" | "Prosumer" | "Professional";
21
- presetId: string; presetVersion: "2.0.0";
52
+ presetId: string; presetVersion: GenericTierPresetVersion;
22
53
  scientificStatus: "approximation";
23
54
  evidence: readonly EvidenceProvenance[];
24
55
  body: GenericTierBodyAssets;
@@ -78,6 +109,15 @@ export interface GenericTierLensAssets {
78
109
  }
79
110
  ```
80
111
 
112
+ ## GenericTierPresetVersion
113
+
114
+ The exact versions of the generic tier presets (no "latest" alias). 2.1.0 (2.14, round 5) adds the third-stop ISO
115
+ settings; 2.0.0 is kept, whole-stop, so saved selections and records of it still resolve to exactly what they named.
116
+
117
+ ```ts
118
+ export type GenericTierPresetVersion = "2.0.0" | "2.1.0";
119
+ ```
120
+
81
121
  ## GenericVersionedAsset
82
122
 
83
123
  The exact contract is declared below; use the domain guide for assumptions and staged integration.
@@ -16,10 +16,18 @@ This call is synchronous; any supplied provider must follow its explicit synchro
16
16
 
17
17
  ## GENERIC_EQUIPMENT_TIER_PRESET_VERSION
18
18
 
19
- The exact contract is declared below; use the domain guide for assumptions and staged integration.
19
+ The current preset version (2.14: 2.1.0, third-stop ISO settings).
20
+
21
+ ```ts
22
+ GENERIC_EQUIPMENT_TIER_PRESET_VERSION = "2.1.0" as const
23
+ ```
24
+
25
+ ## GENERIC_EQUIPMENT_TIER_PRESET_VERSIONS
26
+
27
+ Every exact preset version that resolves, oldest first. A version is never retuned in place (`test/generic-tier-presets.test.ts`).
20
28
 
21
29
  ```ts
22
- GENERIC_EQUIPMENT_TIER_PRESET_VERSION = "2.0.0" as const
30
+ GENERIC_EQUIPMENT_TIER_PRESET_VERSIONS = Object.freeze(["2.0.0", "2.1.0"] as const)
23
31
  ```
24
32
 
25
33
  ## GENERIC_EQUIPMENT_TIERS
@@ -44,7 +52,7 @@ The exact contract is declared below; use the domain guide for assumptions and s
44
52
 
45
53
  ```ts
46
54
  export interface GenericEquipmentTierSelection {
47
- schemaVersion: "0.1.0"; presetVersion: typeof GENERIC_EQUIPMENT_TIER_PRESET_VERSION;
55
+ schemaVersion: "0.1.0"; presetVersion: GenericTierPresetVersion;
48
56
  bodyTier: GenericEquipmentTier; lensTier: GenericEquipmentTier;
49
57
  bodyPresetId: string; lensPresetId: string;
50
58
  profileReferences: readonly GenericEquipmentTierProfileReference[];