@photivra/engine 2.11.1 → 2.13.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 (55) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +2 -4
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +1 -0
  7. package/dist/capture/export-jpeg.d.ts +1 -1
  8. package/dist/capture/export-jpeg.js +6 -4
  9. package/dist/capture/export-jpeg.js.map +1 -1
  10. package/dist/core/version.d.ts +1 -1
  11. package/dist/core/version.js +1 -1
  12. package/dist/core/version.js.map +1 -1
  13. package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
  14. package/dist/exposure/auto-iso-slowest-shutter.js +216 -0
  15. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
  16. package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
  17. package/dist/exposure/exposure-mode-resolver.js +238 -33
  18. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  19. package/dist/facade/diffraction-provider.js +12 -13
  20. package/dist/facade/diffraction-provider.js.map +1 -1
  21. package/dist/facade/display-view.d.ts +27 -0
  22. package/dist/facade/display-view.js +234 -65
  23. package/dist/facade/display-view.js.map +1 -1
  24. package/dist/facade/simulate-capture.d.ts +159 -3
  25. package/dist/facade/simulate-capture.js +203 -10
  26. package/dist/facade/simulate-capture.js.map +1 -1
  27. package/dist/facade/simulate-display-view.js +2 -2
  28. package/dist/facade/simulate-display-view.js.map +1 -1
  29. package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
  30. package/dist/gpu/display-view-quality-gpu.js +24 -14
  31. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  32. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
  33. package/dist/gpu/wgsl-display-view-quality-kernel.js +36 -2
  34. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +2 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/optics/irradiance-convolution.d.ts +10 -0
  39. package/dist/optics/irradiance-convolution.js +48 -13
  40. package/dist/optics/irradiance-convolution.js.map +1 -1
  41. package/dist/output/print-jpeg.js +12 -9
  42. package/dist/output/print-jpeg.js.map +1 -1
  43. package/dist/output/text-metadata.d.ts +22 -2
  44. package/dist/output/text-metadata.js +58 -9
  45. package/dist/output/text-metadata.js.map +1 -1
  46. package/docs/API_REFERENCE.md +23 -1
  47. package/docs/QUICK_START.md +109 -3
  48. package/docs/USAGE.md +234 -1
  49. package/docs/api/core-version.md +1 -1
  50. package/docs/api/exports.json +77 -2
  51. package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
  52. package/docs/api/exposure-exposure-mode-resolver.md +68 -0
  53. package/docs/api/facade-simulate-capture.md +153 -5
  54. package/docs/api/output-text-metadata.md +34 -0
  55. package/package.json +1 -1
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.
@@ -7497,13 +7691,46 @@ const star = await simulateCapture({
7497
7691
  output: { width: 12, height: 8, toneCurve: "positive-reinhard-per-channel", whiteBalance: { kelvin: 5000 } }
7498
7692
  });
7499
7693
 
7500
- console.log(star.exposure.settings.shutterSeconds, star.exposure.ev100);
7694
+ console.log(star.exposure.settings.shutterSeconds, star.exposure.ev100, star.exposure.standardExposureDeviationStops);
7501
7695
  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
 
7505
7699
  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
7700
  `resolveAperturePriorityExposureMode()`.
7701
+ `exposure.standardExposureDeviationStops` is the exposure used minus the standard exposure for the metered luminance L, in stops,
7702
+ log₂(N²/t) − log₂(0.65·L·S/10) from the same relations: positive is darker (under), negative brighter (over). In an automatic
7703
+ mode it is minus the compensation plus any limit the resolver hit; it is `null` only for a scene with no metered luminance.
7704
+ `exposure.standardExposureIlluminanceLux` is the illuminance at which those settings give the standard exposure, E·2^Δ for the
7705
+ request's illuminance E (a reflective scene's metered luminance is proportional to E); `null` for a black scene or a
7706
+ `radiance-function` scene. The display view's `exposure` carries the same values.
7707
+
7708
+ With `exposure.iso: "auto"` (#516) the ISO is chosen too, in every mode, by the resolvers' Auto ISO branches
7709
+ ([Priority modes with Auto ISO](#priority-modes-with-auto-iso)). Aperture priority then needs the slowest shutter
7710
+ before the ISO rises, and takes the policy at the maximum ISO; the result reports the ISO, that it was automatic and
7711
+ what limited it. `exposure.autoIso.limits: { maximum, minimum? }` (#505) sets the photographer's Auto ISO range in any
7712
+ mode ([photographer-set limits](#photographer-set-auto-iso-limits-505)). `slowestPreferredShutterSeconds: "auto"` (#504)
7713
+ derives the slowest shutter from the camera-shake model, in aperture priority and, optionally, program
7714
+ ([automatic slowest shutter](#automatic-slowest-shutter-504)). See [Auto ISO](QUICK_START.md#auto-iso-516).
7715
+
7716
+ ```ts
7717
+ import { prepareDisplayView, simulateCapture, type SimulateCaptureOptions } from "@photivra/engine";
7718
+
7719
+ const request: SimulateCaptureOptions = {
7720
+ exposure: { mode: "aperture-priority", iso: "auto", autoIso: { slowestPreferredShutterSeconds: 1 / 125 } },
7721
+ lens: { fNumber: 4 },
7722
+ scene: { preset: "siemens-star" },
7723
+ illuminanceLux: 50,
7724
+ output: { width: 16, height: 12 }
7725
+ };
7726
+ const capture = await simulateCapture(request);
7727
+ const { settings, isoControl, autoIso } = capture.exposure;
7728
+ // For example "ISO 3200 · Auto", and why: "iso-grid-quantization", or "iso-maximum" with the shutter held.
7729
+ console.log(`ISO ${settings.iso} · ${isoControl === "automatic" ? "Auto" : ""}`, autoIso?.limitingConstraint, autoIso?.afterMaximumIsoFallbackUsed);
7730
+ // The display view of the same request resolves the same exposure.
7731
+ const view = await (await prepareDisplayView(request)).render({ width: 8, height: 6 });
7732
+ console.log(view.exposure.settings.iso === settings.iso);
7733
+ ```
7507
7734
 
7508
7735
  The chart presets are also available on their own:
7509
7736
 
@@ -7547,6 +7774,12 @@ console.log(ran.backend, ran.fallbackReason, ran.assurance, ran.errorBound, ran.
7547
7774
  prepared.dispose(); // releases a device the view acquired; a shared accelerator's device stays its owner's
7548
7775
  ```
7549
7776
 
7777
+ The GPU code (the session, the renderers and their WGSL, about 52 kB gzip) is imported only when `gpu` is `"auto"` or
7778
+ `"require"`, so a bundler that splits dynamic imports leaves it out of a page's first load. A build that cannot split inlines
7779
+ it: Vite builds workers as `iife` by default, so a worker that calls `prepareDisplayView` carries it; build workers as ES
7780
+ modules (`worker: { format: "es" }`, with `new Worker(url, { type: "module" })`) to keep it out. Measured sizes:
7781
+ [Bundle size](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#bundle-size).
7782
+
7550
7783
  #### Sharing AHD correction states between workers
7551
7784
 
7552
7785
  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.11.1" as const
14
+ ENGINE_API_VERSION = "2.13.0" as const
15
15
  ```
@@ -1,6 +1,6 @@
1
1
  {
2
- "packageVersion": "2.11.1",
3
- "engineApiVersion": "2.11.1",
2
+ "packageVersion": "2.13.0",
3
+ "engineApiVersion": "2.13.0",
4
4
  "exports": [
5
5
  {
6
6
  "name": "AbsoluteUncertaintyEstimate",
@@ -27,6 +27,16 @@
27
27
  "source": "src/output/capture-geometry.ts",
28
28
  "kind": "type"
29
29
  },
30
+ {
31
+ "name": "addJpegTextMetadata",
32
+ "source": "src/output/text-metadata.ts",
33
+ "kind": "runtime"
34
+ },
35
+ {
36
+ "name": "addPngTextMetadata",
37
+ "source": "src/output/text-metadata.ts",
38
+ "kind": "runtime"
39
+ },
30
40
  {
31
41
  "name": "AffineGeometricTransform",
32
42
  "source": "src/output/geometric-transforms.ts",
@@ -192,11 +202,36 @@
192
202
  "source": "src/optics/atmospheric-haze.ts",
193
203
  "kind": "type"
194
204
  },
205
+ {
206
+ "name": "AUTO_ISO_SLOWEST_SHUTTER_MAXIMUM_BIAS_STOPS",
207
+ "source": "src/exposure/auto-iso-slowest-shutter.ts",
208
+ "kind": "runtime"
209
+ },
210
+ {
211
+ "name": "AUTO_ISO_SLOWEST_SHUTTER_MODEL_VERSION",
212
+ "source": "src/exposure/auto-iso-slowest-shutter.ts",
213
+ "kind": "runtime"
214
+ },
195
215
  {
196
216
  "name": "AutoIsoBaselinePolicy",
197
217
  "source": "src/exposure/exposure-mode-resolver.ts",
198
218
  "kind": "type"
199
219
  },
220
+ {
221
+ "name": "AutoIsoLimits",
222
+ "source": "src/exposure/exposure-mode-resolver.ts",
223
+ "kind": "type"
224
+ },
225
+ {
226
+ "name": "AutoIsoSlowestShutterBlurBudget",
227
+ "source": "src/exposure/auto-iso-slowest-shutter.ts",
228
+ "kind": "type"
229
+ },
230
+ {
231
+ "name": "AutoIsoSlowestShutterEstimate",
232
+ "source": "src/exposure/auto-iso-slowest-shutter.ts",
233
+ "kind": "type"
234
+ },
200
235
  {
201
236
  "name": "AxisSamplingPitchMicrometers",
202
237
  "source": "src/motion/camera-rotation.ts",
@@ -2797,6 +2832,16 @@
2797
2832
  "source": "src/optics/circle-of-confusion.ts",
2798
2833
  "kind": "type"
2799
2834
  },
2835
+ {
2836
+ "name": "estimateAutoIsoSlowestShutter",
2837
+ "source": "src/exposure/auto-iso-slowest-shutter.ts",
2838
+ "kind": "runtime"
2839
+ },
2840
+ {
2841
+ "name": "EstimateAutoIsoSlowestShutterInput",
2842
+ "source": "src/exposure/auto-iso-slowest-shutter.ts",
2843
+ "kind": "type"
2844
+ },
2800
2845
  {
2801
2846
  "name": "estimateAutoWhiteBalance",
2802
2847
  "source": "src/color/white-balance.ts",
@@ -3327,6 +3372,11 @@
3327
3372
  "source": "src/equipment/generic-tier-presets.ts",
3328
3373
  "kind": "runtime"
3329
3374
  },
3375
+ {
3376
+ "name": "GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC",
3377
+ "source": "src/exposure/auto-iso-slowest-shutter.ts",
3378
+ "kind": "runtime"
3379
+ },
3330
3380
  {
3331
3381
  "name": "GENERIC_ISO_SIGNAL_CHAIN_PRESET_CATALOG_SCHEMA_VERSION",
3332
3382
  "source": "src/sensor/iso-signal-chain.ts",
@@ -5917,6 +5967,11 @@
5917
5967
  "source": "src/exposure/exposure-mode-resolver.ts",
5918
5968
  "kind": "type"
5919
5969
  },
5970
+ {
5971
+ "name": "ProgramSlowestPreferredShutterPolicy",
5972
+ "source": "src/exposure/exposure-mode-resolver.ts",
5973
+ "kind": "type"
5974
+ },
5920
5975
  {
5921
5976
  "name": "ProjectedMotionBlur",
5922
5977
  "source": "src/motion/projected-motion.ts",
@@ -8127,6 +8182,26 @@
8127
8182
  "source": "src/facade/simulate-capture.ts",
8128
8183
  "kind": "type"
8129
8184
  },
8185
+ {
8186
+ "name": "SimulateCaptureAutoIsoAfterMaximum",
8187
+ "source": "src/facade/simulate-capture.ts",
8188
+ "kind": "type"
8189
+ },
8190
+ {
8191
+ "name": "SimulateCaptureAutoIsoReport",
8192
+ "source": "src/facade/simulate-capture.ts",
8193
+ "kind": "type"
8194
+ },
8195
+ {
8196
+ "name": "SimulateCaptureAutomaticSlowestShutterOptions",
8197
+ "source": "src/facade/simulate-capture.ts",
8198
+ "kind": "type"
8199
+ },
8200
+ {
8201
+ "name": "SimulateCaptureAutomaticSlowestShutterReport",
8202
+ "source": "src/facade/simulate-capture.ts",
8203
+ "kind": "type"
8204
+ },
8130
8205
  {
8131
8206
  "name": "SimulateCaptureBorderHandling",
8132
8207
  "source": "src/facade/simulate-capture.ts",
@@ -0,0 +1,139 @@
1
+ # exposure/auto-iso-slowest-shutter.ts public contracts
2
+
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
+
5
+ ## AUTO_ISO_SLOWEST_SHUTTER_MAXIMUM_BIAS_STOPS
6
+
7
+ The largest whole-stop bias accepted, either way (makers offer up to 2 stops faster or slower).
8
+
9
+ ```ts
10
+ AUTO_ISO_SLOWEST_SHUTTER_MAXIMUM_BIAS_STOPS = 2 as const
11
+ ```
12
+
13
+ ## AUTO_ISO_SLOWEST_SHUTTER_MODEL_VERSION
14
+
15
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
16
+
17
+ ```ts
18
+ AUTO_ISO_SLOWEST_SHUTTER_MODEL_VERSION = "1.0.0" as const
19
+ ```
20
+
21
+ ## AutoIsoSlowestShutterBlurBudget
22
+
23
+ The blur allowed during the exposure, on the sensor.
24
+
25
+ ```ts
26
+ export type AutoIsoSlowestShutterBlurBudget =
27
+ | {
28
+ /** The blur trace length in pixels of the given pitch. */
29
+ kind: "pixels";
30
+ pixels: number;
31
+ pixelPitchMicrometers: number;
32
+ }
33
+ | {
34
+ /** The blur trace length as a fraction of the image diagonal (width × height in millimetres). */
35
+ kind: "image-diagonal-fraction";
36
+ fraction: number;
37
+ imageWidthMm: number;
38
+ imageHeightMm: number;
39
+ };
40
+ ```
41
+
42
+ ## AutoIsoSlowestShutterEstimate
43
+
44
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
45
+
46
+ ```ts
47
+ export interface AutoIsoSlowestShutterEstimate {
48
+ /** The exposure time whose shake blur equals the budget, before the bias and the grid. */
49
+ derivedShutterSeconds: number;
50
+ fasterByStops: number;
51
+ /** derivedShutterSeconds · 2^−fasterByStops. */
52
+ biasedShutterSeconds: number;
53
+ /** The slowest preferred shutter: the biased value, snapped to `shutterCapability` when given. */
54
+ shutterSeconds: number;
55
+ shutterResolution: {
56
+ kind: "unconstrained" | "continuous" | "discrete";
57
+ selectionPolicy: "not-longer-than-target";
58
+ quantized: boolean;
59
+ clamped: false | "minimum" | "maximum";
60
+ };
61
+ blurBudgetMm: number;
62
+ /** `estimateCameraShakeBlur()`'s stabilized blur at the derived shutter (equal to the budget within rounding). */
63
+ blurAtDerivedShutterMm: number;
64
+ /** `estimateCameraShakeBlur()`'s stabilized blur at `shutterSeconds`. */
65
+ blurAtShutterMm: number;
66
+ /** The same blur lengths in pixels, for a pixel budget. */
67
+ blurBudgetPixels?: number;
68
+ blurAtShutterPixels?: number;
69
+ projectionDistanceMm: number;
70
+ residualMotionFactor: number;
71
+ angularVelocityRadPerSec: { yaw: number; pitch: number };
72
+ stabilizationStopsEquivalent: number;
73
+ }
74
+ ```
75
+
76
+ ## estimateAutoIsoSlowestShutter
77
+
78
+ Estimates the slowest preferred shutter for Auto ISO from the lens, the sensor and the hand's
79
+ shake: the exposure time at which `estimateCameraShakeBlur()`'s stabilized blur equals the blur
80
+ budget, then shifted by a whole-stop bias and optionally snapped to the body's shutter grid (never
81
+ longer than the biased value). Longer focal lengths give faster shutters; each stabilization stop
82
+ doubles the time; each bias stop halves (+1) or doubles (−1) it.
83
+
84
+ An approximation: constant angular velocity (a caller profile, or the labelled generic default
85
+ `GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC`), stabilization as 2^−stops, blur on the optical axis
86
+ only. The 1/focal-length "reciprocal rule" is a rule of thumb and is not used.
87
+
88
+ ```ts
89
+ export function estimateAutoIsoSlowestShutter(
90
+ input: EstimateAutoIsoSlowestShutterInput
91
+ ): CalculationResult<AutoIsoSlowestShutterEstimate>;
92
+ ```
93
+
94
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
95
+
96
+ ## EstimateAutoIsoSlowestShutterInput
97
+
98
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
99
+
100
+ ```ts
101
+ export interface EstimateAutoIsoSlowestShutterInput {
102
+ focalLengthMm: number;
103
+ /** Optional focus distance in metres: the thin-lens image distance becomes the projection distance, as in `estimateCameraShakeBlur()`. */
104
+ focusDistanceM?: number;
105
+ /**
106
+ * The hand's constant angular velocity in radians per second, the explicit shake profile (signs as in
107
+ * `estimateCameraShakeBlur()`; only the magnitude matters here). At least one axis must be non-zero.
108
+ * `GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC` is a cited generic default, not a measurement of you.
109
+ */
110
+ angularVelocityRadPerSec: { yaw: number; pitch: number };
111
+ /** Stabilization in equivalent stops (≥ 0); the motion is scaled by 2^−stops. Default 0. */
112
+ stabilizationStopsEquivalent?: number;
113
+ blurBudget: AutoIsoSlowestShutterBlurBudget;
114
+ /**
115
+ * The bias in whole stops, applied after the derivation: +1 is one stop faster (half the time), −1 one
116
+ * stop slower (twice the time). An integer from −2 to +2. Default 0.
117
+ */
118
+ fasterByStops?: number;
119
+ /** Optional: the body's resolved shutter capability. The biased shutter then snaps to it, never longer than the biased value, and is held at its ends. */
120
+ shutterCapability?: ResolvedGenericEquipmentExposureCapabilities["shutter"];
121
+ }
122
+ ```
123
+
124
+ ## GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC
125
+
126
+ A generic handheld angular velocity, labelled as a default and not a measurement of any camera or
127
+ photographer: 4.39 °/s (≈ 0.0766 rad/s), the linear camera-motion rate Xiao, Farrell, Catrysse and
128
+ Wandell use for "an average user" holding "a typical 100g mobile handset with two hands", from their
129
+ earlier gyroscope measurements (F. Xiao, J. Farrell, P. Catrysse, B. Wandell, "Mobile imaging: the
130
+ big challenge of the small pixel", Proc. SPIE 7250, Digital Photography V, 2009, §2; author copy:
131
+ https://web.stanford.edu/~wandell/data/papers/2009-CameraShake-Xiao.pdf; they cite F. Xiao et al.,
132
+ "Camera-motion and mobile imaging", Proc. SPIE 6502, 2007). The value is one rate magnitude; it is
133
+ placed on the yaw axis (for small angles the blur length does not depend on the direction). A
134
+ heavier camera, another grip or another photographer moves differently: pass your own profile.
135
+
136
+ ```ts
137
+ GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC: Readonly<{ yaw: number; pitch: number }> =
138
+ Object.freeze({ yaw: (4.39 * Math.PI) / 180, pitch: 0 })
139
+ ```