@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
@@ -7,12 +7,34 @@ export interface RelativeExposureControlAnchor {
7
7
  shutterSeconds: number;
8
8
  iso: number;
9
9
  }
10
+ /**
11
+ * #505: the photographer's Auto ISO limits, a policy choice kept separate from what the body allows
12
+ * (`capabilities.iso.autoIsoMinimum`/`autoIsoMaximum`, else the ISO range). Cameras let the
13
+ * photographer cap Auto ISO, and some also raise its floor (for example the Nikon D850's "maximum
14
+ * sensitivity", Fujifilm's per-preset default and maximum sensitivity, and Panasonic's separate upper
15
+ * and lower Auto ISO limits; makers' manuals, see docs/USAGE.md).
16
+ *
17
+ * Both values are ISO settings: inside the body's Auto ISO range and, on a discrete ISO grid, one of
18
+ * its values, with `minimum` at most `maximum`. Anything else fails with
19
+ * `InvalidScientificInputError`; the engine never clips a limit silently. When a limit is strictly
20
+ * inside the body's range and the ISO is held there, the limiting constraint is `user-iso-maximum` or
21
+ * `user-iso-minimum`; a limit equal to the body's bound adds nothing, and the body's `iso-maximum` or
22
+ * `iso-minimum` is reported.
23
+ */
24
+ export interface AutoIsoLimits {
25
+ /** The highest ISO Auto ISO may choose. */
26
+ maximum: number;
27
+ /** The lowest ISO Auto ISO may choose, and so its baseline. Default: the body's Auto ISO minimum. */
28
+ minimum?: number;
29
+ }
10
30
  export type ManualIsoControl = {
11
31
  kind: "manual";
12
32
  iso: number;
13
33
  } | {
14
34
  kind: "automatic";
15
35
  quantizationPolicy: "nearest-log2-lower-on-tie";
36
+ /** #505: optional photographer-set Auto ISO limits inside the body's Auto ISO range. */
37
+ autoIsoLimits?: AutoIsoLimits;
16
38
  };
17
39
  export interface ResolveManualExposureModeInput {
18
40
  target: ExposureMeterTarget;
@@ -31,7 +53,7 @@ export interface ResolveAperturePriorityExposureModeInput {
31
53
  shutterQuantizationPolicy: "nearest-log2-shorter-on-tie";
32
54
  }
33
55
  export type ExposureTargetResidualState = "matched" | "under-target" | "over-target" | "target-unresolved";
34
- export type ExposureResolutionConstraint = "none" | "iso-minimum" | "iso-maximum" | "iso-grid-quantization" | "shutter-minimum" | "shutter-maximum" | "shutter-grid-quantization" | "aperture-widest" | "aperture-narrowest" | "aperture-grid-quantization";
56
+ export type ExposureResolutionConstraint = "none" | "iso-minimum" | "iso-maximum" | "user-iso-minimum" | "user-iso-maximum" | "iso-grid-quantization" | "shutter-minimum" | "shutter-maximum" | "shutter-grid-quantization" | "aperture-widest" | "aperture-narrowest" | "aperture-grid-quantization";
35
57
  export type ExposureTargetResidual = {
36
58
  status: "resolved";
37
59
  state: "matched" | "under-target" | "over-target";
@@ -193,6 +215,8 @@ export type AperturePriorityExposureModeResolution = (AperturePriorityExposureRe
193
215
  export interface AutoIsoBaselinePolicy {
194
216
  isoBaseline: "minimum-selectable";
195
217
  isoQuantizationPolicy: "nearest-log2-lower-on-tie";
218
+ /** #505: optional photographer-set Auto ISO limits inside the body's Auto ISO range. */
219
+ autoIsoLimits?: AutoIsoLimits;
196
220
  }
197
221
  export interface AperturePriorityAutoIsoPolicy extends AutoIsoBaselinePolicy {
198
222
  kind: "minimum-iso-until-slowest-preferred-shutter";
@@ -394,7 +418,27 @@ export type ProgramAutoIsoControl = {
394
418
  kind: "automatic";
395
419
  isoBaseline: "minimum-selectable";
396
420
  isoQuantizationPolicy: "nearest-log2-lower-on-tie";
421
+ /** #505: optional photographer-set Auto ISO limits inside the body's Auto ISO range. */
422
+ autoIsoLimits?: AutoIsoLimits;
423
+ /**
424
+ * #504: optionally hold the program line's shutter at or faster than a slowest preferred shutter
425
+ * before the ISO rises (for example from `estimateAutoIsoSlowestShutter()`). Absent, the program line
426
+ * is followed as before.
427
+ */
428
+ slowestPreferredShutter?: ProgramSlowestPreferredShutterPolicy;
397
429
  };
430
+ /**
431
+ * #504: Program + Auto ISO's slowest preferred shutter, the two-axis policy of Aperture Priority + Auto
432
+ * ISO applied to the program line. When the line asks for a longer shutter than
433
+ * `slowestPreferredShutterSeconds`, the shutter is held there (snapped, never longer) with the line's
434
+ * aperture, and Auto ISO makes up the rest. At the Auto ISO maximum, `afterMaximumIso` decides:
435
+ * `"allow-slower-shutter"` lengthens the shutter toward the target, but never past the program line's
436
+ * own shutter; `"hold-preferred-shutter"` keeps it and leaves the exposure under target.
437
+ */
438
+ export interface ProgramSlowestPreferredShutterPolicy {
439
+ slowestPreferredShutterSeconds: number;
440
+ afterMaximumIso: "allow-slower-shutter" | "hold-preferred-shutter";
441
+ }
398
442
  export interface ResolveProgramAutoExposureModeInput {
399
443
  target: ExposureMeterTarget;
400
444
  capabilities: ResolvedGenericEquipmentExposureCapabilities;
@@ -407,6 +451,8 @@ export interface FullAutoExposurePolicy {
407
451
  programLine: ExposureProgramLineProfile;
408
452
  isoBaseline: "minimum-selectable";
409
453
  isoQuantizationPolicy: "nearest-log2-lower-on-tie";
454
+ /** #505: optional photographer-set Auto ISO limits inside the body's Auto ISO range. */
455
+ autoIsoLimits?: AutoIsoLimits;
410
456
  }
411
457
  export interface ResolveFullAutoExposureModeInput {
412
458
  target: ExposureMeterTarget;
@@ -481,6 +527,16 @@ export type ProgramAutoExposureModeResolution = (ProgramExposureResolutionBase &
481
527
  targetResidual: Extract<ExposureTargetResidual, {
482
528
  status: "resolved";
483
529
  }>;
530
+ /**
531
+ * #504: present only with `isoControl.slowestPreferredShutter`. `applied` is true when the line's
532
+ * shutter was longer than the slowest preferred shutter and was held; `afterMaximumIsoFallbackUsed`
533
+ * when the slower-shutter fallback ran at the Auto ISO maximum. `programLineSelection` stays the
534
+ * line's own selection.
535
+ */
536
+ slowestPreferredShutter?: ProgramSlowestPreferredShutterPolicy & {
537
+ applied: boolean;
538
+ afterMaximumIsoFallbackUsed: boolean;
539
+ };
484
540
  }) | {
485
541
  resolverVersion: typeof EXPOSURE_MODE_RESOLVER_VERSION;
486
542
  mode: "program-auto";
@@ -17,7 +17,7 @@
17
17
  import { InvalidScientificInputError } from "../core/validation.js";
18
18
  import { EXPOSURE_METER_TARGET_SCHEMA_VERSION } from "./metering-target.js";
19
19
  import { parseExposureProgramLineProfile } from "./program-line.js";
20
- import { requireInputKeys } from "../core/record-validation.js";
20
+ import { rejectUnknownFields, requireInputKeys } from "../core/record-validation.js";
21
21
  import { InvalidConfigurationError } from "../core/configuration-error.js";
22
22
  export const EXPOSURE_MODE_RESOLVER_VERSION = "0.5.0";
23
23
  function requireRecord(value, path) {
@@ -328,7 +328,7 @@ function nearestDiscreteIso(idealIso, values) {
328
328
  : "none"
329
329
  };
330
330
  }
331
- function autoIsoBounds(capabilities) {
331
+ function bodyAutoIsoBounds(capabilities) {
332
332
  return {
333
333
  minimum: capabilities.iso
334
334
  .autoIsoMinimum ??
@@ -338,9 +338,75 @@ function autoIsoBounds(capabilities) {
338
338
  capabilities.iso.maximum
339
339
  };
340
340
  }
341
- function resolveAutomaticIso(idealIso, capabilities) {
341
+ /**
342
+ * #505: the Auto ISO range a resolver chooses in: the body's Auto ISO range, narrowed by the
343
+ * photographer's validated limits when given. A limit outside the body's range, or off a discrete
344
+ * ISO grid, fails; it is never clipped.
345
+ */
346
+ function autoIsoBounds(capabilities, limits, path = "autoIsoLimits") {
347
+ const body = bodyAutoIsoBounds(capabilities);
348
+ if (limits === undefined) {
349
+ return {
350
+ ...body,
351
+ minimumConstraint: "iso-minimum",
352
+ maximumConstraint: "iso-maximum"
353
+ };
354
+ }
355
+ const record = requireRecord(limits, path);
356
+ rejectUnknownFields(record, ["maximum", "minimum"], path);
357
+ const check = (value, field) => {
358
+ const iso = requirePositiveFinite(value, path + "." + field);
359
+ if (iso < body.minimum ||
360
+ iso > body.maximum) {
361
+ throw new InvalidScientificInputError(path +
362
+ "." +
363
+ field +
364
+ " (ISO " +
365
+ iso +
366
+ ") lies outside the body's Auto ISO range, ISO " +
367
+ body.minimum +
368
+ " to " +
369
+ body.maximum +
370
+ ".");
371
+ }
372
+ const grid = capabilities.iso.settingGrid;
373
+ if (grid.kind ===
374
+ "discrete-values" &&
375
+ !grid.values.some((candidate) => settingEquals(candidate, iso))) {
376
+ throw new InvalidScientificInputError(path +
377
+ "." +
378
+ field +
379
+ " (ISO " +
380
+ iso +
381
+ ") is not a selectable ISO of the resolved discrete ISO grid.");
382
+ }
383
+ return iso;
384
+ };
385
+ const maximum = check(record.maximum, "maximum");
386
+ const minimum = record.minimum === undefined
387
+ ? body.minimum
388
+ : check(record.minimum, "minimum");
389
+ if (minimum > maximum) {
390
+ throw new InvalidScientificInputError(path +
391
+ ".minimum must not exceed " +
392
+ path +
393
+ ".maximum.");
394
+ }
395
+ return {
396
+ minimum,
397
+ maximum,
398
+ minimumConstraint: minimum > body.minimum &&
399
+ !settingEquals(minimum, body.minimum)
400
+ ? "user-iso-minimum"
401
+ : "iso-minimum",
402
+ maximumConstraint: maximum < body.maximum &&
403
+ !settingEquals(maximum, body.maximum)
404
+ ? "user-iso-maximum"
405
+ : "iso-maximum"
406
+ };
407
+ }
408
+ function resolveAutomaticIso(idealIso, capabilities, bounds) {
342
409
  const grid = capabilities.iso.settingGrid;
343
- const bounds = autoIsoBounds(capabilities);
344
410
  if (grid.kind ===
345
411
  "discrete-values") {
346
412
  const autoValues = grid.values.filter((value) => value >= bounds.minimum &&
@@ -351,6 +417,13 @@ function resolveAutomaticIso(idealIso, capabilities) {
351
417
  const discrete = nearestDiscreteIso(idealIso, autoValues);
352
418
  return {
353
419
  ...discrete,
420
+ limitingConstraint: discrete.clamped ===
421
+ "minimum"
422
+ ? bounds.minimumConstraint
423
+ : discrete.clamped ===
424
+ "maximum"
425
+ ? bounds.maximumConstraint
426
+ : discrete.limitingConstraint,
354
427
  kind: "discrete"
355
428
  };
356
429
  }
@@ -361,7 +434,7 @@ function resolveAutomaticIso(idealIso, capabilities) {
361
434
  kind: "continuous",
362
435
  quantized: false,
363
436
  clamped: "minimum",
364
- limitingConstraint: "iso-minimum"
437
+ limitingConstraint: bounds.minimumConstraint
365
438
  };
366
439
  }
367
440
  if (idealIso >
@@ -371,7 +444,7 @@ function resolveAutomaticIso(idealIso, capabilities) {
371
444
  kind: "continuous",
372
445
  quantized: false,
373
446
  clamped: "maximum",
374
- limitingConstraint: "iso-maximum"
447
+ limitingConstraint: bounds.maximumConstraint
375
448
  };
376
449
  }
377
450
  return {
@@ -471,9 +544,8 @@ function resolveAutomaticShutter(idealShutterSeconds, capabilities) {
471
544
  limitingConstraint: "none"
472
545
  };
473
546
  }
474
- function minimumSelectableIso(capabilities) {
547
+ function minimumSelectableIso(capabilities, bounds) {
475
548
  const grid = capabilities.iso.settingGrid;
476
- const bounds = autoIsoBounds(capabilities);
477
549
  if (grid.kind ===
478
550
  "continuous-within-range") {
479
551
  return bounds.minimum;
@@ -978,6 +1050,11 @@ export function resolveManualExposureMode(input) {
978
1050
  "nearest-log2-lower-on-tie") {
979
1051
  throw new InvalidConfigurationError('isoControl.quantizationPolicy must be "nearest-log2-lower-on-tie".');
980
1052
  }
1053
+ const autoBounds = input.isoControl.kind ===
1054
+ "automatic"
1055
+ ? autoIsoBounds(input.capabilities, input.isoControl
1056
+ .autoIsoLimits, "isoControl.autoIsoLimits")
1057
+ : undefined;
981
1058
  if (input.isoControl.kind ===
982
1059
  "manual") {
983
1060
  const manualIso = requirePositiveFinite(input.isoControl.iso, "isoControl.iso");
@@ -1052,7 +1129,7 @@ export function resolveManualExposureMode(input) {
1052
1129
  idealIso <= 0) {
1053
1130
  throw new InvalidScientificInputError("Ideal Auto ISO must remain finite and greater than zero.");
1054
1131
  }
1055
- const automatic = resolveAutomaticIso(idealIso, input.capabilities);
1132
+ const automatic = resolveAutomaticIso(idealIso, input.capabilities, autoBounds);
1056
1133
  const achievedScale = opticalFactor *
1057
1134
  (automatic.iso /
1058
1135
  referenceExposure.iso);
@@ -1265,6 +1342,7 @@ export function resolveAperturePriorityAutoIsoExposureMode(input) {
1265
1342
  .maximumSeconds) {
1266
1343
  throw new InvalidScientificInputError("policy.slowestPreferredShutterSeconds lies outside the resolved shutter capability range.");
1267
1344
  }
1345
+ const autoBounds = autoIsoBounds(input.capabilities, policy.autoIsoLimits, "policy.autoIsoLimits");
1268
1346
  const availability = validateAutoIsoAvailability(input.capabilities);
1269
1347
  const commonBlocked = (blocker) => {
1270
1348
  const raw = buildPriorityAutoIsoBlocked("aperture-priority", target, referenceExposure, blocker, { aperture: manualAperture });
@@ -1292,7 +1370,7 @@ export function resolveAperturePriorityAutoIsoExposureMode(input) {
1292
1370
  if (target.status === "no-signal") {
1293
1371
  return commonBlocked("target-no-signal");
1294
1372
  }
1295
- const baselineIso = minimumSelectableIso(input.capabilities);
1373
+ const baselineIso = minimumSelectableIso(input.capabilities, autoBounds);
1296
1374
  const factorAtReferenceShutter = opticalExposureFactor(manualAperture, referenceExposure
1297
1375
  .shutterSeconds, referenceExposure) *
1298
1376
  (baselineIso /
@@ -1316,7 +1394,13 @@ export function resolveAperturePriorityAutoIsoExposureMode(input) {
1316
1394
  manualShutterSeconds: shutter.shutterSeconds,
1317
1395
  isoControl: {
1318
1396
  kind: "automatic",
1319
- quantizationPolicy: policy.isoQuantizationPolicy
1397
+ quantizationPolicy: policy.isoQuantizationPolicy,
1398
+ ...(policy.autoIsoLimits ===
1399
+ undefined
1400
+ ? {}
1401
+ : {
1402
+ autoIsoLimits: policy.autoIsoLimits
1403
+ })
1320
1404
  }
1321
1405
  });
1322
1406
  if (isoPass.status !== "resolved" ||
@@ -1547,6 +1631,7 @@ export function resolveShutterPriorityExposureMode(input) {
1547
1631
  "not-wider-than-target") {
1548
1632
  throw new InvalidConfigurationError("Shutter Priority Auto ISO policy is invalid.");
1549
1633
  }
1634
+ const autoBounds = autoIsoBounds(input.capabilities, policy.autoIsoLimits, "isoControl.policy.autoIsoLimits");
1550
1635
  const availability = validateAutoIsoAvailability(input.capabilities);
1551
1636
  if (availability !== "supported") {
1552
1637
  const raw = buildPriorityAutoIsoBlocked("shutter-priority", target, referenceExposure, availability === "unknown"
@@ -1586,7 +1671,7 @@ export function resolveShutterPriorityExposureMode(input) {
1586
1671
  }
1587
1672
  };
1588
1673
  }
1589
- const baselineIso = minimumSelectableIso(input.capabilities);
1674
+ const baselineIso = minimumSelectableIso(input.capabilities, autoBounds);
1590
1675
  const idealAtBaseline = idealApertureForTarget(target
1591
1676
  .requiredExposureScaleToTarget, manualShutterSeconds, baselineIso, referenceExposure);
1592
1677
  const aperture = selectApertureNotWiderThanTarget(idealAtBaseline, input.capabilities);
@@ -1598,7 +1683,13 @@ export function resolveShutterPriorityExposureMode(input) {
1598
1683
  manualShutterSeconds,
1599
1684
  isoControl: {
1600
1685
  kind: "automatic",
1601
- quantizationPolicy: policy.isoQuantizationPolicy
1686
+ quantizationPolicy: policy.isoQuantizationPolicy,
1687
+ ...(policy.autoIsoLimits ===
1688
+ undefined
1689
+ ? {}
1690
+ : {
1691
+ autoIsoLimits: policy.autoIsoLimits
1692
+ })
1602
1693
  }
1603
1694
  });
1604
1695
  if (isoPass.status !== "resolved" ||
@@ -1738,6 +1829,10 @@ export function resolveProgramAutoExposureMode(input) {
1738
1829
  "nearest-log2-lower-on-tie") {
1739
1830
  throw new InvalidConfigurationError("Program Auto ISO policy is invalid.");
1740
1831
  }
1832
+ const autoBounds = autoIsoBounds(input.capabilities, input.isoControl
1833
+ .autoIsoLimits, "isoControl.autoIsoLimits");
1834
+ const holdPolicy = validateProgramSlowestPreferredShutter(input.isoControl
1835
+ .slowestPreferredShutter, input.capabilities);
1741
1836
  const availability = validateAutoIsoAvailability(input.capabilities);
1742
1837
  if (availability !== "supported") {
1743
1838
  return {
@@ -1767,28 +1862,94 @@ export function resolveProgramAutoExposureMode(input) {
1767
1862
  safetyShiftApplied: false
1768
1863
  };
1769
1864
  }
1770
- const baselineIso = minimumSelectableIso(input.capabilities);
1865
+ const baselineIso = minimumSelectableIso(input.capabilities, autoBounds);
1771
1866
  const requestedOpticalStops = target
1772
1867
  .exposureOffsetStopsToTarget -
1773
1868
  Math.log2(baselineIso /
1774
1869
  referenceExposure.iso);
1775
1870
  const selection = selectProgramLineSettings(requestedOpticalStops, programLine, input.capabilities);
1776
- const isoPass = resolveManualExposureMode({
1777
- target,
1778
- capabilities: input.capabilities,
1779
- referenceExposure,
1780
- manualAperture: selection.resolvedAperture,
1781
- manualShutterSeconds: selection
1782
- .resolvedShutterSeconds,
1783
- isoControl: {
1784
- kind: "automatic",
1785
- quantizationPolicy: input.isoControl
1786
- .isoQuantizationPolicy
1871
+ const isoControl = input.isoControl;
1872
+ const autoIsoPass = (shutterSeconds) => {
1873
+ const pass = resolveManualExposureMode({
1874
+ target,
1875
+ capabilities: input.capabilities,
1876
+ referenceExposure,
1877
+ manualAperture: selection.resolvedAperture,
1878
+ manualShutterSeconds: shutterSeconds,
1879
+ isoControl: {
1880
+ kind: "automatic",
1881
+ quantizationPolicy: isoControl
1882
+ .isoQuantizationPolicy,
1883
+ ...(isoControl
1884
+ .autoIsoLimits ===
1885
+ undefined
1886
+ ? {}
1887
+ : {
1888
+ autoIsoLimits: isoControl
1889
+ .autoIsoLimits
1890
+ })
1891
+ }
1892
+ });
1893
+ if (pass.status !== "resolved" ||
1894
+ pass.isoControl !== "automatic") {
1895
+ throw new InvalidScientificInputError("Program Auto could not resolve its Auto ISO pass.");
1787
1896
  }
1788
- });
1789
- if (isoPass.status !== "resolved" ||
1790
- isoPass.isoControl !== "automatic") {
1791
- throw new InvalidScientificInputError("Program Auto could not resolve its Auto ISO pass.");
1897
+ return pass;
1898
+ };
1899
+ // #504: hold the line's shutter at or faster than the slowest preferred shutter before the ISO rises.
1900
+ const lineShutterSeconds = selection
1901
+ .resolvedShutterSeconds;
1902
+ const holdApplied = holdPolicy !== undefined &&
1903
+ lineShutterSeconds >
1904
+ holdPolicy
1905
+ .slowestPreferredShutterSeconds &&
1906
+ !settingEquals(lineShutterSeconds, holdPolicy
1907
+ .slowestPreferredShutterSeconds);
1908
+ const heldShutterSeconds = holdApplied
1909
+ ? selectShutterNotLongerThanTarget(holdPolicy
1910
+ .slowestPreferredShutterSeconds, input.capabilities).shutterSeconds
1911
+ : lineShutterSeconds;
1912
+ const isoPass = autoIsoPass(heldShutterSeconds);
1913
+ let resolvedSettings = isoPass.resolvedSettings;
1914
+ let residual = isoPass.targetResidual;
1915
+ let fallbackUsed = false;
1916
+ if (holdApplied &&
1917
+ holdPolicy.afterMaximumIso ===
1918
+ "allow-slower-shutter" &&
1919
+ isoPass.isoResolution.clamped ===
1920
+ "maximum" &&
1921
+ isoPass.targetResidual.state ===
1922
+ "under-target") {
1923
+ // At the Auto ISO maximum the shutter may lengthen toward the target, but not past the line's own shutter.
1924
+ const maxIso = isoPass.resolvedSettings.iso;
1925
+ const atReference = opticalExposureFactor(selection.resolvedAperture, referenceExposure
1926
+ .shutterSeconds, referenceExposure) *
1927
+ (maxIso /
1928
+ referenceExposure.iso);
1929
+ const idealShutter = referenceExposure
1930
+ .shutterSeconds *
1931
+ (target
1932
+ .requiredExposureScaleToTarget /
1933
+ atReference);
1934
+ const beyondLine = idealShutter >=
1935
+ lineShutterSeconds;
1936
+ const shutter = beyondLine
1937
+ ? {
1938
+ shutterSeconds: lineShutterSeconds,
1939
+ limitingConstraint: isoPass.targetResidual
1940
+ .limitingConstraint
1941
+ }
1942
+ : resolveAutomaticShutter(idealShutter, input.capabilities);
1943
+ fallbackUsed = true;
1944
+ resolvedSettings = {
1945
+ aperture: selection.resolvedAperture,
1946
+ shutterSeconds: shutter.shutterSeconds,
1947
+ iso: maxIso
1948
+ };
1949
+ residual =
1950
+ targetResidual(target, opticalExposureFactor(selection.resolvedAperture, shutter.shutterSeconds, referenceExposure) *
1951
+ (maxIso /
1952
+ referenceExposure.iso), shutter.limitingConstraint);
1792
1953
  }
1793
1954
  return {
1794
1955
  resolverVersion: EXPOSURE_MODE_RESOLVER_VERSION,
@@ -1814,8 +1975,43 @@ export function resolveProgramAutoExposureMode(input) {
1814
1975
  idealIsoBeforeConstraints: isoPass
1815
1976
  .idealIsoBeforeConstraints,
1816
1977
  isoResolution: isoPass.isoResolution,
1817
- resolvedSettings: isoPass.resolvedSettings,
1818
- targetResidual: isoPass.targetResidual
1978
+ resolvedSettings,
1979
+ targetResidual: residual,
1980
+ ...(holdPolicy === undefined
1981
+ ? {}
1982
+ : {
1983
+ slowestPreferredShutter: {
1984
+ ...holdPolicy,
1985
+ applied: holdApplied,
1986
+ afterMaximumIsoFallbackUsed: fallbackUsed
1987
+ }
1988
+ })
1989
+ };
1990
+ }
1991
+ function validateProgramSlowestPreferredShutter(value, capabilities) {
1992
+ if (value === undefined) {
1993
+ return undefined;
1994
+ }
1995
+ const record = requireRecord(value, "isoControl.slowestPreferredShutter");
1996
+ rejectUnknownFields(record, ["slowestPreferredShutterSeconds", "afterMaximumIso"], "isoControl.slowestPreferredShutter");
1997
+ if (record.afterMaximumIso !==
1998
+ "allow-slower-shutter" &&
1999
+ record.afterMaximumIso !==
2000
+ "hold-preferred-shutter") {
2001
+ throw new InvalidConfigurationError('isoControl.slowestPreferredShutter.afterMaximumIso must be "allow-slower-shutter" or "hold-preferred-shutter".');
2002
+ }
2003
+ const seconds = requirePositiveFinite(record.slowestPreferredShutterSeconds, "isoControl.slowestPreferredShutter.slowestPreferredShutterSeconds");
2004
+ if (seconds <
2005
+ capabilities.shutter
2006
+ .minimumSeconds ||
2007
+ seconds >
2008
+ capabilities.shutter
2009
+ .maximumSeconds) {
2010
+ throw new InvalidScientificInputError("isoControl.slowestPreferredShutter.slowestPreferredShutterSeconds lies outside the resolved shutter capability range.");
2011
+ }
2012
+ return {
2013
+ slowestPreferredShutterSeconds: seconds,
2014
+ afterMaximumIso: record.afterMaximumIso
1819
2015
  };
1820
2016
  }
1821
2017
  /**
@@ -1847,6 +2043,7 @@ export function resolveFullAutoExposureMode(input) {
1847
2043
  "nearest-log2-lower-on-tie") {
1848
2044
  throw new InvalidConfigurationError("Full Auto exposure policy is invalid.");
1849
2045
  }
2046
+ const autoBounds = autoIsoBounds(input.capabilities, input.policy.autoIsoLimits, "policy.autoIsoLimits");
1850
2047
  const programLine = validateProgramLineForResolution(input.policy.programLine, referenceExposure, input.capabilities);
1851
2048
  const blocked = (blocker) => ({
1852
2049
  resolverVersion: EXPOSURE_MODE_RESOLVER_VERSION,
@@ -1890,7 +2087,7 @@ export function resolveFullAutoExposureMode(input) {
1890
2087
  ? "auto-iso-unknown"
1891
2088
  : "auto-iso-unsupported");
1892
2089
  }
1893
- const baselineIso = minimumSelectableIso(input.capabilities);
2090
+ const baselineIso = minimumSelectableIso(input.capabilities, autoBounds);
1894
2091
  const requestedOpticalStops = target
1895
2092
  .exposureOffsetStopsToTarget -
1896
2093
  Math.log2(baselineIso /
@@ -1906,7 +2103,15 @@ export function resolveFullAutoExposureMode(input) {
1906
2103
  isoControl: {
1907
2104
  kind: "automatic",
1908
2105
  quantizationPolicy: input.policy
1909
- .isoQuantizationPolicy
2106
+ .isoQuantizationPolicy,
2107
+ ...(input.policy
2108
+ .autoIsoLimits ===
2109
+ undefined
2110
+ ? {}
2111
+ : {
2112
+ autoIsoLimits: input.policy
2113
+ .autoIsoLimits
2114
+ })
1910
2115
  }
1911
2116
  });
1912
2117
  if (isoPass.status !== "resolved" ||