@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.
- package/CHANGELOG.md +144 -1
- package/CITATION.cff +1 -1
- package/README.md +7 -7
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +3 -0
- package/dist/camera/lens-projection.js +2 -1
- package/dist/camera/lens-projection.js.map +1 -1
- package/dist/capture/dng-import.js +1 -1
- package/dist/capture/dng-import.js.map +1 -1
- package/dist/capture/export-jpeg.d.ts +1 -1
- package/dist/capture/export-jpeg.js +6 -4
- package/dist/capture/export-jpeg.js.map +1 -1
- package/dist/capture/host-yield-cadence.js +1 -1
- package/dist/capture/host-yield-cadence.js.map +1 -1
- package/dist/capture/native-environment-optical-stage.js +1 -1
- package/dist/capture/native-environment-optical-stage.js.map +1 -1
- package/dist/capture/native-raw.js +1 -1
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/packed-sensor-raw.js +3 -3
- package/dist/capture/packed-sensor-raw.js.map +1 -1
- package/dist/capture/sensor-raw-producer.js +1 -1
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/color/capture-color.js +3 -3
- package/dist/color/capture-color.js.map +1 -1
- package/dist/color/chromatic-adaptation.js +2 -2
- package/dist/color/chromatic-adaptation.js.map +1 -1
- package/dist/color/color-matching-functions.js +1 -1
- package/dist/color/color-matching-functions.js.map +1 -1
- package/dist/color/colorimetry-math.js +2 -2
- package/dist/color/colorimetry-math.js.map +1 -1
- package/dist/color/illuminants.js +3 -3
- package/dist/color/illuminants.js.map +1 -1
- package/dist/color/rgb-color-spaces.js +1 -1
- package/dist/color/rgb-color-spaces.js.map +1 -1
- package/dist/color/sensor-color-development.js +1 -1
- package/dist/color/sensor-color-development.js.map +1 -1
- package/dist/core/record-validation.js +3 -3
- package/dist/core/record-validation.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/equipment/generic-prime-lenses.js +3 -3
- package/dist/equipment/generic-prime-lenses.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +23 -1
- package/dist/equipment/generic-tier-assets.js +58 -30
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.d.ts +6 -3
- package/dist/equipment/generic-tier-presets.js +26 -13
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
- package/dist/exposure/auto-iso-slowest-shutter.js +217 -0
- package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
- package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
- package/dist/exposure/exposure-mode-resolver.js +238 -33
- package/dist/exposure/exposure-mode-resolver.js.map +1 -1
- package/dist/exposure/flash-ttl.js +1 -1
- package/dist/exposure/flash-ttl.js.map +1 -1
- package/dist/exposure/flash.js +1 -1
- package/dist/exposure/flash.js.map +1 -1
- package/dist/facade/chart-material-layers.js +2 -2
- package/dist/facade/chart-material-layers.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +1 -1
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +45 -8
- package/dist/facade/chart-scenes.js +101 -44
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/color-patches.d.ts +129 -0
- package/dist/facade/color-patches.js +209 -0
- package/dist/facade/color-patches.js.map +1 -0
- package/dist/facade/diffraction-provider.js +12 -13
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu.js +1 -1
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view.d.ts +27 -0
- package/dist/facade/display-view.js +234 -65
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/scene-radiance.js +2 -2
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +139 -12
- package/dist/facade/simulate-capture.js +209 -36
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +2 -2
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.js +2 -2
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/display-view-composite-material-module.js +1 -1
- package/dist/gpu/display-view-composite-material-module.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
- package/dist/gpu/display-view-quality-gpu.js +24 -14
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/temporal-sum-gpu.js +23 -23
- package/dist/gpu/webgpu-runtime.js +1 -1
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
- package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
- package/dist/gpu/wgsl-chart-kernel.js +117 -117
- package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.js +522 -488
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/gpu/wgsl-kernels.js +203 -203
- package/dist/gpu/wgsl-kernels.js.map +1 -1
- package/dist/gpu/wgsl-sensor-kernel.js +302 -302
- package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/motion/temporal-exposure-sum.js +1 -1
- package/dist/motion/temporal-exposure-sum.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.js +4 -4
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/front-of-lens-filter.js +1 -1
- package/dist/optics/front-of-lens-filter.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +10 -0
- package/dist/optics/irradiance-convolution.js +49 -14
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/output/native-capture-sdr.js +1 -1
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/output/png-encoder.js +2 -2
- package/dist/output/png-encoder.js.map +1 -1
- package/dist/output/print-jpeg.js +13 -10
- package/dist/output/print-jpeg.js.map +1 -1
- package/dist/output/text-metadata.d.ts +22 -2
- package/dist/output/text-metadata.js +58 -9
- package/dist/output/text-metadata.js.map +1 -1
- package/dist/sensor/fixed-pattern-noise.js +1 -1
- package/dist/sensor/fixed-pattern-noise.js.map +1 -1
- package/dist/sensor/readout-exposure-linkage.js +1 -1
- package/dist/sensor/readout-exposure-linkage.js.map +1 -1
- package/docs/API_REFERENCE.md +49 -1
- package/docs/QUICK_START.md +104 -9
- package/docs/USAGE.md +241 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-tier-assets.md +41 -1
- package/docs/api/equipment-generic-tier-presets.md +11 -3
- package/docs/api/exports.json +172 -2
- package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
- package/docs/api/exposure-exposure-mode-resolver.md +68 -0
- package/docs/api/facade-chart-scenes.md +36 -5
- package/docs/api/facade-color-patches.md +150 -0
- package/docs/api/facade-simulate-capture.md +140 -9
- package/docs/api/output-text-metadata.md +34 -0
- package/llms.txt +1 -0
- package/package.json +2 -2
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
* bytes, with no unpaired UTF-16 surrogate (so it encodes to valid UTF-8 without replacement), no NUL or other
|
|
25
25
|
* C0/C1 control character except line feed, and no U+FFFE/U+FFFF (not XML 1.0 characters). Line breaks are a
|
|
26
26
|
* single line feed, as the PNG text chunks require.
|
|
27
|
+
* Root exports (#388): `addJpegTextMetadata` and `addPngTextMetadata` add the text to a JPEG or PNG that the caller
|
|
28
|
+
* encoded (for example a file assembled from `output.region` parts and `composePrintJpegStrips` or `serializePngImage`),
|
|
29
|
+
* with the same bytes as `simulateCapture`'s `output.metadata`, which calls the same two functions.
|
|
27
30
|
* @see docs/QUICK_START.md "Text metadata (2.4)".
|
|
28
31
|
*/
|
|
29
32
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
@@ -113,12 +116,30 @@ function startsWith(bytes, offset, prefix) {
|
|
|
113
116
|
return false;
|
|
114
117
|
return true;
|
|
115
118
|
}
|
|
119
|
+
/** The checked metadata of a root writer: an object of the given fields, `metadata` in the messages. */
|
|
120
|
+
function requireTextMetadata(value, writer) {
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
throw new InvalidConfigurationError(writer + " metadata must be an object.");
|
|
123
|
+
return parseOutputTextMetadata(value, writer + " metadata");
|
|
124
|
+
}
|
|
116
125
|
/**
|
|
117
126
|
* Returns a copy of a baseline JPEG with the text fields added: merged into its XMP APP1 packet (or a new packet
|
|
118
127
|
* after the last leading APPn segment when it has none), plus a COM segment for `comment`. Every other byte,
|
|
119
|
-
* including the entropy-coded data, is unchanged. Reads marker segments only up to SOS.
|
|
128
|
+
* including the entropy-coded data, is unchanged. Reads marker segments only up to SOS. The engine's own
|
|
129
|
+
* `output.metadata` path calls this function, so for the same JPEG and text the bytes are the same.
|
|
130
|
+
*
|
|
131
|
+
* Root export (#388). `metadata` is checked as `output.metadata` is (see the module notes); a JPEG without SOI,
|
|
132
|
+
* a malformed or truncated segment before SOS, an XMP APP1 segment after a non-APPn segment, an XMP packet that is
|
|
133
|
+
* not UTF-8 or has no `rdf:RDF`, or text that makes the packet larger than one APP1 segment fails with
|
|
134
|
+
* `InvalidConfigurationError`. The input is not changed.
|
|
135
|
+
* @param jpeg A baseline JPEG stream, for example `createPrintJpegTask`'s or `composePrintJpegStrips`'s bytes.
|
|
136
|
+
* @param metadata The text: at least one of description, software, copyright and comment.
|
|
137
|
+
* @returns A new array holding the JPEG with the text.
|
|
120
138
|
*/
|
|
121
139
|
export function addJpegTextMetadata(jpeg, metadata) {
|
|
140
|
+
if (!(jpeg instanceof Uint8Array))
|
|
141
|
+
throw new InvalidConfigurationError("addJpegTextMetadata jpeg must be a Uint8Array.");
|
|
142
|
+
const text = requireTextMetadata(metadata, "addJpegTextMetadata");
|
|
122
143
|
if (jpeg.length < 4 || jpeg[0] !== 0xff || jpeg[1] !== 0xd8)
|
|
123
144
|
throw new InvalidConfigurationError("JPEG text metadata needs a JPEG stream starting with SOI.");
|
|
124
145
|
const identifier = encoder.encode(XMP_IDENTIFIER);
|
|
@@ -129,32 +150,46 @@ export function addJpegTextMetadata(jpeg, metadata) {
|
|
|
129
150
|
const marker = jpeg[offset + 1];
|
|
130
151
|
if (marker === 0xda)
|
|
131
152
|
break;
|
|
132
|
-
|
|
153
|
+
// Before SOS every marker has a length (T.81 B.1.1.4): SOI, EOI, RSTm, TEM and fill bytes there are malformed.
|
|
154
|
+
const length = (jpeg[offset + 2] << 8) | jpeg[offset + 3];
|
|
155
|
+
if (marker === 0xff || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd9) || length < 2)
|
|
156
|
+
throw new InvalidConfigurationError("JPEG text metadata found a malformed marker segment.");
|
|
157
|
+
const end = offset + 2 + length;
|
|
133
158
|
if (end > jpeg.length)
|
|
134
159
|
throw new InvalidConfigurationError("JPEG text metadata found a truncated marker segment.");
|
|
135
|
-
if (marker === 0xe1 && !xmp && startsWith(jpeg, offset + 4, identifier))
|
|
160
|
+
if (marker === 0xe1 && !xmp && startsWith(jpeg, offset + 4, identifier)) {
|
|
161
|
+
// The packet is rewritten in place, so it must lie in the leading APPn run that the COM segment follows.
|
|
162
|
+
if (!inLeadingApps)
|
|
163
|
+
throw new InvalidConfigurationError("JPEG text metadata found an XMP APP1 segment after a non-APPn segment.");
|
|
136
164
|
xmp = { start: offset, end };
|
|
165
|
+
}
|
|
137
166
|
if (inLeadingApps && marker >= 0xe0 && marker <= 0xef)
|
|
138
167
|
appEnd = end;
|
|
139
168
|
else
|
|
140
169
|
inLeadingApps = false;
|
|
141
170
|
offset = end;
|
|
142
171
|
}
|
|
143
|
-
const description = xmpTextMetadataDescription(
|
|
172
|
+
const description = xmpTextMetadataDescription(text);
|
|
144
173
|
let xmpPacket;
|
|
145
174
|
if (xmp) {
|
|
146
|
-
|
|
175
|
+
let packet;
|
|
176
|
+
try {
|
|
177
|
+
packet = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(jpeg.subarray(xmp.start + 4 + identifier.length, xmp.end));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
throw new InvalidConfigurationError("JPEG text metadata found an XMP packet that is not valid UTF-8.");
|
|
181
|
+
}
|
|
147
182
|
const close = packet.lastIndexOf("</rdf:RDF>");
|
|
148
183
|
if (close < 0)
|
|
149
184
|
throw new InvalidConfigurationError("JPEG text metadata found an XMP packet without rdf:RDF.");
|
|
150
185
|
xmpPacket = encoder.encode(packet.slice(0, close) + description + packet.slice(close));
|
|
151
186
|
}
|
|
152
187
|
else
|
|
153
|
-
xmpPacket = xmpTextMetadataPacket(
|
|
188
|
+
xmpPacket = xmpTextMetadataPacket(text);
|
|
154
189
|
if (xmpPacket.length > JPEG_XMP_MAXIMUM_PACKET_BYTES)
|
|
155
190
|
throw new InvalidConfigurationError("The text metadata makes the JPEG XMP packet " + xmpPacket.length + " bytes; one APP1 segment holds at most " + JPEG_XMP_MAXIMUM_PACKET_BYTES + ".");
|
|
156
191
|
const xmpSegment = segment(0xe1, concat([identifier, xmpPacket]));
|
|
157
|
-
const com =
|
|
192
|
+
const com = text.comment === undefined ? new Uint8Array(0) : segment(0xfe, encoder.encode(text.comment));
|
|
158
193
|
if (xmp)
|
|
159
194
|
return concat([jpeg.subarray(0, xmp.start), xmpSegment, jpeg.subarray(xmp.end, appEnd), com, jpeg.subarray(appEnd)]);
|
|
160
195
|
return concat([jpeg.subarray(0, appEnd), xmpSegment, com, jpeg.subarray(appEnd)]);
|
|
@@ -184,8 +219,22 @@ export function pngTextMetadataChunks(metadata) {
|
|
|
184
219
|
: pngChunk("iTXt", concat([keyword, Uint8Array.of(0, 0, 0, 0, 0), encoder.encode(text)]));
|
|
185
220
|
});
|
|
186
221
|
}
|
|
187
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* Returns a copy of a PNG with one text chunk per field inserted before the first IDAT chunk. Other chunks are
|
|
224
|
+
* unchanged. The engine's own `output.metadata` path calls this function, so for the same PNG and text the bytes
|
|
225
|
+
* are the same.
|
|
226
|
+
*
|
|
227
|
+
* Root export (#388). `metadata` is checked as `output.metadata` is (see the module notes); a file without the PNG
|
|
228
|
+
* signature, or without an IDAT chunk inside its bytes, fails with `InvalidConfigurationError`. Chunk CRCs before
|
|
229
|
+
* IDAT are not checked. The input is not changed.
|
|
230
|
+
* @param png A PNG file, for example `serializePngImage`'s bytes.
|
|
231
|
+
* @param metadata The text: at least one of description, software, copyright and comment.
|
|
232
|
+
* @returns A new array holding the PNG with the text.
|
|
233
|
+
*/
|
|
188
234
|
export function addPngTextMetadata(png, metadata) {
|
|
235
|
+
if (!(png instanceof Uint8Array))
|
|
236
|
+
throw new InvalidConfigurationError("addPngTextMetadata png must be a Uint8Array.");
|
|
237
|
+
const text = requireTextMetadata(metadata, "addPngTextMetadata");
|
|
189
238
|
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
190
239
|
if (png.length < 8 || signature.some((b, i) => png[i] !== b))
|
|
191
240
|
throw new InvalidConfigurationError("PNG text metadata needs a PNG signature.");
|
|
@@ -193,7 +242,7 @@ export function addPngTextMetadata(png, metadata) {
|
|
|
193
242
|
for (let offset = 8; offset + 12 <= png.length;) {
|
|
194
243
|
const length = view.getUint32(offset), type = String.fromCharCode(...png.subarray(offset + 4, offset + 8));
|
|
195
244
|
if (type === "IDAT")
|
|
196
|
-
return concat([png.subarray(0, offset), ...pngTextMetadataChunks(
|
|
245
|
+
return concat([png.subarray(0, offset), ...pngTextMetadataChunks(text), png.subarray(offset)]);
|
|
197
246
|
offset += 12 + length;
|
|
198
247
|
}
|
|
199
248
|
throw new InvalidConfigurationError("PNG text metadata found no IDAT chunk.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-metadata.js","sourceRoot":"","sources":["../../src/output/text-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Optional caller-supplied text metadata for encoded images (2.4): a description, the creating software, a\n * copyright notice and a comment. The engine writes the caller's text unchanged and adds no text of its own\n * (in a DNG the description follows the existing simulated-RAW statement).\n * Independently written from these free specifications; no third-party source was used:\n *\n * - JPEG: the existing XMP packet in APP1 (Adobe XMP Specification Part 3, 2020, §1.1.3: identifier\n * \"http://ns.adobe.com/xap/1.0/\" and a UTF-8 packet of at most 65 502 bytes, §1.1.3.1). The fields are added\n * as one more top-level `rdf:Description` of the same resource (`rdf:about=\"\"`): XMP Part 1 (2012, identical\n * to ISO 16684-1:2011) §7.4 lets properties be \"arbitrarily apportioned among the rdf:Description elements\"\n * of the single `rdf:RDF`. Mapping: `dc:description` and `dc:rights` as language alternatives with\n * `x-default` (Part 1 §8.3), `xmp:CreatorTool` as text (Part 1 §8.4) and `exif:UserComment` as a language\n * alternative (Part 2, 2022, §3.4, the Exif namespace). The comment is also a COM marker segment (ITU-T T.81\n * §B.2.4.5, \"the interpretation is left to the application\"; written as UTF-8) after the APPn segments.\n * - PNG: one text chunk per field with the predefined keywords Description, Software, Copyright and Comment\n * (W3C PNG Specification, Third Edition, §11.3.3.1, Table 21). `tEXt` (§11.3.3.2; printable Latin-1 plus line\n * feed) when every character fits, otherwise an uncompressed `iTXt` (§11.3.3.4; UTF-8, empty language tag and\n * translated keyword). The chunks go before the first IDAT, as §12.9 recommends for small text.\n * - DNG: `serializeNativeRawDng` writes the same XMP fields as a packet in TIFF tag 700 (XMP Part 3 §1.1.6,\n * Table 13) plus the TIFF ASCII tags that XMP Part 3 §3.2.3 maps to them.\n *\n * Validation: each given field is a non-empty string of at most `OUTPUT_TEXT_METADATA_MAXIMUM_BYTES` UTF-8\n * bytes, with no unpaired UTF-16 surrogate (so it encodes to valid UTF-8 without replacement), no NUL or other\n * C0/C1 control character except line feed, and no U+FFFE/U+FFFF (not XML 1.0 characters). Line breaks are a\n * single line feed, as the PNG text chunks require.\n * @see docs/QUICK_START.md \"Text metadata (2.4)\".\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { pngCrc32 } from \"./png-encoder.js\";\n\n/** Caller-supplied text written into JPEG, PNG and DNG outputs. Every field is optional; at least one must be set. */\nexport interface OutputTextMetadata {\n /** What the image shows. JPEG/DNG `dc:description`, PNG Description, DNG ImageDescription (appended when ASCII). */\n description?: string;\n /** Name and version of the caller's software. JPEG/DNG `xmp:CreatorTool`, PNG Software. */\n software?: string;\n /** Rights statement. JPEG/DNG `dc:rights`; the PNG keyword and DNG tag 33432 (when ASCII) of the same name. */\n copyright?: string;\n /** Free-text comment. JPEG COM segment and `exif:UserComment`, PNG Comment, DNG `exif:UserComment`. */\n comment?: string;\n}\n\n/** Largest UTF-8 length of one metadata field, in bytes. Four escaped fields still fit one JPEG APP1 segment. */\nexport const OUTPUT_TEXT_METADATA_MAXIMUM_BYTES = 2048;\nconst FIELDS = [\"description\", \"software\", \"copyright\", \"comment\"] as const;\ntype Field = typeof FIELDS[number];\n/** PNG Third Edition §11.3.3.1 Table 21 keyword for each field: the field name with an initial capital. */\nconst pngKeyword = (field: Field): string => field[0]!.toUpperCase() + field.slice(1);\nconst XMP_IDENTIFIER = \"http://ns.adobe.com/xap/1.0/\\0\";\n/** Largest XMP packet in one JPEG APP1 segment (XMP Part 3 §1.1.3.1). */\nconst JPEG_XMP_MAXIMUM_PACKET_BYTES = 65502;\nconst encoder = new TextEncoder();\n\n/**\n * Checks caller text metadata. Returns undefined when `value` is undefined, otherwise a frozen copy with only\n * the given fields. `path` prefixes the error messages, for example \"output.metadata\".\n */\nexport function parseOutputTextMetadata(value: unknown, path: string): Readonly<OutputTextMetadata> | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) throw new InvalidConfigurationError(path + \" must be an object.\");\n const out: OutputTextMetadata = {};\n for (const key of Object.keys(value)) if (!(FIELDS as readonly string[]).includes(key)) throw new InvalidConfigurationError(\"Unknown \" + path + \" field: \" + key + \".\");\n for (const field of FIELDS) {\n const text = (value as Record<string, unknown>)[field];\n if (text === undefined) continue;\n const name = path + \".\" + field;\n if (typeof text !== \"string\" || text.length === 0) throw new InvalidConfigurationError(name + \" must be a non-empty string.\");\n if (/\\p{Cs}/u.test(text)) throw new InvalidConfigurationError(name + \" must be valid Unicode text (it has an unpaired UTF-16 surrogate).\");\n if (text.includes(\"\\0\")) throw new InvalidConfigurationError(name + \" must not contain NUL characters.\");\n // eslint-disable-next-line no-control-regex\n if (/[\\u0001-\\u0009\\u000b-\\u001f\\u007f-\\u009f]/u.test(text)) throw new InvalidConfigurationError(name + \" must not contain control characters other than line feed (\\\\n), or U+FFFE/U+FFFF.\");\n const bytes = encoder.encode(text).length;\n if (bytes > OUTPUT_TEXT_METADATA_MAXIMUM_BYTES) throw new InvalidConfigurationError(name + \" must be at most \" + OUTPUT_TEXT_METADATA_MAXIMUM_BYTES + \" UTF-8 bytes (it has \" + bytes + \").\");\n out[field] = text;\n }\n if (Object.keys(out).length === 0) throw new InvalidConfigurationError(path + \" must set at least one of description, software, copyright or comment.\");\n return Object.freeze(out);\n}\n\nfunction xmlText(text: string): string {\n return text.replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\");\n}\nconst alt = (text: string): string => '<rdf:Alt><rdf:li xml:lang=\"x-default\">' + xmlText(text) + \"</rdf:li></rdf:Alt>\";\n\n/** One `rdf:Description` of the current resource with the given fields, declaring only the namespaces it uses. */\nexport function xmpTextMetadataDescription(metadata: Readonly<OutputTextMetadata>): string {\n const dc = metadata.description !== undefined || metadata.copyright !== undefined;\n return '<rdf:Description rdf:about=\"\"' + (dc ? ' xmlns:dc=\"http://purl.org/dc/elements/1.1/\"' : \"\") +\n (metadata.software !== undefined ? ' xmlns:xmp=\"http://ns.adobe.com/xap/1.0/\"' : \"\") +\n (metadata.comment !== undefined ? ' xmlns:exif=\"http://ns.adobe.com/exif/1.0/\"' : \"\") + \">\" +\n (metadata.description !== undefined ? \"<dc:description>\" + alt(metadata.description) + \"</dc:description>\" : \"\") +\n (metadata.copyright !== undefined ? \"<dc:rights>\" + alt(metadata.copyright) + \"</dc:rights>\" : \"\") +\n (metadata.software !== undefined ? \"<xmp:CreatorTool>\" + xmlText(metadata.software) + \"</xmp:CreatorTool>\" : \"\") +\n (metadata.comment !== undefined ? \"<exif:UserComment>\" + alt(metadata.comment) + \"</exif:UserComment>\" : \"\") + \"</rdf:Description>\";\n}\n\n/** A complete XMP packet (`x:xmpmeta` wrapper, no `xpacket` processing instructions) holding only the text fields. */\nexport function xmpTextMetadataPacket(metadata: Readonly<OutputTextMetadata>): Uint8Array {\n return encoder.encode('<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">' +\n xmpTextMetadataDescription(metadata) + \"</rdf:RDF></x:xmpmeta>\");\n}\n\nfunction concat(parts: readonly Uint8Array[]): Uint8Array {\n const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));\n let offset = 0;\n for (const part of parts) { out.set(part, offset); offset += part.length; }\n return out;\n}\nfunction segment(marker: number, payload: Uint8Array): Uint8Array {\n return concat([Uint8Array.of(0xff, marker, (payload.length + 2) >> 8, (payload.length + 2) & 255), payload]);\n}\nfunction startsWith(bytes: Uint8Array, offset: number, prefix: Uint8Array): boolean {\n if (offset + prefix.length > bytes.length) return false;\n for (let i = 0; i < prefix.length; i++) if (bytes[offset + i] !== prefix[i]) return false;\n return true;\n}\n\n/**\n * Returns a copy of a baseline JPEG with the text fields added: merged into its XMP APP1 packet (or a new packet\n * after the last leading APPn segment when it has none), plus a COM segment for `comment`. Every other byte,\n * including the entropy-coded data, is unchanged. Reads marker segments only up to SOS.\n */\nexport function addJpegTextMetadata(jpeg: Uint8Array, metadata: Readonly<OutputTextMetadata>): Uint8Array {\n if (jpeg.length < 4 || jpeg[0] !== 0xff || jpeg[1] !== 0xd8) throw new InvalidConfigurationError(\"JPEG text metadata needs a JPEG stream starting with SOI.\");\n const identifier = encoder.encode(XMP_IDENTIFIER);\n let offset = 2, xmp: { start: number; end: number } | undefined, appEnd = 2, inLeadingApps = true;\n for (;;) {\n if (offset + 4 > jpeg.length || jpeg[offset] !== 0xff) throw new InvalidConfigurationError(\"JPEG text metadata found a malformed marker segment.\");\n const marker = jpeg[offset + 1]!;\n if (marker === 0xda) break;\n const end = offset + 2 + ((jpeg[offset + 2]! << 8) | jpeg[offset + 3]!);\n if (end > jpeg.length) throw new InvalidConfigurationError(\"JPEG text metadata found a truncated marker segment.\");\n if (marker === 0xe1 && !xmp && startsWith(jpeg, offset + 4, identifier)) xmp = { start: offset, end };\n if (inLeadingApps && marker >= 0xe0 && marker <= 0xef) appEnd = end; else inLeadingApps = false;\n offset = end;\n }\n const description = xmpTextMetadataDescription(metadata);\n let xmpPacket: Uint8Array;\n if (xmp) {\n const packet = new TextDecoder(\"utf-8\", { fatal: true }).decode(jpeg.subarray(xmp.start + 4 + identifier.length, xmp.end));\n const close = packet.lastIndexOf(\"</rdf:RDF>\");\n if (close < 0) throw new InvalidConfigurationError(\"JPEG text metadata found an XMP packet without rdf:RDF.\");\n xmpPacket = encoder.encode(packet.slice(0, close) + description + packet.slice(close));\n } else xmpPacket = xmpTextMetadataPacket(metadata);\n if (xmpPacket.length > JPEG_XMP_MAXIMUM_PACKET_BYTES) throw new InvalidConfigurationError(\"The text metadata makes the JPEG XMP packet \" + xmpPacket.length + \" bytes; one APP1 segment holds at most \" + JPEG_XMP_MAXIMUM_PACKET_BYTES + \".\");\n const xmpSegment = segment(0xe1, concat([identifier, xmpPacket]));\n const com = metadata.comment === undefined ? new Uint8Array(0) : segment(0xfe, encoder.encode(metadata.comment));\n if (xmp) return concat([jpeg.subarray(0, xmp.start), xmpSegment, jpeg.subarray(xmp.end, appEnd), com, jpeg.subarray(appEnd)]);\n return concat([jpeg.subarray(0, appEnd), xmpSegment, com, jpeg.subarray(appEnd)]);\n}\n\nfunction latin1Text(text: string): boolean {\n for (const ch of text) { const c = ch.codePointAt(0)!; if (!(c === 10 || (c >= 0x20 && c <= 0x7e) || (c >= 0xa1 && c <= 0xff))) return false; }\n return true;\n}\nfunction pngChunk(type: string, data: Uint8Array): Uint8Array {\n const out = new Uint8Array(12 + data.length), view = new DataView(out.buffer), typeBytes = Uint8Array.from(type, (c) => c.charCodeAt(0));\n view.setUint32(0, data.length); out.set(typeBytes, 4); out.set(data, 8); view.setUint32(8 + data.length, pngCrc32(data, pngCrc32(typeBytes)));\n return out;\n}\n/** The text chunks for the given fields, in the order description, software, copyright, comment. */\nexport function pngTextMetadataChunks(metadata: Readonly<OutputTextMetadata>): Uint8Array[] {\n return FIELDS.filter((field) => metadata[field] !== undefined).map((field) => {\n const keyword = Uint8Array.from(pngKeyword(field), (c) => c.charCodeAt(0)), text = metadata[field]!;\n // tEXt: keyword, NUL, Latin-1 text. iTXt: keyword, NUL, compression flag 0, method 0, empty language tag, NUL, empty translated keyword, NUL, UTF-8 text.\n return latin1Text(text) ? pngChunk(\"tEXt\", concat([keyword, Uint8Array.of(0), Uint8Array.from(text, (c) => c.charCodeAt(0))]))\n : pngChunk(\"iTXt\", concat([keyword, Uint8Array.of(0, 0, 0, 0, 0), encoder.encode(text)]));\n });\n}\n\n/** Returns a copy of a PNG with one text chunk per field inserted before the first IDAT chunk. Other chunks are unchanged. */\nexport function addPngTextMetadata(png: Uint8Array, metadata: Readonly<OutputTextMetadata>): Uint8Array {\n const signature = [137, 80, 78, 71, 13, 10, 26, 10];\n if (png.length < 8 || signature.some((b, i) => png[i] !== b)) throw new InvalidConfigurationError(\"PNG text metadata needs a PNG signature.\");\n const view = new DataView(png.buffer, png.byteOffset, png.byteLength);\n for (let offset = 8; offset + 12 <= png.length;) {\n const length = view.getUint32(offset), type = String.fromCharCode(...png.subarray(offset + 4, offset + 8));\n if (type === \"IDAT\") return concat([png.subarray(0, offset), ...pngTextMetadataChunks(metadata), png.subarray(offset)]);\n offset += 12 + length;\n }\n throw new InvalidConfigurationError(\"PNG text metadata found no IDAT chunk.\");\n}\n"]}
|
|
1
|
+
{"version":3,"file":"text-metadata.js","sourceRoot":"","sources":["../../src/output/text-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Optional caller-supplied text metadata for encoded images (2.4): a description, the creating software, a\n * copyright notice and a comment. The engine writes the caller's text unchanged and adds no text of its own\n * (in a DNG the description follows the existing simulated-RAW statement).\n * Independently written from these free specifications; no third-party source was used:\n *\n * - JPEG: the existing XMP packet in APP1 (Adobe XMP Specification Part 3, 2020, §1.1.3: identifier\n * \"http://ns.adobe.com/xap/1.0/\" and a UTF-8 packet of at most 65 502 bytes, §1.1.3.1). The fields are added\n * as one more top-level `rdf:Description` of the same resource (`rdf:about=\"\"`): XMP Part 1 (2012, identical\n * to ISO 16684-1:2011) §7.4 lets properties be \"arbitrarily apportioned among the rdf:Description elements\"\n * of the single `rdf:RDF`. Mapping: `dc:description` and `dc:rights` as language alternatives with\n * `x-default` (Part 1 §8.3), `xmp:CreatorTool` as text (Part 1 §8.4) and `exif:UserComment` as a language\n * alternative (Part 2, 2022, §3.4, the Exif namespace). The comment is also a COM marker segment (ITU-T T.81\n * §B.2.4.5, \"the interpretation is left to the application\"; written as UTF-8) after the APPn segments.\n * - PNG: one text chunk per field with the predefined keywords Description, Software, Copyright and Comment\n * (W3C PNG Specification, Third Edition, §11.3.3.1, Table 21). `tEXt` (§11.3.3.2; printable Latin-1 plus line\n * feed) when every character fits, otherwise an uncompressed `iTXt` (§11.3.3.4; UTF-8, empty language tag and\n * translated keyword). The chunks go before the first IDAT, as §12.9 recommends for small text.\n * - DNG: `serializeNativeRawDng` writes the same XMP fields as a packet in TIFF tag 700 (XMP Part 3 §1.1.6,\n * Table 13) plus the TIFF ASCII tags that XMP Part 3 §3.2.3 maps to them.\n *\n * Validation: each given field is a non-empty string of at most `OUTPUT_TEXT_METADATA_MAXIMUM_BYTES` UTF-8\n * bytes, with no unpaired UTF-16 surrogate (so it encodes to valid UTF-8 without replacement), no NUL or other\n * C0/C1 control character except line feed, and no U+FFFE/U+FFFF (not XML 1.0 characters). Line breaks are a\n * single line feed, as the PNG text chunks require.\n * Root exports (#388): `addJpegTextMetadata` and `addPngTextMetadata` add the text to a JPEG or PNG that the caller\n * encoded (for example a file assembled from `output.region` parts and `composePrintJpegStrips` or `serializePngImage`),\n * with the same bytes as `simulateCapture`'s `output.metadata`, which calls the same two functions.\n * @see docs/QUICK_START.md \"Text metadata (2.4)\".\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { pngCrc32 } from \"./png-encoder.js\";\n\n/** Caller-supplied text written into JPEG, PNG and DNG outputs. Every field is optional; at least one must be set. */\nexport interface OutputTextMetadata {\n /** What the image shows. JPEG/DNG `dc:description`, PNG Description, DNG ImageDescription (appended when ASCII). */\n description?: string;\n /** Name and version of the caller's software. JPEG/DNG `xmp:CreatorTool`, PNG Software. */\n software?: string;\n /** Rights statement. JPEG/DNG `dc:rights`; the PNG keyword and DNG tag 33432 (when ASCII) of the same name. */\n copyright?: string;\n /** Free-text comment. JPEG COM segment and `exif:UserComment`, PNG Comment, DNG `exif:UserComment`. */\n comment?: string;\n}\n\n/** Largest UTF-8 length of one metadata field, in bytes. Four escaped fields still fit one JPEG APP1 segment. */\nexport const OUTPUT_TEXT_METADATA_MAXIMUM_BYTES = 2048;\nconst FIELDS = [\"description\", \"software\", \"copyright\", \"comment\"] as const;\ntype Field = typeof FIELDS[number];\n/** PNG Third Edition §11.3.3.1 Table 21 keyword for each field: the field name with an initial capital. */\nconst pngKeyword = (field: Field): string => field[0]!.toUpperCase() + field.slice(1);\nconst XMP_IDENTIFIER = \"http://ns.adobe.com/xap/1.0/\\0\";\n/** Largest XMP packet in one JPEG APP1 segment (XMP Part 3 §1.1.3.1). */\nconst JPEG_XMP_MAXIMUM_PACKET_BYTES = 65502;\nconst encoder = new TextEncoder();\n\n/**\n * Checks caller text metadata. Returns undefined when `value` is undefined, otherwise a frozen copy with only\n * the given fields. `path` prefixes the error messages, for example \"output.metadata\".\n */\nexport function parseOutputTextMetadata(value: unknown, path: string): Readonly<OutputTextMetadata> | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) throw new InvalidConfigurationError(path + \" must be an object.\");\n const out: OutputTextMetadata = {};\n for (const key of Object.keys(value)) if (!(FIELDS as readonly string[]).includes(key)) throw new InvalidConfigurationError(\"Unknown \" + path + \" field: \" + key + \".\");\n for (const field of FIELDS) {\n const text = (value as Record<string, unknown>)[field];\n if (text === undefined) continue;\n const name = path + \".\" + field;\n if (typeof text !== \"string\" || text.length === 0) throw new InvalidConfigurationError(name + \" must be a non-empty string.\");\n if (/\\p{Cs}/u.test(text)) throw new InvalidConfigurationError(name + \" must be valid Unicode text (it has an unpaired UTF-16 surrogate).\");\n if (text.includes(\"\\0\")) throw new InvalidConfigurationError(name + \" must not contain NUL characters.\");\n // eslint-disable-next-line no-control-regex\n if (/[\\u0001-\\u0009\\u000b-\\u001f\\u007f-\\u009f]/u.test(text)) throw new InvalidConfigurationError(name + \" must not contain control characters other than line feed (\\\\n), or U+FFFE/U+FFFF.\");\n const bytes = encoder.encode(text).length;\n if (bytes > OUTPUT_TEXT_METADATA_MAXIMUM_BYTES) throw new InvalidConfigurationError(name + \" must be at most \" + OUTPUT_TEXT_METADATA_MAXIMUM_BYTES + \" UTF-8 bytes (it has \" + bytes + \").\");\n out[field] = text;\n }\n if (Object.keys(out).length === 0) throw new InvalidConfigurationError(path + \" must set at least one of description, software, copyright or comment.\");\n return Object.freeze(out);\n}\n\nfunction xmlText(text: string): string {\n return text.replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\");\n}\nconst alt = (text: string): string => '<rdf:Alt><rdf:li xml:lang=\"x-default\">' + xmlText(text) + \"</rdf:li></rdf:Alt>\";\n\n/** One `rdf:Description` of the current resource with the given fields, declaring only the namespaces it uses. */\nexport function xmpTextMetadataDescription(metadata: Readonly<OutputTextMetadata>): string {\n const dc = metadata.description !== undefined || metadata.copyright !== undefined;\n return '<rdf:Description rdf:about=\"\"' + (dc ? ' xmlns:dc=\"http://purl.org/dc/elements/1.1/\"' : \"\") +\n (metadata.software !== undefined ? ' xmlns:xmp=\"http://ns.adobe.com/xap/1.0/\"' : \"\") +\n (metadata.comment !== undefined ? ' xmlns:exif=\"http://ns.adobe.com/exif/1.0/\"' : \"\") + \">\" +\n (metadata.description !== undefined ? \"<dc:description>\" + alt(metadata.description) + \"</dc:description>\" : \"\") +\n (metadata.copyright !== undefined ? \"<dc:rights>\" + alt(metadata.copyright) + \"</dc:rights>\" : \"\") +\n (metadata.software !== undefined ? \"<xmp:CreatorTool>\" + xmlText(metadata.software) + \"</xmp:CreatorTool>\" : \"\") +\n (metadata.comment !== undefined ? \"<exif:UserComment>\" + alt(metadata.comment) + \"</exif:UserComment>\" : \"\") + \"</rdf:Description>\";\n}\n\n/** A complete XMP packet (`x:xmpmeta` wrapper, no `xpacket` processing instructions) holding only the text fields. */\nexport function xmpTextMetadataPacket(metadata: Readonly<OutputTextMetadata>): Uint8Array {\n return encoder.encode('<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">' +\n xmpTextMetadataDescription(metadata) + \"</rdf:RDF></x:xmpmeta>\");\n}\n\nfunction concat(parts: readonly Uint8Array[]): Uint8Array {\n const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));\n let offset = 0;\n for (const part of parts) { out.set(part, offset); offset += part.length; }\n return out;\n}\nfunction segment(marker: number, payload: Uint8Array): Uint8Array {\n return concat([Uint8Array.of(0xff, marker, (payload.length + 2) >> 8, (payload.length + 2) & 255), payload]);\n}\nfunction startsWith(bytes: Uint8Array, offset: number, prefix: Uint8Array): boolean {\n if (offset + prefix.length > bytes.length) return false;\n for (let i = 0; i < prefix.length; i++) if (bytes[offset + i] !== prefix[i]) return false;\n return true;\n}\n\n/** The checked metadata of a root writer: an object of the given fields, `metadata` in the messages. */\nfunction requireTextMetadata(value: unknown, writer: string): Readonly<OutputTextMetadata> {\n if (value === undefined) throw new InvalidConfigurationError(writer + \" metadata must be an object.\");\n return parseOutputTextMetadata(value, writer + \" metadata\")!;\n}\n\n/**\n * Returns a copy of a baseline JPEG with the text fields added: merged into its XMP APP1 packet (or a new packet\n * after the last leading APPn segment when it has none), plus a COM segment for `comment`. Every other byte,\n * including the entropy-coded data, is unchanged. Reads marker segments only up to SOS. The engine's own\n * `output.metadata` path calls this function, so for the same JPEG and text the bytes are the same.\n *\n * Root export (#388). `metadata` is checked as `output.metadata` is (see the module notes); a JPEG without SOI,\n * a malformed or truncated segment before SOS, an XMP APP1 segment after a non-APPn segment, an XMP packet that is\n * not UTF-8 or has no `rdf:RDF`, or text that makes the packet larger than one APP1 segment fails with\n * `InvalidConfigurationError`. The input is not changed.\n * @param jpeg A baseline JPEG stream, for example `createPrintJpegTask`'s or `composePrintJpegStrips`'s bytes.\n * @param metadata The text: at least one of description, software, copyright and comment.\n * @returns A new array holding the JPEG with the text.\n */\nexport function addJpegTextMetadata(jpeg: Uint8Array, metadata: Readonly<OutputTextMetadata>): Uint8Array {\n if (!(jpeg instanceof Uint8Array)) throw new InvalidConfigurationError(\"addJpegTextMetadata jpeg must be a Uint8Array.\");\n const text = requireTextMetadata(metadata, \"addJpegTextMetadata\");\n if (jpeg.length < 4 || jpeg[0] !== 0xff || jpeg[1] !== 0xd8) throw new InvalidConfigurationError(\"JPEG text metadata needs a JPEG stream starting with SOI.\");\n const identifier = encoder.encode(XMP_IDENTIFIER);\n let offset = 2, xmp: { start: number; end: number } | undefined, appEnd = 2, inLeadingApps = true;\n for (;;) {\n if (offset + 4 > jpeg.length || jpeg[offset] !== 0xff) throw new InvalidConfigurationError(\"JPEG text metadata found a malformed marker segment.\");\n const marker = jpeg[offset + 1]!;\n if (marker === 0xda) break;\n // Before SOS every marker has a length (T.81 B.1.1.4): SOI, EOI, RSTm, TEM and fill bytes there are malformed.\n const length = (jpeg[offset + 2]! << 8) | jpeg[offset + 3]!;\n if (marker === 0xff || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd9) || length < 2) throw new InvalidConfigurationError(\"JPEG text metadata found a malformed marker segment.\");\n const end = offset + 2 + length;\n if (end > jpeg.length) throw new InvalidConfigurationError(\"JPEG text metadata found a truncated marker segment.\");\n if (marker === 0xe1 && !xmp && startsWith(jpeg, offset + 4, identifier)) {\n // The packet is rewritten in place, so it must lie in the leading APPn run that the COM segment follows.\n if (!inLeadingApps) throw new InvalidConfigurationError(\"JPEG text metadata found an XMP APP1 segment after a non-APPn segment.\");\n xmp = { start: offset, end };\n }\n if (inLeadingApps && marker >= 0xe0 && marker <= 0xef) appEnd = end; else inLeadingApps = false;\n offset = end;\n }\n const description = xmpTextMetadataDescription(text);\n let xmpPacket: Uint8Array;\n if (xmp) {\n let packet: string;\n try { packet = new TextDecoder(\"utf-8\", { fatal: true, ignoreBOM: true }).decode(jpeg.subarray(xmp.start + 4 + identifier.length, xmp.end)); }\n catch { throw new InvalidConfigurationError(\"JPEG text metadata found an XMP packet that is not valid UTF-8.\"); }\n const close = packet.lastIndexOf(\"</rdf:RDF>\");\n if (close < 0) throw new InvalidConfigurationError(\"JPEG text metadata found an XMP packet without rdf:RDF.\");\n xmpPacket = encoder.encode(packet.slice(0, close) + description + packet.slice(close));\n } else xmpPacket = xmpTextMetadataPacket(text);\n if (xmpPacket.length > JPEG_XMP_MAXIMUM_PACKET_BYTES) throw new InvalidConfigurationError(\"The text metadata makes the JPEG XMP packet \" + xmpPacket.length + \" bytes; one APP1 segment holds at most \" + JPEG_XMP_MAXIMUM_PACKET_BYTES + \".\");\n const xmpSegment = segment(0xe1, concat([identifier, xmpPacket]));\n const com = text.comment === undefined ? new Uint8Array(0) : segment(0xfe, encoder.encode(text.comment));\n if (xmp) return concat([jpeg.subarray(0, xmp.start), xmpSegment, jpeg.subarray(xmp.end, appEnd), com, jpeg.subarray(appEnd)]);\n return concat([jpeg.subarray(0, appEnd), xmpSegment, com, jpeg.subarray(appEnd)]);\n}\n\nfunction latin1Text(text: string): boolean {\n for (const ch of text) { const c = ch.codePointAt(0)!; if (!(c === 10 || (c >= 0x20 && c <= 0x7e) || (c >= 0xa1 && c <= 0xff))) return false; }\n return true;\n}\nfunction pngChunk(type: string, data: Uint8Array): Uint8Array {\n const out = new Uint8Array(12 + data.length), view = new DataView(out.buffer), typeBytes = Uint8Array.from(type, (c) => c.charCodeAt(0));\n view.setUint32(0, data.length); out.set(typeBytes, 4); out.set(data, 8); view.setUint32(8 + data.length, pngCrc32(data, pngCrc32(typeBytes)));\n return out;\n}\n/** The text chunks for the given fields, in the order description, software, copyright, comment. */\nexport function pngTextMetadataChunks(metadata: Readonly<OutputTextMetadata>): Uint8Array[] {\n return FIELDS.filter((field) => metadata[field] !== undefined).map((field) => {\n const keyword = Uint8Array.from(pngKeyword(field), (c) => c.charCodeAt(0)), text = metadata[field]!;\n // tEXt: keyword, NUL, Latin-1 text. iTXt: keyword, NUL, compression flag 0, method 0, empty language tag, NUL, empty translated keyword, NUL, UTF-8 text.\n return latin1Text(text) ? pngChunk(\"tEXt\", concat([keyword, Uint8Array.of(0), Uint8Array.from(text, (c) => c.charCodeAt(0))]))\n : pngChunk(\"iTXt\", concat([keyword, Uint8Array.of(0, 0, 0, 0, 0), encoder.encode(text)]));\n });\n}\n\n/**\n * Returns a copy of a PNG with one text chunk per field inserted before the first IDAT chunk. Other chunks are\n * unchanged. The engine's own `output.metadata` path calls this function, so for the same PNG and text the bytes\n * are the same.\n *\n * Root export (#388). `metadata` is checked as `output.metadata` is (see the module notes); a file without the PNG\n * signature, or without an IDAT chunk inside its bytes, fails with `InvalidConfigurationError`. Chunk CRCs before\n * IDAT are not checked. The input is not changed.\n * @param png A PNG file, for example `serializePngImage`'s bytes.\n * @param metadata The text: at least one of description, software, copyright and comment.\n * @returns A new array holding the PNG with the text.\n */\nexport function addPngTextMetadata(png: Uint8Array, metadata: Readonly<OutputTextMetadata>): Uint8Array {\n if (!(png instanceof Uint8Array)) throw new InvalidConfigurationError(\"addPngTextMetadata png must be a Uint8Array.\");\n const text = requireTextMetadata(metadata, \"addPngTextMetadata\");\n const signature = [137, 80, 78, 71, 13, 10, 26, 10];\n if (png.length < 8 || signature.some((b, i) => png[i] !== b)) throw new InvalidConfigurationError(\"PNG text metadata needs a PNG signature.\");\n const view = new DataView(png.buffer, png.byteOffset, png.byteLength);\n for (let offset = 8; offset + 12 <= png.length;) {\n const length = view.getUint32(offset), type = String.fromCharCode(...png.subarray(offset + 4, offset + 8));\n if (type === \"IDAT\") return concat([png.subarray(0, offset), ...pngTextMetadataChunks(text), png.subarray(offset)]);\n offset += 12 + length;\n }\n throw new InvalidConfigurationError(\"PNG text metadata found no IDAT chunk.\");\n}\n"]}
|
|
@@ -30,7 +30,7 @@ import { InvalidScientificInputError } from "../core/validation.js";
|
|
|
30
30
|
import { createDeterministicUniformStream, deriveSensorFixedPatternSeedUint32, sampleDeterministicStandardNormal, SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID, SENSOR_FIXED_PATTERN_DSNU_STREAM_ID, SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID, SENSOR_FIXED_PATTERN_PRNU_STREAM_ID, SENSOR_FIXED_PATTERN_ROW_STREAM_ID } from "./stochastic-seed.js";
|
|
31
31
|
export const SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION = "0.1.0";
|
|
32
32
|
/** Versioned identity of the pattern-generation equations and seed schedule. */
|
|
33
|
-
export const SENSOR_FIXED_PATTERN_NOISE_MODEL = Object.freeze({ id: "photivra-sensor-fixed-pattern", version: "1.0.0" });
|
|
33
|
+
export const SENSOR_FIXED_PATTERN_NOISE_MODEL = /* @__PURE__ */ Object.freeze({ id: "photivra-sensor-fixed-pattern", version: "1.0.0" });
|
|
34
34
|
const FACT_KEYS = ["value", "evidence"];
|
|
35
35
|
function fact(value, path, maximum, minimum = 0) {
|
|
36
36
|
const record = requireAllowlistedRecord(value, FACT_KEYS, path + " must be an evidence-backed fact.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixed-pattern-noise.js","sourceRoot":"","sources":["../../src/sensor/fixed-pattern-noise.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Fixed-pattern noise (FPN): the part of sensor noise that stays the same from shot to shot for one\n * physical sensor. It covers photo-response non-uniformity (PRNU), dark-signal non-uniformity (DSNU),\n * hot pixels and row and column readout offsets, as defined in EMVA Standard 1288 Release 4.0 (spatial\n * non-uniformity of the photo response and of the dark signal).\n *\n * The pattern is deterministic. It is seeded from an explicit sensor-instance seed and absolute\n * colour-sampling site coordinates, never from the per-capture noise seed, so the same sensor shows\n * the same pattern in every capture.\n *\n * Application points in the readout chain (see raw-readout.ts):\n * - PRNU multiplies the expected photo charge before photo shot-noise sampling;\n * - DSNU and hot pixels multiply the population-mean dark-current expectation before dark shot-noise\n * sampling, so their excess charge scales with exposure duration and with the declared\n * temperature model of the dark-current profile;\n * - row and column offsets are input-referred electron offsets added with read noise, before the\n * pre-ADC threshold.\n *\n * The dark-current model itself stays a population mean; this module is a separate spatial model that\n * must be attached explicitly. Values are declared by the caller (or are synthetic preset values).\n * Nothing here is a measured calibration.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { parseEvidenceList, type EvidenceBackedFact, type EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport { requireAllowlistedRecord, requirePublicOpaqueId, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n createDeterministicUniformStream,\n deriveSensorFixedPatternSeedUint32,\n sampleDeterministicStandardNormal,\n SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID,\n SENSOR_FIXED_PATTERN_DSNU_STREAM_ID,\n SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID,\n SENSOR_FIXED_PATTERN_PRNU_STREAM_ID,\n SENSOR_FIXED_PATTERN_ROW_STREAM_ID\n} from \"./stochastic-seed.js\";\n\nexport const SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION = \"0.1.0\" as const;\n/** Versioned identity of the pattern-generation equations and seed schedule. */\nexport const SENSOR_FIXED_PATTERN_NOISE_MODEL = Object.freeze({ id: \"photivra-sensor-fixed-pattern\", version: \"1.0.0\" } as const);\n\n/**\n * Explicit fixed-pattern parameters for one sensor instance. Every component is required; a zero\n * standard deviation, fraction or RMS disables that component.\n */\nexport interface SensorFixedPatternNoiseProfile {\n schemaVersion: typeof SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION;\n profileId: string;\n /** The colour-sampling grid whose absolute site coordinates index the pattern. */\n colorSamplingProfileId: string;\n scientificStatus: \"approximation\";\n /** Identifies the physical sensor. Independent of every per-capture noise seed. */\n sensorInstanceSeedUint32: number;\n /** EMVA 1288 PRNU: spatial standard deviation of the photo response divided by its mean. */\n photoResponseNonUniformity: { relativeStandardDeviation: EvidenceBackedFact<number> };\n /** Spatial standard deviation of the dark-current rate divided by the population-mean rate. */\n darkSignalNonUniformity: { relativeStandardDeviation: EvidenceBackedFact<number> };\n /** Fraction of sites whose dark-current rate is the population mean times the multiplier. */\n hotPixels: { siteFraction: EvidenceBackedFact<number>; darkCurrentMultiplier: EvidenceBackedFact<number> };\n /** Fixed readout offset per row, in input-referred electrons, normally distributed with zero mean. */\n rowOffset: { rmsElectrons: EvidenceBackedFact<number> };\n /** Fixed readout offset per column, in input-referred electrons, normally distributed with zero mean. */\n columnOffset: { rmsElectrons: EvidenceBackedFact<number> };\n patternDistribution: \"normal-factors-truncated-at-zero\";\n evidence: readonly EvidenceProvenance[];\n limitations: readonly string[];\n}\n\n/** The deterministic pattern values of one colour-sampling site. */\nexport interface SensorFixedPatternSiteValues {\n profileId: string;\n modelId: typeof SENSOR_FIXED_PATTERN_NOISE_MODEL.id;\n modelVersion: typeof SENSOR_FIXED_PATTERN_NOISE_MODEL.version;\n site: { x: number; y: number };\n /** Multiplies the expected photo charge (1 = no PRNU); never negative. */\n photoResponseGainFactor: number;\n /** Multiplies the population-mean dark expectation (1 = no DSNU); never negative. */\n darkCurrentFactor: number;\n hotPixel: boolean;\n rowOffsetElectrons: number;\n columnOffsetElectrons: number;\n /** rowOffsetElectrons + columnOffsetElectrons, added at readout. */\n readoutOffsetElectrons: number;\n}\n\nconst FACT_KEYS = [\"value\", \"evidence\"];\n\nfunction fact(value: unknown, path: string, maximum: number, minimum = 0): EvidenceBackedFact<number> {\n const record = requireAllowlistedRecord(value, FACT_KEYS, path + \" must be an evidence-backed fact.\");\n const v = record.value;\n if (typeof v !== \"number\" || !Number.isFinite(v) || v < minimum || v > maximum) {\n throw new InvalidConfigurationError(path + \".value must be finite from \" + minimum + \" through \" + maximum + \".\");\n }\n return { value: v, evidence: parseEvidenceList(record.evidence, path + \".evidence\") };\n}\n\nfunction limitations(value: unknown): readonly string[] {\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== \"string\" || entry.trim().length === 0)) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.limitations must be an array of non-empty strings.\");\n }\n if (new Set(value).size !== value.length) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.limitations must not contain duplicates.\");\n }\n return value.map((entry: string) => entry.trim());\n}\n\n/**\n * Validates an untrusted fixed-pattern noise profile. Unknown keys, out-of-range parameters and\n * missing evidence are rejected. The PRNU and DSNU relative standard deviations are limited to 0.5\n * and 5, and the hot-pixel multiplier to 1–1e6.\n * @param value - Untrusted data. TypeScript types alone are not validation.\n * @returns The validated profile.\n * @see docs/MOTION_AND_SIGNAL.md for the model and its sources.\n */\nexport function parseSensorFixedPatternNoiseProfile(value: unknown): SensorFixedPatternNoiseProfile {\n const r = requireAllowlistedRecord(value, [\"schemaVersion\", \"profileId\", \"colorSamplingProfileId\", \"scientificStatus\",\n \"sensorInstanceSeedUint32\", \"photoResponseNonUniformity\", \"darkSignalNonUniformity\", \"hotPixels\", \"rowOffset\", \"columnOffset\",\n \"patternDistribution\", \"evidence\", \"limitations\"], \"Invalid sensor fixed-pattern noise profile fields.\");\n if (r.schemaVersion !== SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION) {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.schemaVersion must be \"' + SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION + '\".');\n }\n if (r.scientificStatus !== \"approximation\") {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.scientificStatus must be \"approximation\".');\n }\n if (r.patternDistribution !== \"normal-factors-truncated-at-zero\") {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.patternDistribution must be \"normal-factors-truncated-at-zero\".');\n }\n const seed = r.sensorInstanceSeedUint32;\n if (typeof seed !== \"number\" || !Number.isSafeInteger(seed) || seed < 0 || seed > 0xffff_ffff) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.sensorInstanceSeedUint32 must be an unsigned 32-bit integer.\");\n }\n const prnu = requireAllowlistedRecord(r.photoResponseNonUniformity, [\"relativeStandardDeviation\"], \"Invalid PRNU fields.\");\n const dsnu = requireAllowlistedRecord(r.darkSignalNonUniformity, [\"relativeStandardDeviation\"], \"Invalid DSNU fields.\");\n const hot = requireAllowlistedRecord(r.hotPixels, [\"siteFraction\", \"darkCurrentMultiplier\"], \"Invalid hot-pixel fields.\");\n const row = requireAllowlistedRecord(r.rowOffset, [\"rmsElectrons\"], \"Invalid row-offset fields.\");\n const column = requireAllowlistedRecord(r.columnOffset, [\"rmsElectrons\"], \"Invalid column-offset fields.\");\n return {\n schemaVersion: SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION,\n profileId: requirePublicOpaqueId(r.profileId, \"sensorFixedPatternNoise.profileId must be a public opaque identifier.\"),\n colorSamplingProfileId: requirePublicOpaqueId(r.colorSamplingProfileId, \"sensorFixedPatternNoise.colorSamplingProfileId must be a public opaque identifier.\"),\n scientificStatus: \"approximation\",\n sensorInstanceSeedUint32: seed,\n photoResponseNonUniformity: { relativeStandardDeviation: fact(prnu.relativeStandardDeviation, \"photoResponseNonUniformity.relativeStandardDeviation\", 0.5) },\n darkSignalNonUniformity: { relativeStandardDeviation: fact(dsnu.relativeStandardDeviation, \"darkSignalNonUniformity.relativeStandardDeviation\", 5) },\n hotPixels: { siteFraction: fact(hot.siteFraction, \"hotPixels.siteFraction\", 1),\n darkCurrentMultiplier: fact(hot.darkCurrentMultiplier, \"hotPixels.darkCurrentMultiplier\", 1e6, 1) },\n rowOffset: { rmsElectrons: fact(row.rmsElectrons, \"rowOffset.rmsElectrons\", 1e6) },\n columnOffset: { rmsElectrons: fact(column.rmsElectrons, \"columnOffset.rmsElectrons\", 1e6) },\n patternDistribution: \"normal-factors-truncated-at-zero\",\n evidence: parseEvidenceList(r.evidence, \"sensorFixedPatternNoise.evidence\"),\n limitations: limitations(r.limitations)\n };\n}\n\nfunction normal(seed: number, x: number, y: number, stream: number): number {\n return sampleDeterministicStandardNormal(createDeterministicUniformStream(deriveSensorFixedPatternSeedUint32(seed, x, y, stream)));\n}\n\n/**\n * Computes the fixed-pattern values of one absolute colour-sampling site.\n *\n * - PRNU gain g = max(0, 1 + σ_PRNU·z₁);\n * - hot pixel when u < f_hot, then dark factor d = m_hot; otherwise d = max(0, 1 + σ_DSNU·z₂);\n * - row offset σ_row·z(row y), column offset σ_col·z(column x), in input-referred electrons.\n *\n * Each z is a standard normal value and u a uniform value, each from its own stream hashed with\n * MurmurHash3 from (sensorInstanceSeed, x, y, stream). Row values use x = 0 and column values y = 0,\n * so a whole row or column shares one value. Cutting negative factors off at zero shifts the mean\n * factor by less than 1e-6 for σ ≤ 0.2.\n * @param input - A parsed or untrusted profile plus a nonnegative integer site.\n * @returns The site's fixed-pattern values.\n */\nexport function calculateSensorFixedPatternSite(input: {\n profile: SensorFixedPatternNoiseProfile; site: { x: number; y: number };\n}): SensorFixedPatternSiteValues {\n requireInputKeys(input, \"calculateSensorFixedPatternSite\", [\"profile\", \"site\"]);\n const profile = parseSensorFixedPatternNoiseProfile(input.profile);\n return evaluateParsedFixedPatternSite(profile, input.site);\n}\n\n/** Internal: the PRNU gain, dark-current factor and hot-pixel state of one site (no readout offsets). */\nexport function evaluateFixedPatternSiteFactors(profile: SensorFixedPatternNoiseProfile, x: number, y: number):\n { photoResponseGainFactor: number; darkCurrentFactor: number; hotPixel: boolean } {\n const seed = profile.sensorInstanceSeedUint32;\n const prnuSd = profile.photoResponseNonUniformity.relativeStandardDeviation.value;\n const dsnuSd = profile.darkSignalNonUniformity.relativeStandardDeviation.value;\n const hotFraction = profile.hotPixels.siteFraction.value;\n const photoResponseGainFactor = prnuSd === 0 ? 1 : Math.max(0, 1 + prnuSd * normal(seed, x, y, SENSOR_FIXED_PATTERN_PRNU_STREAM_ID));\n const hotPixel = hotFraction > 0 &&\n createDeterministicUniformStream(deriveSensorFixedPatternSeedUint32(seed, x, y, SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID)).next() < hotFraction;\n const darkCurrentFactor = hotPixel ? profile.hotPixels.darkCurrentMultiplier.value :\n dsnuSd === 0 ? 1 : Math.max(0, 1 + dsnuSd * normal(seed, x, y, SENSOR_FIXED_PATTERN_DSNU_STREAM_ID));\n return { photoResponseGainFactor, darkCurrentFactor, hotPixel };\n}\n\n/** Internal: the fixed readout offset of row `y`, in input-referred electrons. */\nexport function evaluateFixedPatternRowOffset(profile: SensorFixedPatternNoiseProfile, y: number): number {\n const rms = profile.rowOffset.rmsElectrons.value;\n return rms === 0 ? 0 : rms * normal(profile.sensorInstanceSeedUint32, 0, y, SENSOR_FIXED_PATTERN_ROW_STREAM_ID);\n}\n\n/** Internal: the fixed readout offset of column `x`, in input-referred electrons. */\nexport function evaluateFixedPatternColumnOffset(profile: SensorFixedPatternNoiseProfile, x: number): number {\n const rms = profile.columnOffset.rmsElectrons.value;\n return rms === 0 ? 0 : rms * normal(profile.sensorInstanceSeedUint32, x, 0, SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID);\n}\n\n/** Internal: the same calculation for an already parsed profile. */\nexport function evaluateParsedFixedPatternSite(profile: SensorFixedPatternNoiseProfile,\n site: { x: number; y: number }): SensorFixedPatternSiteValues {\n const { x, y } = site ?? {};\n if (!Number.isSafeInteger(x) || !Number.isSafeInteger(y) || x < 0 || y < 0 || x > 0xffff_ffff || y > 0xffff_ffff) {\n throw new InvalidScientificInputError(\"Fixed-pattern site coordinates must be nonnegative uint32 integers.\");\n }\n const { photoResponseGainFactor, darkCurrentFactor, hotPixel } = evaluateFixedPatternSiteFactors(profile, x, y);\n const rowOffsetElectrons = evaluateFixedPatternRowOffset(profile, y);\n const columnOffsetElectrons = evaluateFixedPatternColumnOffset(profile, x);\n return {\n profileId: profile.profileId,\n modelId: SENSOR_FIXED_PATTERN_NOISE_MODEL.id,\n modelVersion: SENSOR_FIXED_PATTERN_NOISE_MODEL.version,\n site: { x, y },\n photoResponseGainFactor,\n darkCurrentFactor,\n hotPixel,\n rowOffsetElectrons,\n columnOffsetElectrons,\n readoutOffsetElectrons: rowOffsetElectrons + columnOffsetElectrons\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fixed-pattern-noise.js","sourceRoot":"","sources":["../../src/sensor/fixed-pattern-noise.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Fixed-pattern noise (FPN): the part of sensor noise that stays the same from shot to shot for one\n * physical sensor. It covers photo-response non-uniformity (PRNU), dark-signal non-uniformity (DSNU),\n * hot pixels and row and column readout offsets, as defined in EMVA Standard 1288 Release 4.0 (spatial\n * non-uniformity of the photo response and of the dark signal).\n *\n * The pattern is deterministic. It is seeded from an explicit sensor-instance seed and absolute\n * colour-sampling site coordinates, never from the per-capture noise seed, so the same sensor shows\n * the same pattern in every capture.\n *\n * Application points in the readout chain (see raw-readout.ts):\n * - PRNU multiplies the expected photo charge before photo shot-noise sampling;\n * - DSNU and hot pixels multiply the population-mean dark-current expectation before dark shot-noise\n * sampling, so their excess charge scales with exposure duration and with the declared\n * temperature model of the dark-current profile;\n * - row and column offsets are input-referred electron offsets added with read noise, before the\n * pre-ADC threshold.\n *\n * The dark-current model itself stays a population mean; this module is a separate spatial model that\n * must be attached explicitly. Values are declared by the caller (or are synthetic preset values).\n * Nothing here is a measured calibration.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { parseEvidenceList, type EvidenceBackedFact, type EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport { requireAllowlistedRecord, requirePublicOpaqueId, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport {\n createDeterministicUniformStream,\n deriveSensorFixedPatternSeedUint32,\n sampleDeterministicStandardNormal,\n SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID,\n SENSOR_FIXED_PATTERN_DSNU_STREAM_ID,\n SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID,\n SENSOR_FIXED_PATTERN_PRNU_STREAM_ID,\n SENSOR_FIXED_PATTERN_ROW_STREAM_ID\n} from \"./stochastic-seed.js\";\n\nexport const SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION = \"0.1.0\" as const;\n/** Versioned identity of the pattern-generation equations and seed schedule. */\nexport const SENSOR_FIXED_PATTERN_NOISE_MODEL = /* @__PURE__ */ Object.freeze({ id: \"photivra-sensor-fixed-pattern\", version: \"1.0.0\" } as const);\n\n/**\n * Explicit fixed-pattern parameters for one sensor instance. Every component is required; a zero\n * standard deviation, fraction or RMS disables that component.\n */\nexport interface SensorFixedPatternNoiseProfile {\n schemaVersion: typeof SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION;\n profileId: string;\n /** The colour-sampling grid whose absolute site coordinates index the pattern. */\n colorSamplingProfileId: string;\n scientificStatus: \"approximation\";\n /** Identifies the physical sensor. Independent of every per-capture noise seed. */\n sensorInstanceSeedUint32: number;\n /** EMVA 1288 PRNU: spatial standard deviation of the photo response divided by its mean. */\n photoResponseNonUniformity: { relativeStandardDeviation: EvidenceBackedFact<number> };\n /** Spatial standard deviation of the dark-current rate divided by the population-mean rate. */\n darkSignalNonUniformity: { relativeStandardDeviation: EvidenceBackedFact<number> };\n /** Fraction of sites whose dark-current rate is the population mean times the multiplier. */\n hotPixels: { siteFraction: EvidenceBackedFact<number>; darkCurrentMultiplier: EvidenceBackedFact<number> };\n /** Fixed readout offset per row, in input-referred electrons, normally distributed with zero mean. */\n rowOffset: { rmsElectrons: EvidenceBackedFact<number> };\n /** Fixed readout offset per column, in input-referred electrons, normally distributed with zero mean. */\n columnOffset: { rmsElectrons: EvidenceBackedFact<number> };\n patternDistribution: \"normal-factors-truncated-at-zero\";\n evidence: readonly EvidenceProvenance[];\n limitations: readonly string[];\n}\n\n/** The deterministic pattern values of one colour-sampling site. */\nexport interface SensorFixedPatternSiteValues {\n profileId: string;\n modelId: typeof SENSOR_FIXED_PATTERN_NOISE_MODEL.id;\n modelVersion: typeof SENSOR_FIXED_PATTERN_NOISE_MODEL.version;\n site: { x: number; y: number };\n /** Multiplies the expected photo charge (1 = no PRNU); never negative. */\n photoResponseGainFactor: number;\n /** Multiplies the population-mean dark expectation (1 = no DSNU); never negative. */\n darkCurrentFactor: number;\n hotPixel: boolean;\n rowOffsetElectrons: number;\n columnOffsetElectrons: number;\n /** rowOffsetElectrons + columnOffsetElectrons, added at readout. */\n readoutOffsetElectrons: number;\n}\n\nconst FACT_KEYS = [\"value\", \"evidence\"];\n\nfunction fact(value: unknown, path: string, maximum: number, minimum = 0): EvidenceBackedFact<number> {\n const record = requireAllowlistedRecord(value, FACT_KEYS, path + \" must be an evidence-backed fact.\");\n const v = record.value;\n if (typeof v !== \"number\" || !Number.isFinite(v) || v < minimum || v > maximum) {\n throw new InvalidConfigurationError(path + \".value must be finite from \" + minimum + \" through \" + maximum + \".\");\n }\n return { value: v, evidence: parseEvidenceList(record.evidence, path + \".evidence\") };\n}\n\nfunction limitations(value: unknown): readonly string[] {\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== \"string\" || entry.trim().length === 0)) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.limitations must be an array of non-empty strings.\");\n }\n if (new Set(value).size !== value.length) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.limitations must not contain duplicates.\");\n }\n return value.map((entry: string) => entry.trim());\n}\n\n/**\n * Validates an untrusted fixed-pattern noise profile. Unknown keys, out-of-range parameters and\n * missing evidence are rejected. The PRNU and DSNU relative standard deviations are limited to 0.5\n * and 5, and the hot-pixel multiplier to 1–1e6.\n * @param value - Untrusted data. TypeScript types alone are not validation.\n * @returns The validated profile.\n * @see docs/MOTION_AND_SIGNAL.md for the model and its sources.\n */\nexport function parseSensorFixedPatternNoiseProfile(value: unknown): SensorFixedPatternNoiseProfile {\n const r = requireAllowlistedRecord(value, [\"schemaVersion\", \"profileId\", \"colorSamplingProfileId\", \"scientificStatus\",\n \"sensorInstanceSeedUint32\", \"photoResponseNonUniformity\", \"darkSignalNonUniformity\", \"hotPixels\", \"rowOffset\", \"columnOffset\",\n \"patternDistribution\", \"evidence\", \"limitations\"], \"Invalid sensor fixed-pattern noise profile fields.\");\n if (r.schemaVersion !== SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION) {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.schemaVersion must be \"' + SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION + '\".');\n }\n if (r.scientificStatus !== \"approximation\") {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.scientificStatus must be \"approximation\".');\n }\n if (r.patternDistribution !== \"normal-factors-truncated-at-zero\") {\n throw new InvalidConfigurationError('sensorFixedPatternNoise.patternDistribution must be \"normal-factors-truncated-at-zero\".');\n }\n const seed = r.sensorInstanceSeedUint32;\n if (typeof seed !== \"number\" || !Number.isSafeInteger(seed) || seed < 0 || seed > 0xffff_ffff) {\n throw new InvalidConfigurationError(\"sensorFixedPatternNoise.sensorInstanceSeedUint32 must be an unsigned 32-bit integer.\");\n }\n const prnu = requireAllowlistedRecord(r.photoResponseNonUniformity, [\"relativeStandardDeviation\"], \"Invalid PRNU fields.\");\n const dsnu = requireAllowlistedRecord(r.darkSignalNonUniformity, [\"relativeStandardDeviation\"], \"Invalid DSNU fields.\");\n const hot = requireAllowlistedRecord(r.hotPixels, [\"siteFraction\", \"darkCurrentMultiplier\"], \"Invalid hot-pixel fields.\");\n const row = requireAllowlistedRecord(r.rowOffset, [\"rmsElectrons\"], \"Invalid row-offset fields.\");\n const column = requireAllowlistedRecord(r.columnOffset, [\"rmsElectrons\"], \"Invalid column-offset fields.\");\n return {\n schemaVersion: SENSOR_FIXED_PATTERN_NOISE_PROFILE_SCHEMA_VERSION,\n profileId: requirePublicOpaqueId(r.profileId, \"sensorFixedPatternNoise.profileId must be a public opaque identifier.\"),\n colorSamplingProfileId: requirePublicOpaqueId(r.colorSamplingProfileId, \"sensorFixedPatternNoise.colorSamplingProfileId must be a public opaque identifier.\"),\n scientificStatus: \"approximation\",\n sensorInstanceSeedUint32: seed,\n photoResponseNonUniformity: { relativeStandardDeviation: fact(prnu.relativeStandardDeviation, \"photoResponseNonUniformity.relativeStandardDeviation\", 0.5) },\n darkSignalNonUniformity: { relativeStandardDeviation: fact(dsnu.relativeStandardDeviation, \"darkSignalNonUniformity.relativeStandardDeviation\", 5) },\n hotPixels: { siteFraction: fact(hot.siteFraction, \"hotPixels.siteFraction\", 1),\n darkCurrentMultiplier: fact(hot.darkCurrentMultiplier, \"hotPixels.darkCurrentMultiplier\", 1e6, 1) },\n rowOffset: { rmsElectrons: fact(row.rmsElectrons, \"rowOffset.rmsElectrons\", 1e6) },\n columnOffset: { rmsElectrons: fact(column.rmsElectrons, \"columnOffset.rmsElectrons\", 1e6) },\n patternDistribution: \"normal-factors-truncated-at-zero\",\n evidence: parseEvidenceList(r.evidence, \"sensorFixedPatternNoise.evidence\"),\n limitations: limitations(r.limitations)\n };\n}\n\nfunction normal(seed: number, x: number, y: number, stream: number): number {\n return sampleDeterministicStandardNormal(createDeterministicUniformStream(deriveSensorFixedPatternSeedUint32(seed, x, y, stream)));\n}\n\n/**\n * Computes the fixed-pattern values of one absolute colour-sampling site.\n *\n * - PRNU gain g = max(0, 1 + σ_PRNU·z₁);\n * - hot pixel when u < f_hot, then dark factor d = m_hot; otherwise d = max(0, 1 + σ_DSNU·z₂);\n * - row offset σ_row·z(row y), column offset σ_col·z(column x), in input-referred electrons.\n *\n * Each z is a standard normal value and u a uniform value, each from its own stream hashed with\n * MurmurHash3 from (sensorInstanceSeed, x, y, stream). Row values use x = 0 and column values y = 0,\n * so a whole row or column shares one value. Cutting negative factors off at zero shifts the mean\n * factor by less than 1e-6 for σ ≤ 0.2.\n * @param input - A parsed or untrusted profile plus a nonnegative integer site.\n * @returns The site's fixed-pattern values.\n */\nexport function calculateSensorFixedPatternSite(input: {\n profile: SensorFixedPatternNoiseProfile; site: { x: number; y: number };\n}): SensorFixedPatternSiteValues {\n requireInputKeys(input, \"calculateSensorFixedPatternSite\", [\"profile\", \"site\"]);\n const profile = parseSensorFixedPatternNoiseProfile(input.profile);\n return evaluateParsedFixedPatternSite(profile, input.site);\n}\n\n/** Internal: the PRNU gain, dark-current factor and hot-pixel state of one site (no readout offsets). */\nexport function evaluateFixedPatternSiteFactors(profile: SensorFixedPatternNoiseProfile, x: number, y: number):\n { photoResponseGainFactor: number; darkCurrentFactor: number; hotPixel: boolean } {\n const seed = profile.sensorInstanceSeedUint32;\n const prnuSd = profile.photoResponseNonUniformity.relativeStandardDeviation.value;\n const dsnuSd = profile.darkSignalNonUniformity.relativeStandardDeviation.value;\n const hotFraction = profile.hotPixels.siteFraction.value;\n const photoResponseGainFactor = prnuSd === 0 ? 1 : Math.max(0, 1 + prnuSd * normal(seed, x, y, SENSOR_FIXED_PATTERN_PRNU_STREAM_ID));\n const hotPixel = hotFraction > 0 &&\n createDeterministicUniformStream(deriveSensorFixedPatternSeedUint32(seed, x, y, SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID)).next() < hotFraction;\n const darkCurrentFactor = hotPixel ? profile.hotPixels.darkCurrentMultiplier.value :\n dsnuSd === 0 ? 1 : Math.max(0, 1 + dsnuSd * normal(seed, x, y, SENSOR_FIXED_PATTERN_DSNU_STREAM_ID));\n return { photoResponseGainFactor, darkCurrentFactor, hotPixel };\n}\n\n/** Internal: the fixed readout offset of row `y`, in input-referred electrons. */\nexport function evaluateFixedPatternRowOffset(profile: SensorFixedPatternNoiseProfile, y: number): number {\n const rms = profile.rowOffset.rmsElectrons.value;\n return rms === 0 ? 0 : rms * normal(profile.sensorInstanceSeedUint32, 0, y, SENSOR_FIXED_PATTERN_ROW_STREAM_ID);\n}\n\n/** Internal: the fixed readout offset of column `x`, in input-referred electrons. */\nexport function evaluateFixedPatternColumnOffset(profile: SensorFixedPatternNoiseProfile, x: number): number {\n const rms = profile.columnOffset.rmsElectrons.value;\n return rms === 0 ? 0 : rms * normal(profile.sensorInstanceSeedUint32, x, 0, SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID);\n}\n\n/** Internal: the same calculation for an already parsed profile. */\nexport function evaluateParsedFixedPatternSite(profile: SensorFixedPatternNoiseProfile,\n site: { x: number; y: number }): SensorFixedPatternSiteValues {\n const { x, y } = site ?? {};\n if (!Number.isSafeInteger(x) || !Number.isSafeInteger(y) || x < 0 || y < 0 || x > 0xffff_ffff || y > 0xffff_ffff) {\n throw new InvalidScientificInputError(\"Fixed-pattern site coordinates must be nonnegative uint32 integers.\");\n }\n const { photoResponseGainFactor, darkCurrentFactor, hotPixel } = evaluateFixedPatternSiteFactors(profile, x, y);\n const rowOffsetElectrons = evaluateFixedPatternRowOffset(profile, y);\n const columnOffsetElectrons = evaluateFixedPatternColumnOffset(profile, x);\n return {\n profileId: profile.profileId,\n modelId: SENSOR_FIXED_PATTERN_NOISE_MODEL.id,\n modelVersion: SENSOR_FIXED_PATTERN_NOISE_MODEL.version,\n site: { x, y },\n photoResponseGainFactor,\n darkCurrentFactor,\n hotPixel,\n rowOffsetElectrons,\n columnOffsetElectrons,\n readoutOffsetElectrons: rowOffsetElectrons + columnOffsetElectrons\n };\n}\n"]}
|
|
@@ -223,7 +223,7 @@ export function assessReadoutExposureTimingLinkage(input) {
|
|
|
223
223
|
]);
|
|
224
224
|
}
|
|
225
225
|
/** Model identity of the electronic rolling-shutter (line reset → line read) exposure schedule. */
|
|
226
|
-
export const ELECTRONIC_ROLLING_SHUTTER_MODEL = Object.freeze({
|
|
226
|
+
export const ELECTRONIC_ROLLING_SHUTTER_MODEL = /* @__PURE__ */ Object.freeze({
|
|
227
227
|
id: "electronic-rolling-shutter-line-reset-read",
|
|
228
228
|
version: "1.0.0"
|
|
229
229
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readout-exposure-linkage.js","sourceRoot":"","sources":["../../src/sensor/readout-exposure-linkage.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Links rolling sensor readout to electronic shutter scans, and derives the exposure schedule of a\n * fully electronic rolling shutter.\n *\n * A declared link says that the readout scan and one or more exposure-boundary scans move across the\n * sensor in the same order (or in reverse). That is deliberately weaker than time synchronization: it\n * does not say when readout happens relative to the start or end of exposure. The total data-readout\n * time is kept for diagnostics but never used to check the link. The readout skew and the boundary\n * sweep times may differ; their ratio is reported without implying a shared clock.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationProvenance,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n parseEvidenceList,\n type EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport type { RasterPoint, RasterRect } from \"../output/capture-geometry.js\";\nimport {\n calculateCaptureExposureWindows,\n parseBoundarySchedule,\n parseSecondsFact,\n type CalculateCaptureExposureWindowsInput,\n type ExposureBoundarySchedule,\n type UniformLinearExposureBoundarySchedule,\n type SourcedCaptureTimingSeconds\n} from \"./exposure-window.js\";\nimport {\n calculateSensorReadoutTiming,\n type CaptureShutterMechanism,\n type NativeSensorReadoutScanDirection,\n type RollingSensorReadoutTimingDeclaration,\n type SensorReadoutTimingDeclaration,\n type SourcedSensorTimingSeconds\n} from \"./readout-timing.js\";\nimport type { NativeImageRaster } from \"./sensor-geometry.js\";\nimport { requireInputKeys, rejectUnknownFields } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport type ReadoutExposureBoundaryId = \"opening\" | \"closing\";\n\nexport type ReadoutExposureSpatialPhaseOrientation =\n | \"same\"\n | \"reversed\";\n\nexport interface ReadoutExposureBoundarySpatialLink {\n boundary: ReadoutExposureBoundaryId;\n /**\n * How the normalized position of the rolling readout scan relates to that\n * of the selected electronic exposure-boundary scan.\n *\n * \"same\": boundary normalized phase = readout normalized phase.\n * \"reversed\": boundary normalized phase = 1 - readout normalized phase.\n *\n * This does not establish absolute temporal synchronization.\n */\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation;\n /**\n * Evidence for the link itself.\n *\n * The readout direction and timing, and the exposure-boundary direction and\n * timing, keep their own separate evidence.\n */\n evidence: readonly EvidenceProvenance[];\n}\n\nexport type ReadoutExposureTimingLinkageDeclaration =\n | {\n /**\n * No link between readout and exposure is stated for this capture.\n * This is not evidence that the physical processes are independent.\n */\n kind: \"unlinked\";\n links?: never;\n }\n | {\n /**\n * One or more exposure boundaries are explicitly linked to the rolling\n * readout by their normalized position across the native sensor.\n */\n kind: \"spatial-phase-linked\";\n links: readonly ReadoutExposureBoundarySpatialLink[];\n };\n\nexport interface AssessReadoutExposureTimingLinkageInput {\n nativeRaster: NativeImageRaster;\n activeCaptureRect?: RasterRect;\n shutterMechanism: CaptureShutterMechanism;\n readout: SensorReadoutTimingDeclaration;\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n opening: ExposureBoundarySchedule;\n closing: ExposureBoundarySchedule;\n linkage: ReadoutExposureTimingLinkageDeclaration;\n samplePointsNative?: readonly RasterPoint[];\n}\n\nexport interface ReadoutExposureBoundarySpatialLinkAssessment {\n boundary: ReadoutExposureBoundaryId;\n boundaryActuator: \"electronic\";\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation;\n normalizedPhaseRelationship:\n | \"boundary-phase-equals-readout-phase\"\n | \"boundary-phase-equals-one-minus-readout-phase\";\n relationshipEvidence: readonly EvidenceProvenance[];\n readoutDirectionNative: NativeSensorReadoutScanDirection;\n boundaryDirectionNative: NativeSensorReadoutScanDirection;\n /**\n * Rolling readout skew: time from the first to the last scan position.\n */\n readoutSpatialSamplingSkewSeconds: SourcedSensorTimingSeconds;\n /**\n * Time for the exposure boundary to sweep from first to last position.\n */\n boundaryTraversalDurationSeconds: SourcedCaptureTimingSeconds;\n /**\n * A descriptive ratio only. A value of 1 does not prove time\n * synchronization or a common clock origin.\n */\n boundaryTraversalToReadoutSpatialSkewRatio: number;\n absoluteTemporalAlignment: \"not-established\";\n}\n\nexport interface ReadoutExposureTimingLinkageAssessment {\n linkageKind: ReadoutExposureTimingLinkageDeclaration[\"kind\"];\n relationshipMeaning:\n | \"no-readout-exposure-relationship-asserted\"\n | \"normalized-spatial-phase-relationship-asserted\";\n readoutMode: \"rolling\" | \"global\";\n shutterMechanism: CaptureShutterMechanism;\n activeCaptureRect: RasterRect;\n absoluteTemporalAlignment: \"not-established\";\n /**\n * Data-readout time for the capture. Kept for diagnostics, but never used to\n * check an exposure-boundary link.\n */\n captureReadoutDurationSeconds: SourcedSensorTimingSeconds;\n links: readonly ReadoutExposureBoundarySpatialLinkAssessment[];\n componentProvenance: {\n sensorReadout: CalculationProvenance;\n exposureWindows: CalculationProvenance;\n };\n}\n\ntype UnknownRecord = Record<string, unknown>;\n\nconst BOUNDARIES = new Set<ReadoutExposureBoundaryId>([\n \"opening\",\n \"closing\"\n]);\n\nconst PHASE_ORIENTATIONS =\n new Set<ReadoutExposureSpatialPhaseOrientation>([\n \"same\",\n \"reversed\"\n ]);\n\nfunction requireRecord(\n value: unknown,\n path: string,\n allowedKeys: readonly string[] | null\n): UnknownRecord {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(path + \" must be an object.\");\n }\n if (allowedKeys !== null) rejectUnknownFields(value, allowedKeys, path);\n return value as UnknownRecord;\n}\n\nfunction parseSpatialLink(\n value: unknown,\n path: string\n): ReadoutExposureBoundarySpatialLink {\n const record = requireRecord(value, path, [\"boundary\", \"phaseOrientation\", \"evidence\"]);\n\n if (\n typeof record.boundary !== \"string\" ||\n !BOUNDARIES.has(record.boundary as ReadoutExposureBoundaryId)\n ) {\n throw new InvalidConfigurationError(path + \".boundary is invalid.\");\n }\n\n if (\n typeof record.phaseOrientation !== \"string\" ||\n !PHASE_ORIENTATIONS.has(\n record.phaseOrientation as ReadoutExposureSpatialPhaseOrientation\n )\n ) {\n throw new InvalidConfigurationError(\n path + \".phaseOrientation is invalid.\"\n );\n }\n\n return {\n boundary: record.boundary as ReadoutExposureBoundaryId,\n phaseOrientation:\n record.phaseOrientation as ReadoutExposureSpatialPhaseOrientation,\n evidence: parseEvidenceList(record.evidence, path + \".evidence\")\n };\n}\n\nfunction parseLinkageDeclaration(\n value: unknown\n): ReadoutExposureTimingLinkageDeclaration {\n const record = requireRecord(value, \"linkage\", [\"kind\", \"links\"]);\n\n if (record.kind === \"unlinked\") {\n if (record.links !== undefined) {\n throw new InvalidScientificInputError(\n \"linkage.links must be omitted when linkage.kind is unlinked.\"\n );\n }\n return { kind: \"unlinked\" };\n }\n\n if (record.kind !== \"spatial-phase-linked\") {\n throw new InvalidConfigurationError(\"linkage.kind is invalid.\");\n }\n\n if (!Array.isArray(record.links) || record.links.length === 0) {\n throw new InvalidConfigurationError(\n \"linkage.links must be a non-empty array for spatial-phase-linked linkage.\"\n );\n }\n\n const links = record.links.map((entry, index) =>\n parseSpatialLink(entry, \"linkage.links[\" + index + \"]\")\n );\n\n const seen = new Set<ReadoutExposureBoundaryId>();\n for (const link of links) {\n if (seen.has(link.boundary)) {\n throw new InvalidScientificInputError(\n \"linkage.links must not contain duplicate boundary links.\"\n );\n }\n seen.add(link.boundary);\n }\n\n return {\n kind: \"spatial-phase-linked\",\n links\n };\n}\n\nfunction oppositeDirection(\n direction: NativeSensorReadoutScanDirection\n): NativeSensorReadoutScanDirection {\n switch (direction) {\n case \"top-to-bottom\":\n return \"bottom-to-top\";\n case \"bottom-to-top\":\n return \"top-to-bottom\";\n case \"left-to-right\":\n return \"right-to-left\";\n case \"right-to-left\":\n return \"left-to-right\";\n }\n}\n\nfunction expectedBoundaryDirection(\n readoutDirection: NativeSensorReadoutScanDirection,\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation\n): NativeSensorReadoutScanDirection {\n return phaseOrientation === \"same\"\n ? readoutDirection\n : oppositeDirection(readoutDirection);\n}\n\n/**\n * Checks an explicitly declared link between the rolling readout scan and one\n * or more electronic exposure-boundary scans.\n *\n * This is deliberately weaker than time synchronization. It can show that two\n * schedules sweep the native sensor in the same order (or in reverse), but not\n * when readout happens relative to the start or end of exposure.\n *\n * The total data-readout time is kept for diagnostics and never used to check\n * the link. The readout skew and the boundary sweep time may differ; their\n * ratio is reported without implying a shared clock.\n */\nexport function assessReadoutExposureTimingLinkage(\n input: AssessReadoutExposureTimingLinkageInput\n): CalculationResult<ReadoutExposureTimingLinkageAssessment> {\n requireInputKeys(input, \"assessReadoutExposureTimingLinkage\", [\"nativeRaster\", \"activeCaptureRect\", \"shutterMechanism\", \"readout\", \"nominalExposureDurationSeconds\", \"opening\", \"closing\", \"linkage\", \"samplePointsNative\"]);\n const linkage = parseLinkageDeclaration(input.linkage);\n\n const sensorReadout = calculateSensorReadoutTiming({\n nativeRaster: input.nativeRaster,\n ...(input.activeCaptureRect === undefined\n ? {}\n : { activeCaptureRect: input.activeCaptureRect }),\n shutterMechanism: input.shutterMechanism,\n readout: input.readout,\n ...(input.samplePointsNative === undefined\n ? {}\n : { samplePointsNative: input.samplePointsNative })\n });\n\n const exposureWindows = calculateCaptureExposureWindows({\n nativeRaster: input.nativeRaster,\n ...(input.activeCaptureRect === undefined\n ? {}\n : { activeCaptureRect: input.activeCaptureRect }),\n shutterMechanism: input.shutterMechanism,\n nominalExposureDurationSeconds:\n input.nominalExposureDurationSeconds,\n opening: input.opening,\n closing: input.closing,\n ...(input.samplePointsNative === undefined\n ? {}\n : { samplePointsNative: input.samplePointsNative })\n });\n\n if (linkage.kind === \"unlinked\") {\n return approximationResult(\n {\n linkageKind: \"unlinked\",\n relationshipMeaning:\n \"no-readout-exposure-relationship-asserted\",\n readoutMode: sensorReadout.value.readoutMode,\n shutterMechanism: input.shutterMechanism,\n activeCaptureRect: {\n ...sensorReadout.value.activeCaptureRect\n },\n absoluteTemporalAlignment: \"not-established\",\n captureReadoutDurationSeconds:\n sensorReadout.value.captureReadoutDurationSeconds,\n links: [],\n componentProvenance: {\n sensorReadout: sensorReadout.provenance,\n exposureWindows: exposureWindows.provenance\n }\n },\n \"readout-exposure-spatial-linkage\",\n \"1.0.0\",\n [\n \"Unlinked means Photivra asserts no readout-to-exposure relationship for this capture; it does not prove the physical processes are independent.\",\n \"Sensor data-readout timing and exposure-window timing retain independent provenance and semantics.\",\n \"Absolute temporal alignment between data readout and exposure boundaries is not established.\"\n ]\n );\n }\n\n const readoutScan = sensorReadout.value.scan;\n if (readoutScan === null) {\n throw new InvalidScientificInputError(\n \"spatial-phase-linked linkage requires rolling sensor readout with a spatial scan.\"\n );\n }\n\n const assessments =\n linkage.links.map<ReadoutExposureBoundarySpatialLinkAssessment>(\n (link) => {\n const boundary =\n link.boundary === \"opening\"\n ? exposureWindows.value.opening\n : exposureWindows.value.closing;\n\n if (boundary.actuator !== \"electronic\") {\n throw new InvalidScientificInputError(\n \"A readout spatial-phase link may target only an electronic exposure boundary.\"\n );\n }\n\n if (\n boundary.schedule.kind !==\n \"uniform-linear-native-scan\"\n ) {\n throw new InvalidScientificInputError(\n \"A readout spatial-phase link requires the selected exposure boundary to use a uniform-linear native scan.\"\n );\n }\n\n const readoutDirection =\n readoutScan.directionNative.value;\n const boundaryDirection =\n boundary.schedule.directionNative.value;\n const expectedDirection = expectedBoundaryDirection(\n readoutDirection,\n link.phaseOrientation\n );\n\n if (boundaryDirection !== expectedDirection) {\n throw new InvalidScientificInputError(\n \"Declared readout/exposure spatial phase orientation contradicts the readout and exposure-boundary scan directions.\"\n );\n }\n\n const readoutSpatialSamplingSkewSeconds =\n readoutScan.spatialSamplingSkewSeconds;\n const boundaryTraversalDurationSeconds =\n boundary.schedule.traversalDurationSeconds;\n\n return {\n boundary: link.boundary,\n boundaryActuator: \"electronic\",\n phaseOrientation: link.phaseOrientation,\n normalizedPhaseRelationship:\n link.phaseOrientation === \"same\"\n ? \"boundary-phase-equals-readout-phase\"\n : \"boundary-phase-equals-one-minus-readout-phase\",\n relationshipEvidence: link.evidence,\n readoutDirectionNative: readoutDirection,\n boundaryDirectionNative: boundaryDirection,\n readoutSpatialSamplingSkewSeconds,\n boundaryTraversalDurationSeconds,\n boundaryTraversalToReadoutSpatialSkewRatio:\n boundaryTraversalDurationSeconds.value /\n readoutSpatialSamplingSkewSeconds.value,\n absoluteTemporalAlignment: \"not-established\"\n };\n }\n );\n\n return approximationResult(\n {\n linkageKind: \"spatial-phase-linked\",\n relationshipMeaning:\n \"normalized-spatial-phase-relationship-asserted\",\n readoutMode: sensorReadout.value.readoutMode,\n shutterMechanism: input.shutterMechanism,\n activeCaptureRect: {\n ...sensorReadout.value.activeCaptureRect\n },\n absoluteTemporalAlignment: \"not-established\",\n captureReadoutDurationSeconds:\n sensorReadout.value.captureReadoutDurationSeconds,\n links: assessments,\n componentProvenance: {\n sensorReadout: sensorReadout.provenance,\n exposureWindows: exposureWindows.provenance\n }\n },\n \"readout-exposure-spatial-linkage\",\n \"1.0.0\",\n [\n \"The linkage establishes normalized native-sensor spatial phase/order only, not absolute temporal synchronization.\",\n \"Every asserted boundary link carries evidence separate from the evidence for the underlying readout and exposure timing facts.\",\n \"Only electronic exposure boundaries may be linked to rolling sensor readout spatial phase.\",\n \"Same phase orientation requires matching native scan direction; reversed phase orientation requires the exact opposite direction on the same native axis.\",\n \"Rolling readout spatial skew and exposure-boundary traversal duration may differ; their ratio is descriptive and does not establish a common clock origin.\",\n \"Capture data-readout duration is not used to validate exposure-boundary timing linkage.\",\n \"Active capture, native coordinate semantics and shutter mechanism are shared because both component schedules are resolved from the same input.\",\n \"Sensor readout and exposure-window component provenance remain visible; linkage does not upgrade either component to calibrated status.\",\n \"No camera-specific timing relationship is inferred from shutter mechanism, sensor architecture metadata, output resolution or adjacent specifications.\"\n ]\n );\n}\n\n/** Model identity of the electronic rolling-shutter (line reset → line read) exposure schedule. */\nexport const ELECTRONIC_ROLLING_SHUTTER_MODEL = Object.freeze({\n id: \"electronic-rolling-shutter-line-reset-read\",\n version: \"1.0.0\"\n} as const);\n\nexport interface ResolveElectronicRollingShutterExposureScheduleInput {\n /** Rolling readout declaration whose scan direction and spatial skew drive both exposure boundaries. */\n readout: RollingSensorReadoutTimingDeclaration;\n /** Per-line integration time between line reset and line read. */\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n /**\n * Evidence that this capture uses a fully electronic, line-by-line (rolling) shutter whose reset\n * scan runs at the readout line rate. This explicit link is required before sensor readout timing\n * may set the exposure-window timing.\n */\n linkageEvidence: readonly EvidenceProvenance[];\n}\n\n/**\n * Exposure-window schedule of an electronic rolling shutter. Its `shutterMechanism`,\n * `nominalExposureDurationSeconds`, `opening` and `closing` fields form a complete exposure schedule, so\n * they can be passed as a native RAW / environment `exposureWindow` or spread into a production\n * `temporalCapture.exposureWindowInput` (see {@link createRollingShutterCaptureExposureSchedule}).\n */\nexport interface ElectronicRollingShutterExposureSchedule {\n modelId: typeof ELECTRONIC_ROLLING_SHUTTER_MODEL.id;\n modelVersion: typeof ELECTRONIC_ROLLING_SHUTTER_MODEL.version;\n shutterMechanism: \"electronic\";\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n /** Line-reset scan: a native point at normalized scan position s starts integrating at s·τ. */\n opening: UniformLinearExposureBoundarySchedule;\n /** Line-read scan: the same point stops integrating at T + s·τ. */\n closing: UniformLinearExposureBoundarySchedule;\n /** Explicit linkage declaration accepted by {@link assessReadoutExposureTimingLinkage}. */\n linkage: Extract<ReadoutExposureTimingLinkageDeclaration, { kind: \"spatial-phase-linked\" }>;\n /** τ: offset between the first and last exposure starts (equal to the readout skew). */\n lineScanSkewSeconds: number;\n /** Every point integrates for exactly the nominal exposure duration T. */\n localExposureDurationSeconds: number;\n limitations: readonly string[];\n}\n\n/** The exposure-schedule fields of a derived rolling-shutter schedule, without model metadata. */\nexport function createRollingShutterCaptureExposureSchedule(\n schedule: ElectronicRollingShutterExposureSchedule\n): Pick<CalculateCaptureExposureWindowsInput, \"shutterMechanism\" | \"nominalExposureDurationSeconds\" | \"opening\" | \"closing\"> {\n // 2.0.0: the whole schedule is checked, so an unknown field at any level is rejected.\n const record = requireRecord(schedule, \"rollingShutterSchedule\", [\"modelId\", \"modelVersion\", \"shutterMechanism\",\n \"nominalExposureDurationSeconds\", \"opening\", \"closing\", \"linkage\", \"lineScanSkewSeconds\", \"localExposureDurationSeconds\",\n \"limitations\"]);\n parseSecondsFact(record.nominalExposureDurationSeconds, \"rollingShutterSchedule.nominalExposureDurationSeconds\");\n parseBoundarySchedule(record.opening, \"rollingShutterSchedule.opening\");\n parseBoundarySchedule(record.closing, \"rollingShutterSchedule.closing\");\n if (record.linkage !== undefined) parseLinkageDeclaration(record.linkage);\n return structuredClone({ shutterMechanism: schedule.shutterMechanism,\n nominalExposureDurationSeconds: schedule.nominalExposureDurationSeconds,\n opening: schedule.opening, closing: schedule.closing });\n}\n\n/**\n * Derives the exposure-window schedule of a fully electronic rolling shutter from its rolling readout\n * declaration.\n *\n * In a CMOS rolling shutter, each line is reset, collects light for the exposure time T and is then\n * read. The reset pointer runs T ahead of the read pointer, and both move at the line rate (El Gamal &\n * Eltoukhy 2005; Liang, Chang & Chen 2008). With the uniform linear scan used throughout this engine, a\n * native point at normalized scan position s ∈ [0, 1] (along the declared scan direction, over the\n * active capture) collects light over [s·τ, s·τ + T], where τ is the declared first-to-last readout\n * skew. So both boundaries become uniform linear scans in the readout direction that take τ to sweep.\n *\n * When this schedule feeds the per-site exposure windows, everything that samples motion, moving\n * sources or time-varying light at local exposure times (environment RAW, rotation quadrature,\n * temporal illumination integration) produces rolling-shutter skew, wobble, partial exposure and\n * flicker banding.\n *\n * Approximation: constant line rate, no line-reset jitter, no hybrid of global reset with a mechanical\n * closing shutter, and the active-capture skew is declared by the caller (never scaled from a\n * full-frame value).\n * @see docs/MOTION_AND_SIGNAL.md#electronic-rolling-shutter-image-formation\n */\nexport function resolveElectronicRollingShutterExposureSchedule(\n input: ResolveElectronicRollingShutterExposureScheduleInput\n): ElectronicRollingShutterExposureSchedule {\n requireInputKeys(input, \"resolveElectronicRollingShutterExposureSchedule\", [\"readout\", \"nominalExposureDurationSeconds\", \"linkageEvidence\"]);\n // The input keys are checked above; calculateSensorReadoutTiming checks the readout's own fields.\n const record = requireRecord(input, \"rollingShutter\", null);\n const readout = requireRecord(record.readout, \"rollingShutter.readout\", null);\n if (readout.readoutMode !== \"rolling\") {\n throw new InvalidScientificInputError(\n \"An electronic rolling-shutter exposure schedule requires a rolling sensor readout declaration.\"\n );\n }\n const linkageEvidence = parseEvidenceList(input.linkageEvidence, \"rollingShutter.linkageEvidence\");\n const rolling = input.readout;\n // Check the declaration and the exposure fact with the existing readout and exposure-window calculators.\n const probeRaster = { pixelWidth: 2, pixelHeight: 2 };\n calculateSensorReadoutTiming({ nativeRaster: probeRaster, shutterMechanism: \"electronic\", readout: rolling });\n const opening: UniformLinearExposureBoundarySchedule = {\n kind: \"uniform-linear-native-scan\",\n directionNative: { value: rolling.scanDirectionNative.value, evidence: [...rolling.scanDirectionNative.evidence] },\n traversalDurationSeconds: { value: rolling.spatialSamplingSkewSeconds.value, unit: \"s\",\n evidence: [...rolling.spatialSamplingSkewSeconds.evidence] }\n };\n const closing: UniformLinearExposureBoundarySchedule = structuredClone(opening);\n const nominal = calculateCaptureExposureWindows({ nativeRaster: probeRaster, shutterMechanism: \"electronic\",\n nominalExposureDurationSeconds: input.nominalExposureDurationSeconds, opening, closing }).value.nominalExposureDurationSeconds;\n return {\n modelId: ELECTRONIC_ROLLING_SHUTTER_MODEL.id,\n modelVersion: ELECTRONIC_ROLLING_SHUTTER_MODEL.version,\n shutterMechanism: \"electronic\",\n nominalExposureDurationSeconds: { value: nominal.value, unit: \"s\", evidence: [...nominal.evidence] },\n opening,\n closing,\n linkage: { kind: \"spatial-phase-linked\", links: [\n { boundary: \"opening\", phaseOrientation: \"same\", evidence: linkageEvidence },\n { boundary: \"closing\", phaseOrientation: \"same\", evidence: linkageEvidence }] },\n lineScanSkewSeconds: rolling.spatialSamplingSkewSeconds.value,\n localExposureDurationSeconds: nominal.value,\n limitations: [\n \"Uniform-linear line timing: each native point integrates over [s·τ, s·τ + T] with s its normalized scan position.\",\n \"Line-reset jitter, non-uniform line timing, global-reset release and mechanical-close hybrids are not modeled.\",\n \"The skew τ is the caller-declared active-capture spatial sampling skew; it is never scaled from a full-frame value.\"\n ]\n };\n}\n\n/**\n * True when an exposure schedule is exactly the electronic rolling-shutter schedule derived from this\n * readout declaration: an electronic mechanism, with both boundaries scanning in the readout direction\n * and taking the readout skew to sweep.\n */\nexport function isElectronicRollingShutterExposureSchedule(\n schedule: Pick<CalculateCaptureExposureWindowsInput, \"shutterMechanism\" | \"opening\" | \"closing\">,\n readout: SensorReadoutTimingDeclaration\n): boolean {\n if (readout.readoutMode !== \"rolling\" || schedule.shutterMechanism !== \"electronic\") return false;\n return [schedule.opening, schedule.closing].every((boundary) => boundary.kind === \"uniform-linear-native-scan\" &&\n boundary.directionNative.value === readout.scanDirectionNative.value &&\n boundary.traversalDurationSeconds.value === readout.spatialSamplingSkewSeconds.value);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"readout-exposure-linkage.js","sourceRoot":"","sources":["../../src/sensor/readout-exposure-linkage.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Links rolling sensor readout to electronic shutter scans, and derives the exposure schedule of a\n * fully electronic rolling shutter.\n *\n * A declared link says that the readout scan and one or more exposure-boundary scans move across the\n * sensor in the same order (or in reverse). That is deliberately weaker than time synchronization: it\n * does not say when readout happens relative to the start or end of exposure. The total data-readout\n * time is kept for diagnostics but never used to check the link. The readout skew and the boundary\n * sweep times may differ; their ratio is reported without implying a shared clock.\n * @see docs/MOTION_AND_SIGNAL.md for equations, coordinate/unit conventions, blockers and support\n * limits.\n */\n\nimport {\n approximationResult,\n type CalculationProvenance,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n parseEvidenceList,\n type EvidenceProvenance\n} from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport type { RasterPoint, RasterRect } from \"../output/capture-geometry.js\";\nimport {\n calculateCaptureExposureWindows,\n parseBoundarySchedule,\n parseSecondsFact,\n type CalculateCaptureExposureWindowsInput,\n type ExposureBoundarySchedule,\n type UniformLinearExposureBoundarySchedule,\n type SourcedCaptureTimingSeconds\n} from \"./exposure-window.js\";\nimport {\n calculateSensorReadoutTiming,\n type CaptureShutterMechanism,\n type NativeSensorReadoutScanDirection,\n type RollingSensorReadoutTimingDeclaration,\n type SensorReadoutTimingDeclaration,\n type SourcedSensorTimingSeconds\n} from \"./readout-timing.js\";\nimport type { NativeImageRaster } from \"./sensor-geometry.js\";\nimport { requireInputKeys, rejectUnknownFields } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport type ReadoutExposureBoundaryId = \"opening\" | \"closing\";\n\nexport type ReadoutExposureSpatialPhaseOrientation =\n | \"same\"\n | \"reversed\";\n\nexport interface ReadoutExposureBoundarySpatialLink {\n boundary: ReadoutExposureBoundaryId;\n /**\n * How the normalized position of the rolling readout scan relates to that\n * of the selected electronic exposure-boundary scan.\n *\n * \"same\": boundary normalized phase = readout normalized phase.\n * \"reversed\": boundary normalized phase = 1 - readout normalized phase.\n *\n * This does not establish absolute temporal synchronization.\n */\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation;\n /**\n * Evidence for the link itself.\n *\n * The readout direction and timing, and the exposure-boundary direction and\n * timing, keep their own separate evidence.\n */\n evidence: readonly EvidenceProvenance[];\n}\n\nexport type ReadoutExposureTimingLinkageDeclaration =\n | {\n /**\n * No link between readout and exposure is stated for this capture.\n * This is not evidence that the physical processes are independent.\n */\n kind: \"unlinked\";\n links?: never;\n }\n | {\n /**\n * One or more exposure boundaries are explicitly linked to the rolling\n * readout by their normalized position across the native sensor.\n */\n kind: \"spatial-phase-linked\";\n links: readonly ReadoutExposureBoundarySpatialLink[];\n };\n\nexport interface AssessReadoutExposureTimingLinkageInput {\n nativeRaster: NativeImageRaster;\n activeCaptureRect?: RasterRect;\n shutterMechanism: CaptureShutterMechanism;\n readout: SensorReadoutTimingDeclaration;\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n opening: ExposureBoundarySchedule;\n closing: ExposureBoundarySchedule;\n linkage: ReadoutExposureTimingLinkageDeclaration;\n samplePointsNative?: readonly RasterPoint[];\n}\n\nexport interface ReadoutExposureBoundarySpatialLinkAssessment {\n boundary: ReadoutExposureBoundaryId;\n boundaryActuator: \"electronic\";\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation;\n normalizedPhaseRelationship:\n | \"boundary-phase-equals-readout-phase\"\n | \"boundary-phase-equals-one-minus-readout-phase\";\n relationshipEvidence: readonly EvidenceProvenance[];\n readoutDirectionNative: NativeSensorReadoutScanDirection;\n boundaryDirectionNative: NativeSensorReadoutScanDirection;\n /**\n * Rolling readout skew: time from the first to the last scan position.\n */\n readoutSpatialSamplingSkewSeconds: SourcedSensorTimingSeconds;\n /**\n * Time for the exposure boundary to sweep from first to last position.\n */\n boundaryTraversalDurationSeconds: SourcedCaptureTimingSeconds;\n /**\n * A descriptive ratio only. A value of 1 does not prove time\n * synchronization or a common clock origin.\n */\n boundaryTraversalToReadoutSpatialSkewRatio: number;\n absoluteTemporalAlignment: \"not-established\";\n}\n\nexport interface ReadoutExposureTimingLinkageAssessment {\n linkageKind: ReadoutExposureTimingLinkageDeclaration[\"kind\"];\n relationshipMeaning:\n | \"no-readout-exposure-relationship-asserted\"\n | \"normalized-spatial-phase-relationship-asserted\";\n readoutMode: \"rolling\" | \"global\";\n shutterMechanism: CaptureShutterMechanism;\n activeCaptureRect: RasterRect;\n absoluteTemporalAlignment: \"not-established\";\n /**\n * Data-readout time for the capture. Kept for diagnostics, but never used to\n * check an exposure-boundary link.\n */\n captureReadoutDurationSeconds: SourcedSensorTimingSeconds;\n links: readonly ReadoutExposureBoundarySpatialLinkAssessment[];\n componentProvenance: {\n sensorReadout: CalculationProvenance;\n exposureWindows: CalculationProvenance;\n };\n}\n\ntype UnknownRecord = Record<string, unknown>;\n\nconst BOUNDARIES = new Set<ReadoutExposureBoundaryId>([\n \"opening\",\n \"closing\"\n]);\n\nconst PHASE_ORIENTATIONS =\n new Set<ReadoutExposureSpatialPhaseOrientation>([\n \"same\",\n \"reversed\"\n ]);\n\nfunction requireRecord(\n value: unknown,\n path: string,\n allowedKeys: readonly string[] | null\n): UnknownRecord {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(path + \" must be an object.\");\n }\n if (allowedKeys !== null) rejectUnknownFields(value, allowedKeys, path);\n return value as UnknownRecord;\n}\n\nfunction parseSpatialLink(\n value: unknown,\n path: string\n): ReadoutExposureBoundarySpatialLink {\n const record = requireRecord(value, path, [\"boundary\", \"phaseOrientation\", \"evidence\"]);\n\n if (\n typeof record.boundary !== \"string\" ||\n !BOUNDARIES.has(record.boundary as ReadoutExposureBoundaryId)\n ) {\n throw new InvalidConfigurationError(path + \".boundary is invalid.\");\n }\n\n if (\n typeof record.phaseOrientation !== \"string\" ||\n !PHASE_ORIENTATIONS.has(\n record.phaseOrientation as ReadoutExposureSpatialPhaseOrientation\n )\n ) {\n throw new InvalidConfigurationError(\n path + \".phaseOrientation is invalid.\"\n );\n }\n\n return {\n boundary: record.boundary as ReadoutExposureBoundaryId,\n phaseOrientation:\n record.phaseOrientation as ReadoutExposureSpatialPhaseOrientation,\n evidence: parseEvidenceList(record.evidence, path + \".evidence\")\n };\n}\n\nfunction parseLinkageDeclaration(\n value: unknown\n): ReadoutExposureTimingLinkageDeclaration {\n const record = requireRecord(value, \"linkage\", [\"kind\", \"links\"]);\n\n if (record.kind === \"unlinked\") {\n if (record.links !== undefined) {\n throw new InvalidScientificInputError(\n \"linkage.links must be omitted when linkage.kind is unlinked.\"\n );\n }\n return { kind: \"unlinked\" };\n }\n\n if (record.kind !== \"spatial-phase-linked\") {\n throw new InvalidConfigurationError(\"linkage.kind is invalid.\");\n }\n\n if (!Array.isArray(record.links) || record.links.length === 0) {\n throw new InvalidConfigurationError(\n \"linkage.links must be a non-empty array for spatial-phase-linked linkage.\"\n );\n }\n\n const links = record.links.map((entry, index) =>\n parseSpatialLink(entry, \"linkage.links[\" + index + \"]\")\n );\n\n const seen = new Set<ReadoutExposureBoundaryId>();\n for (const link of links) {\n if (seen.has(link.boundary)) {\n throw new InvalidScientificInputError(\n \"linkage.links must not contain duplicate boundary links.\"\n );\n }\n seen.add(link.boundary);\n }\n\n return {\n kind: \"spatial-phase-linked\",\n links\n };\n}\n\nfunction oppositeDirection(\n direction: NativeSensorReadoutScanDirection\n): NativeSensorReadoutScanDirection {\n switch (direction) {\n case \"top-to-bottom\":\n return \"bottom-to-top\";\n case \"bottom-to-top\":\n return \"top-to-bottom\";\n case \"left-to-right\":\n return \"right-to-left\";\n case \"right-to-left\":\n return \"left-to-right\";\n }\n}\n\nfunction expectedBoundaryDirection(\n readoutDirection: NativeSensorReadoutScanDirection,\n phaseOrientation: ReadoutExposureSpatialPhaseOrientation\n): NativeSensorReadoutScanDirection {\n return phaseOrientation === \"same\"\n ? readoutDirection\n : oppositeDirection(readoutDirection);\n}\n\n/**\n * Checks an explicitly declared link between the rolling readout scan and one\n * or more electronic exposure-boundary scans.\n *\n * This is deliberately weaker than time synchronization. It can show that two\n * schedules sweep the native sensor in the same order (or in reverse), but not\n * when readout happens relative to the start or end of exposure.\n *\n * The total data-readout time is kept for diagnostics and never used to check\n * the link. The readout skew and the boundary sweep time may differ; their\n * ratio is reported without implying a shared clock.\n */\nexport function assessReadoutExposureTimingLinkage(\n input: AssessReadoutExposureTimingLinkageInput\n): CalculationResult<ReadoutExposureTimingLinkageAssessment> {\n requireInputKeys(input, \"assessReadoutExposureTimingLinkage\", [\"nativeRaster\", \"activeCaptureRect\", \"shutterMechanism\", \"readout\", \"nominalExposureDurationSeconds\", \"opening\", \"closing\", \"linkage\", \"samplePointsNative\"]);\n const linkage = parseLinkageDeclaration(input.linkage);\n\n const sensorReadout = calculateSensorReadoutTiming({\n nativeRaster: input.nativeRaster,\n ...(input.activeCaptureRect === undefined\n ? {}\n : { activeCaptureRect: input.activeCaptureRect }),\n shutterMechanism: input.shutterMechanism,\n readout: input.readout,\n ...(input.samplePointsNative === undefined\n ? {}\n : { samplePointsNative: input.samplePointsNative })\n });\n\n const exposureWindows = calculateCaptureExposureWindows({\n nativeRaster: input.nativeRaster,\n ...(input.activeCaptureRect === undefined\n ? {}\n : { activeCaptureRect: input.activeCaptureRect }),\n shutterMechanism: input.shutterMechanism,\n nominalExposureDurationSeconds:\n input.nominalExposureDurationSeconds,\n opening: input.opening,\n closing: input.closing,\n ...(input.samplePointsNative === undefined\n ? {}\n : { samplePointsNative: input.samplePointsNative })\n });\n\n if (linkage.kind === \"unlinked\") {\n return approximationResult(\n {\n linkageKind: \"unlinked\",\n relationshipMeaning:\n \"no-readout-exposure-relationship-asserted\",\n readoutMode: sensorReadout.value.readoutMode,\n shutterMechanism: input.shutterMechanism,\n activeCaptureRect: {\n ...sensorReadout.value.activeCaptureRect\n },\n absoluteTemporalAlignment: \"not-established\",\n captureReadoutDurationSeconds:\n sensorReadout.value.captureReadoutDurationSeconds,\n links: [],\n componentProvenance: {\n sensorReadout: sensorReadout.provenance,\n exposureWindows: exposureWindows.provenance\n }\n },\n \"readout-exposure-spatial-linkage\",\n \"1.0.0\",\n [\n \"Unlinked means Photivra asserts no readout-to-exposure relationship for this capture; it does not prove the physical processes are independent.\",\n \"Sensor data-readout timing and exposure-window timing retain independent provenance and semantics.\",\n \"Absolute temporal alignment between data readout and exposure boundaries is not established.\"\n ]\n );\n }\n\n const readoutScan = sensorReadout.value.scan;\n if (readoutScan === null) {\n throw new InvalidScientificInputError(\n \"spatial-phase-linked linkage requires rolling sensor readout with a spatial scan.\"\n );\n }\n\n const assessments =\n linkage.links.map<ReadoutExposureBoundarySpatialLinkAssessment>(\n (link) => {\n const boundary =\n link.boundary === \"opening\"\n ? exposureWindows.value.opening\n : exposureWindows.value.closing;\n\n if (boundary.actuator !== \"electronic\") {\n throw new InvalidScientificInputError(\n \"A readout spatial-phase link may target only an electronic exposure boundary.\"\n );\n }\n\n if (\n boundary.schedule.kind !==\n \"uniform-linear-native-scan\"\n ) {\n throw new InvalidScientificInputError(\n \"A readout spatial-phase link requires the selected exposure boundary to use a uniform-linear native scan.\"\n );\n }\n\n const readoutDirection =\n readoutScan.directionNative.value;\n const boundaryDirection =\n boundary.schedule.directionNative.value;\n const expectedDirection = expectedBoundaryDirection(\n readoutDirection,\n link.phaseOrientation\n );\n\n if (boundaryDirection !== expectedDirection) {\n throw new InvalidScientificInputError(\n \"Declared readout/exposure spatial phase orientation contradicts the readout and exposure-boundary scan directions.\"\n );\n }\n\n const readoutSpatialSamplingSkewSeconds =\n readoutScan.spatialSamplingSkewSeconds;\n const boundaryTraversalDurationSeconds =\n boundary.schedule.traversalDurationSeconds;\n\n return {\n boundary: link.boundary,\n boundaryActuator: \"electronic\",\n phaseOrientation: link.phaseOrientation,\n normalizedPhaseRelationship:\n link.phaseOrientation === \"same\"\n ? \"boundary-phase-equals-readout-phase\"\n : \"boundary-phase-equals-one-minus-readout-phase\",\n relationshipEvidence: link.evidence,\n readoutDirectionNative: readoutDirection,\n boundaryDirectionNative: boundaryDirection,\n readoutSpatialSamplingSkewSeconds,\n boundaryTraversalDurationSeconds,\n boundaryTraversalToReadoutSpatialSkewRatio:\n boundaryTraversalDurationSeconds.value /\n readoutSpatialSamplingSkewSeconds.value,\n absoluteTemporalAlignment: \"not-established\"\n };\n }\n );\n\n return approximationResult(\n {\n linkageKind: \"spatial-phase-linked\",\n relationshipMeaning:\n \"normalized-spatial-phase-relationship-asserted\",\n readoutMode: sensorReadout.value.readoutMode,\n shutterMechanism: input.shutterMechanism,\n activeCaptureRect: {\n ...sensorReadout.value.activeCaptureRect\n },\n absoluteTemporalAlignment: \"not-established\",\n captureReadoutDurationSeconds:\n sensorReadout.value.captureReadoutDurationSeconds,\n links: assessments,\n componentProvenance: {\n sensorReadout: sensorReadout.provenance,\n exposureWindows: exposureWindows.provenance\n }\n },\n \"readout-exposure-spatial-linkage\",\n \"1.0.0\",\n [\n \"The linkage establishes normalized native-sensor spatial phase/order only, not absolute temporal synchronization.\",\n \"Every asserted boundary link carries evidence separate from the evidence for the underlying readout and exposure timing facts.\",\n \"Only electronic exposure boundaries may be linked to rolling sensor readout spatial phase.\",\n \"Same phase orientation requires matching native scan direction; reversed phase orientation requires the exact opposite direction on the same native axis.\",\n \"Rolling readout spatial skew and exposure-boundary traversal duration may differ; their ratio is descriptive and does not establish a common clock origin.\",\n \"Capture data-readout duration is not used to validate exposure-boundary timing linkage.\",\n \"Active capture, native coordinate semantics and shutter mechanism are shared because both component schedules are resolved from the same input.\",\n \"Sensor readout and exposure-window component provenance remain visible; linkage does not upgrade either component to calibrated status.\",\n \"No camera-specific timing relationship is inferred from shutter mechanism, sensor architecture metadata, output resolution or adjacent specifications.\"\n ]\n );\n}\n\n/** Model identity of the electronic rolling-shutter (line reset → line read) exposure schedule. */\nexport const ELECTRONIC_ROLLING_SHUTTER_MODEL = /* @__PURE__ */ Object.freeze({\n id: \"electronic-rolling-shutter-line-reset-read\",\n version: \"1.0.0\"\n} as const);\n\nexport interface ResolveElectronicRollingShutterExposureScheduleInput {\n /** Rolling readout declaration whose scan direction and spatial skew drive both exposure boundaries. */\n readout: RollingSensorReadoutTimingDeclaration;\n /** Per-line integration time between line reset and line read. */\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n /**\n * Evidence that this capture uses a fully electronic, line-by-line (rolling) shutter whose reset\n * scan runs at the readout line rate. This explicit link is required before sensor readout timing\n * may set the exposure-window timing.\n */\n linkageEvidence: readonly EvidenceProvenance[];\n}\n\n/**\n * Exposure-window schedule of an electronic rolling shutter. Its `shutterMechanism`,\n * `nominalExposureDurationSeconds`, `opening` and `closing` fields form a complete exposure schedule, so\n * they can be passed as a native RAW / environment `exposureWindow` or spread into a production\n * `temporalCapture.exposureWindowInput` (see {@link createRollingShutterCaptureExposureSchedule}).\n */\nexport interface ElectronicRollingShutterExposureSchedule {\n modelId: typeof ELECTRONIC_ROLLING_SHUTTER_MODEL.id;\n modelVersion: typeof ELECTRONIC_ROLLING_SHUTTER_MODEL.version;\n shutterMechanism: \"electronic\";\n nominalExposureDurationSeconds: SourcedCaptureTimingSeconds;\n /** Line-reset scan: a native point at normalized scan position s starts integrating at s·τ. */\n opening: UniformLinearExposureBoundarySchedule;\n /** Line-read scan: the same point stops integrating at T + s·τ. */\n closing: UniformLinearExposureBoundarySchedule;\n /** Explicit linkage declaration accepted by {@link assessReadoutExposureTimingLinkage}. */\n linkage: Extract<ReadoutExposureTimingLinkageDeclaration, { kind: \"spatial-phase-linked\" }>;\n /** τ: offset between the first and last exposure starts (equal to the readout skew). */\n lineScanSkewSeconds: number;\n /** Every point integrates for exactly the nominal exposure duration T. */\n localExposureDurationSeconds: number;\n limitations: readonly string[];\n}\n\n/** The exposure-schedule fields of a derived rolling-shutter schedule, without model metadata. */\nexport function createRollingShutterCaptureExposureSchedule(\n schedule: ElectronicRollingShutterExposureSchedule\n): Pick<CalculateCaptureExposureWindowsInput, \"shutterMechanism\" | \"nominalExposureDurationSeconds\" | \"opening\" | \"closing\"> {\n // 2.0.0: the whole schedule is checked, so an unknown field at any level is rejected.\n const record = requireRecord(schedule, \"rollingShutterSchedule\", [\"modelId\", \"modelVersion\", \"shutterMechanism\",\n \"nominalExposureDurationSeconds\", \"opening\", \"closing\", \"linkage\", \"lineScanSkewSeconds\", \"localExposureDurationSeconds\",\n \"limitations\"]);\n parseSecondsFact(record.nominalExposureDurationSeconds, \"rollingShutterSchedule.nominalExposureDurationSeconds\");\n parseBoundarySchedule(record.opening, \"rollingShutterSchedule.opening\");\n parseBoundarySchedule(record.closing, \"rollingShutterSchedule.closing\");\n if (record.linkage !== undefined) parseLinkageDeclaration(record.linkage);\n return structuredClone({ shutterMechanism: schedule.shutterMechanism,\n nominalExposureDurationSeconds: schedule.nominalExposureDurationSeconds,\n opening: schedule.opening, closing: schedule.closing });\n}\n\n/**\n * Derives the exposure-window schedule of a fully electronic rolling shutter from its rolling readout\n * declaration.\n *\n * In a CMOS rolling shutter, each line is reset, collects light for the exposure time T and is then\n * read. The reset pointer runs T ahead of the read pointer, and both move at the line rate (El Gamal &\n * Eltoukhy 2005; Liang, Chang & Chen 2008). With the uniform linear scan used throughout this engine, a\n * native point at normalized scan position s ∈ [0, 1] (along the declared scan direction, over the\n * active capture) collects light over [s·τ, s·τ + T], where τ is the declared first-to-last readout\n * skew. So both boundaries become uniform linear scans in the readout direction that take τ to sweep.\n *\n * When this schedule feeds the per-site exposure windows, everything that samples motion, moving\n * sources or time-varying light at local exposure times (environment RAW, rotation quadrature,\n * temporal illumination integration) produces rolling-shutter skew, wobble, partial exposure and\n * flicker banding.\n *\n * Approximation: constant line rate, no line-reset jitter, no hybrid of global reset with a mechanical\n * closing shutter, and the active-capture skew is declared by the caller (never scaled from a\n * full-frame value).\n * @see docs/MOTION_AND_SIGNAL.md#electronic-rolling-shutter-image-formation\n */\nexport function resolveElectronicRollingShutterExposureSchedule(\n input: ResolveElectronicRollingShutterExposureScheduleInput\n): ElectronicRollingShutterExposureSchedule {\n requireInputKeys(input, \"resolveElectronicRollingShutterExposureSchedule\", [\"readout\", \"nominalExposureDurationSeconds\", \"linkageEvidence\"]);\n // The input keys are checked above; calculateSensorReadoutTiming checks the readout's own fields.\n const record = requireRecord(input, \"rollingShutter\", null);\n const readout = requireRecord(record.readout, \"rollingShutter.readout\", null);\n if (readout.readoutMode !== \"rolling\") {\n throw new InvalidScientificInputError(\n \"An electronic rolling-shutter exposure schedule requires a rolling sensor readout declaration.\"\n );\n }\n const linkageEvidence = parseEvidenceList(input.linkageEvidence, \"rollingShutter.linkageEvidence\");\n const rolling = input.readout;\n // Check the declaration and the exposure fact with the existing readout and exposure-window calculators.\n const probeRaster = { pixelWidth: 2, pixelHeight: 2 };\n calculateSensorReadoutTiming({ nativeRaster: probeRaster, shutterMechanism: \"electronic\", readout: rolling });\n const opening: UniformLinearExposureBoundarySchedule = {\n kind: \"uniform-linear-native-scan\",\n directionNative: { value: rolling.scanDirectionNative.value, evidence: [...rolling.scanDirectionNative.evidence] },\n traversalDurationSeconds: { value: rolling.spatialSamplingSkewSeconds.value, unit: \"s\",\n evidence: [...rolling.spatialSamplingSkewSeconds.evidence] }\n };\n const closing: UniformLinearExposureBoundarySchedule = structuredClone(opening);\n const nominal = calculateCaptureExposureWindows({ nativeRaster: probeRaster, shutterMechanism: \"electronic\",\n nominalExposureDurationSeconds: input.nominalExposureDurationSeconds, opening, closing }).value.nominalExposureDurationSeconds;\n return {\n modelId: ELECTRONIC_ROLLING_SHUTTER_MODEL.id,\n modelVersion: ELECTRONIC_ROLLING_SHUTTER_MODEL.version,\n shutterMechanism: \"electronic\",\n nominalExposureDurationSeconds: { value: nominal.value, unit: \"s\", evidence: [...nominal.evidence] },\n opening,\n closing,\n linkage: { kind: \"spatial-phase-linked\", links: [\n { boundary: \"opening\", phaseOrientation: \"same\", evidence: linkageEvidence },\n { boundary: \"closing\", phaseOrientation: \"same\", evidence: linkageEvidence }] },\n lineScanSkewSeconds: rolling.spatialSamplingSkewSeconds.value,\n localExposureDurationSeconds: nominal.value,\n limitations: [\n \"Uniform-linear line timing: each native point integrates over [s·τ, s·τ + T] with s its normalized scan position.\",\n \"Line-reset jitter, non-uniform line timing, global-reset release and mechanical-close hybrids are not modeled.\",\n \"The skew τ is the caller-declared active-capture spatial sampling skew; it is never scaled from a full-frame value.\"\n ]\n };\n}\n\n/**\n * True when an exposure schedule is exactly the electronic rolling-shutter schedule derived from this\n * readout declaration: an electronic mechanism, with both boundaries scanning in the readout direction\n * and taking the readout skew to sweep.\n */\nexport function isElectronicRollingShutterExposureSchedule(\n schedule: Pick<CalculateCaptureExposureWindowsInput, \"shutterMechanism\" | \"opening\" | \"closing\">,\n readout: SensorReadoutTimingDeclaration\n): boolean {\n if (readout.readoutMode !== \"rolling\" || schedule.shutterMechanism !== \"electronic\") return false;\n return [schedule.opening, schedule.closing].every((boundary) => boundary.kind === \"uniform-linear-native-scan\" &&\n boundary.directionNative.value === readout.scanDirectionNative.value &&\n boundary.traversalDurationSeconds.value === readout.spatialSamplingSkewSeconds.value);\n}\n"]}
|