@photivra/engine 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/dist/capture/native-raw-dng.d.ts +3 -0
  6. package/dist/capture/native-raw-dng.js +23 -3
  7. package/dist/capture/native-raw-dng.js.map +1 -1
  8. package/dist/core/version.d.ts +1 -1
  9. package/dist/core/version.js +1 -1
  10. package/dist/core/version.js.map +1 -1
  11. package/dist/facade/simulate-capture.d.ts +12 -1
  12. package/dist/facade/simulate-capture.js +45 -5
  13. package/dist/facade/simulate-capture.js.map +1 -1
  14. package/dist/facade/subject-framing.d.ts +62 -0
  15. package/dist/facade/subject-framing.js +62 -19
  16. package/dist/facade/subject-framing.js.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.js +2 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/optics/diffraction-psf-profile.d.ts +51 -2
  21. package/dist/optics/diffraction-psf-profile.js +116 -3
  22. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  23. package/dist/output/text-metadata.d.ts +32 -0
  24. package/dist/output/text-metadata.js +201 -0
  25. package/dist/output/text-metadata.js.map +1 -0
  26. package/docs/API_REFERENCE.md +12 -1
  27. package/docs/QUICK_START.md +151 -6
  28. package/docs/api/capture-native-raw-dng.md +2 -0
  29. package/docs/api/core-version.md +1 -1
  30. package/docs/api/exports.json +22 -2
  31. package/docs/api/facade-simulate-capture.md +11 -1
  32. package/docs/api/facade-subject-framing.md +67 -0
  33. package/docs/api/optics-diffraction-psf-profile.md +8 -1
  34. package/docs/api/output-text-metadata.md +20 -0
  35. package/package.json +1 -1
@@ -0,0 +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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\");\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,6 +1,6 @@
1
1
  # Public API reference
2
2
 
3
- Generated from the exact root exports for **@photivra/engine 2.3.0 / root contract 2.3.0** by `node scripts/generate-api-reference.mjs`. No module deep import is supported.
3
+ Generated from the exact root exports for **@photivra/engine 2.5.0 / root contract 2.5.0** by `node scripts/generate-api-reference.mjs`. No module deep import is supported.
4
4
 
5
5
  Use [the developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md) for executable paths and scientific boundaries. Types are contracts, not runtime validation: parse untrusted data, preserve units/reference frames, read assessment blockers, and retain evidence/limitations. Optional properties do not imply a universal default. Model/schema IDs remain independent of the distribution.
6
6
 
@@ -1208,8 +1208,11 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1208
1208
 
1209
1209
  | Export | Kind |
1210
1210
  | --- | --- |
1211
+ | [`calculateChartFraming`](api/facade-subject-framing.md#calculatechartframing) | Runtime |
1212
+ | [`CalculateChartFramingInput`](api/facade-subject-framing.md#calculatechartframinginput) | Type |
1211
1213
  | [`calculateSubjectDistanceForFraming`](api/facade-subject-framing.md#calculatesubjectdistanceforframing) | Runtime |
1212
1214
  | [`CalculateSubjectDistanceForFramingInput`](api/facade-subject-framing.md#calculatesubjectdistanceforframinginput) | Type |
1215
+ | [`ChartFraming`](api/facade-subject-framing.md#chartframing) | Type |
1213
1216
  | [`SubjectFraming`](api/facade-subject-framing.md#subjectframing) | Type |
1214
1217
  | [`SubjectFramingFit`](api/facade-subject-framing.md#subjectframingfit) | Type |
1215
1218
 
@@ -2077,6 +2080,14 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
2077
2080
  | [`CalculateSubjectFramingCropInput`](api/output-subject-framing-crop.md#calculatesubjectframingcropinput) | Type |
2078
2081
  | [`SubjectFramingCrop`](api/output-subject-framing-crop.md#subjectframingcrop) | Type |
2079
2082
 
2083
+ ## output/text-metadata.ts
2084
+
2085
+ [Detailed contracts](api/output-text-metadata.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/output/text-metadata.ts)
2086
+
2087
+ | Export | Kind |
2088
+ | --- | --- |
2089
+ | [`OutputTextMetadata`](api/output-text-metadata.md#outputtextmetadata) | Type |
2090
+
2080
2091
  ## processing/demosaic.ts
2081
2092
 
2082
2093
  [Detailed contracts](api/processing-demosaic.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/processing/demosaic.ts)
@@ -90,6 +90,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
90
90
  | `output.bitDepth` | 8 | PNG only: 8 or 16. |
91
91
  | `output.gamutMapping` | `"clip"` | PNG only: or `"perceptual"`. |
92
92
  | `output.dng.capturedAtUtc` | none | DNG only: capture time for the date tags. |
93
+ | `output.metadata` | none | 2.4: `{ description?, software?, copyright?, comment? }` written into JPEG, PNG and DNG files unchanged (see [Text metadata](#text-metadata-24)). |
93
94
  | `output.demosaic` | the 1.0 bilinear kernels | `"bilinear"` or, from 2.1, `"ahd"` (see [Demosaic](#demosaic)). |
94
95
  | `output.borderHandling` | `"margin"` | Or `"mirror"`. |
95
96
  | `output.toneCurve` | `"identity"` | Any `SdrToneCurve`, for example `"positive-reinhard-per-channel"`. |
@@ -97,7 +98,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
97
98
  | `output.highlightMode` | `"clip"` | Or `"reconstruct"`. |
98
99
  | `output.whiteBalance` | `"scene-illuminant"` | Also `"auto"`, `"none"`, `{ kelvin, tint? }` and `{ channelGains }`. |
99
100
  | `output.jpegQuantizationStep` | 2 | JPEG only: constant step 1–255, not a manufacturer quality scale. |
100
- | `sensor.pixelPitchMicrometers` | 6 | 0.5–100. The sensor is `width × pitch` wide (see [Geometry](#geometry)); the ISO readout scales with the pixel area (see [ISO and pixel size](#iso-and-pixel-size)). |
101
+ | `sensor.pixelPitchMicrometers` | 6 | 0.5–1000 (2.5; 0.5–100 before). The sensor is `width × pitch` wide (see [Geometry](#geometry)); the ISO readout scales with the pixel area (see [ISO and pixel size](#iso-and-pixel-size)). Above 100 µm the capture is a downsized view of a whole frame (see [Whole-frame views](#whole-frame-views-25)). |
101
102
  | `sensor.spectralStepNanometers` | 20 | Spectral quadrature over 380–780 nm. |
102
103
  | `sensor.temporalSampleCount` | 1, or 8 when the camera rotates | Midpoints across each site's exposure window. |
103
104
  | `sensor.noiseSeed` | 1 | Output is deterministic for a given seed. |
@@ -150,7 +151,8 @@ console.log(a3.scene.chart?.frameWidthFraction, a3.scene.chart?.magnification, a
150
151
  The simulated sensor has real pixels of `sensor.pixelPitchMicrometers`. A 64 × 64 capture with 6 µm
151
152
  pixels is therefore a 0.38 mm crop of a large sensor, not a downscaled full frame. This keeps the
152
153
  signal per pixel, and so the noise and the ISO behaviour, physically consistent with the tier's
153
- sensor values.
154
+ sensor values. To see a whole frame at a small output size instead, raise the pitch (see
155
+ [Whole-frame views](#whole-frame-views-25)).
154
156
 
155
157
  ### Generic prime lenses (2.3)
156
158
 
@@ -161,7 +163,8 @@ real products, and carry no brand:
161
163
 
162
164
  - **Focal length** = equivalent ÷ the diagonal crop factor of the sensor (`calculateEquivalentFocalLength35Mm`,
163
165
  36 × 24 mm reference). In `simulateCapture` the sensor is the simulated frame, output width × height ×
164
- pixel pitch: to simulate a whole full-frame sensor at 768 × 512, set the pitch to 36 mm / 768 = 46.875 µm.
166
+ pixel pitch: to simulate a whole full-frame sensor at 768 × 512, set the pitch to 36 mm / 768 = 46.875 µm (or 750 µm at
167
+ 48 × 32, see [Whole-frame views](#whole-frame-views-25)).
165
168
  - **Aperture range:** widest f/2.8 (10–16 mm), f/1.8 (20–28 mm), f/1.4 (35–85 mm), f/2 (105–135 mm),
166
169
  f/2.8 (200–300 mm), f/4 (400–600 mm), f/5.6 (800 mm), f/8 (1200 mm); narrowest f/16 up to 16 mm, f/22 up
167
170
  to 135 mm, f/32 beyond. Any f-number inside the range is accepted; `fNumbers` lists the marked
@@ -189,7 +192,8 @@ cropped on its other side). The result reports the magnification, image distance
189
192
  `coverage` (the fraction of the frame width and height the chart covers), the generic prime of that
190
193
  equivalent focal length if there is one, the prime family's minimum focus distance at that focal length
191
194
  and `belowMinimumFocus`, plus assumptions in the same form as `simulateCapture`. It is the same framing
192
- `simulateCapture` uses, so `chartSize` at the returned distance gives `frameWidthFraction` 1 (tested).
195
+ `simulateCapture` uses, so `chartSize` at the returned distance gives `frameWidthFraction` 1 (tested). For the coverage at any other distance, see
196
+ [Chart coverage at any distance](#chart-coverage-at-any-distance-25).
193
197
 
194
198
  An A3 chart (0.42 × 0.28 m) on a full-frame sensor: 0.127 m at 10 mm, 0.633 m at 50 mm, 2.53 m at
195
199
  200 mm and 15.2 m at 1200 mm (magnification 0.0857, minimum focus 0.10, 0.38, 1.53 and 9.2 m). On a
@@ -207,6 +211,87 @@ const capture = await simulateCapture({ scene: { preset: "siemens-star" }, lens:
207
211
  console.log(capture.scene.chart?.frameWidthFraction);
208
212
  ```
209
213
 
214
+ ### Chart coverage at any distance (2.5)
215
+
216
+ `calculateChartFraming({ sensor, focalLengthMm | equivalentFocalLengthMm, chart, subjectDistanceM, focusDistanceM? })`
217
+ answers the inverse question: how much of the frame does a chart of physical size `chart` (`{ widthM, heightM? }`, as
218
+ for `chartSize`) cover at a given distance? It uses the facade's own thin-lens framing: image distance
219
+ v = f·s/(s − f) at the focus distance s (default: the subject distance), magnification m = v/d on the chart plane at
220
+ d, and a frame that spans sensor side ÷ m. The result reports the magnification, image distance, framed plane size,
221
+ `coverage` (`frameWidthFraction` and `frameHeightFraction`; above 1 when the chart is larger than the frame and
222
+ cropped), the chart's image size in millimetres, the generic prime of that equivalent focal length if there is one,
223
+ the prime family's minimum focus distance and `belowMinimumFocus` (the subject or focus distance is closer than it,
224
+ which `simulateCapture` with `lens.prime` refuses), plus assumptions.
225
+
226
+ An A3 chart (0.42 × 0.28 m) with the 50 mm full-frame prime covers 130 % of the frame width at 0.5 m (cropped),
227
+ 61.4 % at 1 m, 29.9 % at 2 m and 11.8 % at 5 m (magnification 0.111, 0.0526, 0.0256 and 0.0101).
228
+
229
+ ```ts
230
+ import { calculateChartFraming } from "@photivra/engine";
231
+
232
+ const coverage = calculateChartFraming({ sensor: { widthMm: 36, heightMm: 24 }, equivalentFocalLengthMm: 50, chart: { widthM: 0.42, heightM: 0.28 }, subjectDistanceM: 2 });
233
+ console.log(coverage.coverage.frameWidthFraction, coverage.magnification, coverage.belowMinimumFocus);
234
+ ```
235
+
236
+ Tests check that at `calculateSubjectDistanceForFraming`'s distance the coverage is 1 on the fitted side (every fit,
237
+ three sensor shapes, four primes), and that at any distance it equals `result.scene.chart` from `simulateCapture` with
238
+ `chartSize` (magnification, both coverage fractions, the chart's image size and the framed size, within 1e-12
239
+ relative), including a separate `lens.focusDistanceM`.
240
+
241
+ ### Whole-frame views (2.5)
242
+
243
+ To render the whole sensor frame at a small output size, set the pitch to sensor width ÷ output width:
244
+ 36 mm at 48 pixels is 750 µm, 44 mm at 48 pixels 917 µm. From 2.5 the pitch may be up to 1000 µm. The
245
+ frame is then the real sensor's size, so `lens.prime` (its crop factor and minimum focus),
246
+ `calculateSubjectDistanceForFraming` and `chartSize` all match that sensor, and the image is a shrunk view of
247
+ the whole frame. No new option is needed: the facade has always simulated a sensor `width × pitch` wide.
248
+
249
+ ```ts
250
+ import { simulateCapture } from "@photivra/engine";
251
+
252
+ const view = await simulateCapture({ scene: { preset: "dead-leaves" }, lens: { prime: 35, fNumber: 2.8 }, subjectDistanceM: 3,
253
+ sensor: { pixelPitchMicrometers: 36000 / 48, pixelAperture: "box" }, output: { width: 48, height: 32 } });
254
+ console.log(view.assumptions.find((a) => a.id === "sensor.wholeFrameView")?.value); // { pixelPitchMicrometers: 750, frameWidthMm: 36, frameHeightMm: 24 }
255
+ ```
256
+
257
+ Above 100 µm (larger than any camera photosite) the assumption `sensor.wholeFrameView` records the frame size.
258
+ What changes and what does not:
259
+
260
+ - **Brightness is the same as at any pitch.** The ISO area normalization (below) multiplies the conversion gain
261
+ and saturation by the area ratio, up to 27 778 at 1000 µm, so the RAW codes and the image are those of a small
262
+ pitch. Tested at 150–1000 µm against 6 µm: every CFA channel's mean RAW code within half a code (plus the 6 µm
263
+ reference's own noise), the 8-bit sRGB mean within one code, and a 64× overexposure clipping at the same white
264
+ code. Charge counts reach 1.7 × 10⁹ e⁻; the Poisson sampler's normal branch above 10⁹ keeps variance/mean at 1.
265
+ - **Noise is that of a binned, downsized view, not the real sensor's per-pixel noise.** Each output pixel
266
+ collects the charge of its whole square, so its shot noise equals that of all the real photosites in the
267
+ square summed (Poisson counts add) or averaged, and its SNR √N_e grows in proportion to the pitch. On an 18 %
268
+ card at f/8, 1/125 s, ISO 100 a 917 µm pixel collects about 1.6 × 10⁸ e⁻ (SNR about 12 700): under a fifth of a
269
+ RAW code, so the image looks noise-free, as a downsized photograph does. Read noise and dark current are applied
270
+ once per output pixel, not once per real photosite, so in deep shadows the noise is lower than a real sensor's
271
+ binned sum (√n times the read noise for n photosites). Tested by photon transfer at 150, 458 and 917 µm, ISO
272
+ 12 800: the code variance matches N_e/K² + σ_read²/K² + 1/12 within 12 % (EMVA 1288 Release 4.0, §2, free).
273
+ - **Sampling.** The default point aperture samples one point of each square, so detail finer than the pitch
274
+ aliases at full contrast. Use `sensor.pixelAperture: "box"` (or `{ kind: "box", samplesPerAxis }` up to 16) to
275
+ average over the square, or the four-spot filter, whose separation scales with the pitch.
276
+ - **Diffraction.** The Airy pattern (a few µm) is far smaller than a pixel. The default kernel is 3 × 3 sites and
277
+ the centre site keeps 1 − 8√2/(π³β) of the energy, β = pitch/(λ·N_w): 99.96 % at 780 nm, f/1.4 and 917 µm.
278
+ Cells larger than 192 cut-off periods are integrated in real space (see [PSF foundation](https://github.com/Photivra/photivra/blob/main/docs/PSF_FOUNDATION.md#diffraction-limited-sampled-profile-21)),
279
+ so a 48 × 32 view of a 44 mm frame with `lens.diffraction` at f/1.4 took 1.0 s (the frequency-domain cells took
280
+ 296 s there; both measured on the same machine in Node).
281
+ - **Motion, rolling shutter, anti-aliasing filter, AHD and the box aperture** follow the same angles at any pitch:
282
+ a yaw of ω moves the image ω·f/pitch pixels per second. A test renders each of them at 917 µm and at 9.17 µm with
283
+ the same 50 mm-equivalent prime (so f scales with the frame) and finds the same image within the small pitch's
284
+ own noise (mean difference under 1 code; AHD under 2.5).
285
+ - **Region tiles** stay byte-identical to the whole render (tested with diffraction, the four-spot filter, the box
286
+ aperture, AHD, rotation and a rolling shutter at 640 and 917 µm).
287
+ - **WebGPU.** `simulateCapture` runs on the CPU. The optional `webgpu-f32` sensor stage of
288
+ `createImageKernelAccelerator` handles capacities up to 2^24 e⁻, which the tier's 60 000 e⁻ reach at 100 µm; above
289
+ that it fails closed with `unsupported-float32-domain` (see [WebGPU backend](https://github.com/Photivra/photivra/blob/main/docs/WEBGPU.md)). The `cpu-reference`
290
+ backend runs it. The convolution, temporal-sum and chart stages work in sites and frame units, so the pitch does
291
+ not enter them.
292
+
293
+ Requests at 0.5–100 µm are byte-identical to 2.4.1.
294
+
210
295
  ### ISO and pixel size
211
296
 
212
297
  The tier's ISO signal chain (conversion gain, saturation signal and read noise per ISO setting) is
@@ -225,7 +310,8 @@ independent of pixel size, so the same exposure gives the same image brightness
225
310
  What you see: brightness stays the same, and per-pixel noise rises as pixels shrink, because a
226
311
  smaller pixel collects fewer electrons for the same exposure (shot-noise SNR = √N_e). At f/8, 1/125 s,
227
312
  ISO 100 on an 18 % grey card, the 8-bit sRGB mean is 101.2 at 30, 6 and 3 µm, and the green standard
228
- deviation is 0.18, 0.93 and 1.83 codes. Below about 1 µm the 8-bit sRGB mean falls slightly (about 1 code at 0.55 µm)
313
+ deviation is 0.18, 0.93 and 1.83 codes. At whole-frame pitches (above 100 µm) the per-pixel noise falls below one
314
+ code; see [Whole-frame views](#whole-frame-views-25). Below about 1 µm the 8-bit sRGB mean falls slightly (about 1 code at 0.55 µm)
229
315
  although the linear RAW mean is unchanged (within 0.3 %): strong noise passed through the concave
230
316
  sRGB curve lowers the mean of the encoded values.
231
317
 
@@ -463,7 +549,8 @@ The DNG states only what the capture has:
463
549
  | ColorMatrix1 (D65), ForwardMatrix1, AnalogBalance, AsShotNeutral | The facade's diagonal camera→XYZ profile and the capture's white-balance intent |
464
550
  | BaselineExposure | 0: the RAW white level maps to display white, as in the facade's development |
465
551
  | NoiseProfile | Each channel's readout regime: conversion gain ÷ digital gain and the combined read noise (Poisson, Gaussian and quantization only) |
466
- | ImageDescription | States that it is a simulated packed RAW with no linear master or rendering stored |
552
+ | ImageDescription | States that it is a simulated packed RAW with no linear master or rendering stored; with `output.metadata` (2.4) the caller's description follows when it is printable ASCII |
553
+ | Copyright, XMP (tag 700) | Only with `output.metadata` (2.4); see [Text metadata](#text-metadata-24) |
467
554
 
468
555
  - Saturation flags of sites whose code is below WhiteLevel have no DNG field. They are counted in the
469
556
  assumptions.
@@ -482,6 +569,59 @@ const dng = parseDngRawImage({ bytes: shot.image.dng! });
482
569
  console.log(dng.width, dng.activeArea, dng.whiteLevel, dng.codes.length === shot.raw.codes.length);
483
570
  ```
484
571
 
572
+ ### Text metadata (2.4)
573
+
574
+ `output.metadata` writes the caller's own text into the file. The engine writes the text exactly as
575
+ given and adds no text of its own (in a DNG the description follows the existing simulated-RAW statement). It applies to formats `"jpeg"`, `"png"` and `"dng"` (and to a PNG
576
+ `simulateIdealReference`); other formats reject it. Without it, every file is byte-identical to 2.3.0
577
+ (hash locks in `test/output-text-metadata.test.ts`).
578
+
579
+ ```ts
580
+ import { simulateCapture } from "@photivra/engine";
581
+
582
+ const shot = await simulateCapture({
583
+ scene: { preset: "siemens-star" },
584
+ output: { width: 8, height: 8, format: "png",
585
+ metadata: { description: "Siemens star at f/8", software: "My Viewer 1.0", copyright: "Rights held by the photographer", comment: "Test run 3" } }
586
+ });
587
+ console.log(shot.image.png?.length, shot.assumptions.find((a) => a.id === "output.metadata")?.value);
588
+ ```
589
+
590
+ | Field | JPEG | PNG | DNG |
591
+ | --- | --- | --- | --- |
592
+ | `description` | XMP `dc:description` | `Description` | ImageDescription after the simulated-RAW statement (ASCII only); XMP `dc:description` (statement, then the text) |
593
+ | `software` | XMP `xmp:CreatorTool` | `Software` | XMP `xmp:CreatorTool` |
594
+ | `copyright` | XMP `dc:rights` | `Copyright` | Copyright tag (ASCII only); XMP `dc:rights` |
595
+ | `comment` | COM segment and XMP `exif:UserComment` | `Comment` | XMP `exif:UserComment` |
596
+
597
+ Each field is optional, but at least one must be set. A field is a non-empty string of at most
598
+ 2048 UTF-8 bytes. Lone UTF-16 surrogates (which have no UTF-8 form), NUL, U+FFFE, U+FFFF and control
599
+ characters other than line feed (`\n`) are rejected with an `InvalidConfigurationError` that names the
600
+ field. Use `\n` for line breaks.
601
+
602
+ Why these containers:
603
+
604
+ - **JPEG: XMP plus COM.** The JPEG already carries one XMP packet in APP1 (the print plan). XMP allows only
605
+ one main packet per JPEG, of at most 65 502 bytes (XMP Part 3 §1.1.3, §1.1.3.1), so the fields go into
606
+ that packet as a second `rdf:Description` of the same resource, which XMP allows (Part 1 §7.4). XMP is the
607
+ smallest standard JPEG container with labelled, UTF-8 fields (Part 1 §8.3–8.4). A COM segment (ITU-T T.81
608
+ §B.2.4.5) has no field names or declared character set, so it carries only the comment, in UTF-8. Exif tags are not used: Exif ASCII fields are 7-bit, and
609
+ rewriting the Exif block would move every offset in it. The existing Exif Software tag keeps naming the
610
+ engine's JPEG writer.
611
+ - **PNG: tEXt or iTXt.** One chunk per field with the predefined keywords (PNG Third Edition §11.3.3.1).
612
+ `tEXt` holds printable Latin-1 and line feed (§11.3.3.2). Text outside that range goes into an
613
+ uncompressed `iTXt` (§11.3.3.4; UTF-8, no language tag). The specification prefers iTXt for new text
614
+ (§12.9); `tEXt` is kept for Latin-1 text because every PNG reader supports it. The chunks go before the
615
+ first IDAT, as §12.9 recommends for short text.
616
+ - **DNG: TIFF tags plus XMP.** TIFF ASCII fields hold only 7-bit codes (TIFF 6.0 §2), so the
617
+ description and copyright reach ImageDescription and Copyright only when they are printable ASCII.
618
+ An XMP packet in tag 700 (XMP Part 3 §1.1.6, Table 13) always carries every field in UTF-8. Its
619
+ `dc:description` repeats the full ImageDescription, statement first, because XMP maps tag 270 to
620
+ `dc:description` (Part 3 §3.2.3). The Software tag keeps naming the engine's DNG writer.
621
+
622
+ Pillow 12.3 and ImageMagick 6.9 read the PNG text chunks and the JPEG comment, and Python's XML parser
623
+ reads both XMP packets.
624
+
485
625
  ## Demosaic
486
626
 
487
627
  `output.demosaic` selects:
@@ -1022,6 +1162,11 @@ provided, for rights reasons.
1022
1162
  free at <https://www.w3.org/TR/css-color-4/>; ITU-R BT.709-6 and BT.2020-2 (primaries and luminance
1023
1163
  coefficients), free at <https://www.itu.int/rec/R-REC-BT>; ITU-T H.273 (cICP code points); W3C PNG Specification, Third
1024
1164
  Edition (2025).
1165
+ - Text metadata (2.4), all free: ITU-T T.81 (1992) | ISO/IEC 10918-1, §B.2.4.5 comment segment, at
1166
+ <https://www.w3.org/Graphics/JPEG/itu-t81.pdf>; Adobe XMP Specification Part 1 (2012, the text of
1167
+ ISO 16684-1:2011) §7.4, §8.3, §8.4, Part 2 (2022) §3.4 and Part 3 (2020) §1.1.3, §1.1.6, §3.2.3, at
1168
+ <https://github.com/adobe/XMP-Toolkit-SDK/tree/main/docs>; W3C PNG Specification, Third Edition (2025)
1169
+ §11.3.3 and §12.9, at <https://www.w3.org/TR/png-3/>; TIFF Revision 6.0 (1992) §2.
1025
1170
  - Adobe DNG Specification 1.7.1.0 (2023); W3C CSS Color Module Level 4 §14 (free). Also see: J. Morovič,
1026
1171
  *Color Gamut Mapping*, Wiley 2008, §10.
1027
1172
  - K. Miyamoto, "Fish eye lens", *JOSA* 54, 1964; J. Kannala, S. Brandt, *IEEE TPAMI* 28, 2006
@@ -80,5 +80,7 @@ export interface SerializeNativeRawDngInput {
80
80
  baselineExposureEv?: number;
81
81
  /** Optional UTC time of the capture, exactly YYYY-MM-DDTHH:mm:ss.sssZ. If omitted, no date tags are written; the engine never makes up a clock time. */
82
82
  capturedAtUtc?: string;
83
+ /** Optional (2.4) caller text: description, software, copyright, comment (see the module notes). If omitted, no text is added. */
84
+ textMetadata?: OutputTextMetadata;
83
85
  }
84
86
  ```
@@ -11,5 +11,5 @@ version they were made with. Proof-of-concept (POC), schema and model contracts
11
11
  own versions. Release checks confirm it matches package.json and the packed package.
12
12
 
13
13
  ```ts
14
- ENGINE_API_VERSION = "2.3.0" as const
14
+ ENGINE_API_VERSION = "2.5.0" as const
15
15
  ```
@@ -1,6 +1,6 @@
1
1
  {
2
- "packageVersion": "2.3.0",
3
- "engineApiVersion": "2.3.0",
2
+ "packageVersion": "2.5.0",
3
+ "engineApiVersion": "2.5.0",
4
4
  "exports": [
5
5
  {
6
6
  "name": "AbsoluteUncertaintyEstimate",
@@ -437,6 +437,16 @@
437
437
  "source": "src/output/crop.ts",
438
438
  "kind": "type"
439
439
  },
440
+ {
441
+ "name": "calculateChartFraming",
442
+ "source": "src/facade/subject-framing.ts",
443
+ "kind": "runtime"
444
+ },
445
+ {
446
+ "name": "CalculateChartFramingInput",
447
+ "source": "src/facade/subject-framing.ts",
448
+ "kind": "type"
449
+ },
440
450
  {
441
451
  "name": "calculateChartRadianceBatch",
442
452
  "source": "src/facade/chart-radiance-batch.ts",
@@ -1842,6 +1852,11 @@
1842
1852
  "source": "src/facade/chart-scenes.ts",
1843
1853
  "kind": "type"
1844
1854
  },
1855
+ {
1856
+ "name": "ChartFraming",
1857
+ "source": "src/facade/subject-framing.ts",
1858
+ "kind": "type"
1859
+ },
1845
1860
  {
1846
1861
  "name": "ChartRadianceBatchInput",
1847
1862
  "source": "src/facade/chart-radiance-batch.ts",
@@ -4612,6 +4627,11 @@
4612
4627
  "source": "src/output/capture-geometry.ts",
4613
4628
  "kind": "type"
4614
4629
  },
4630
+ {
4631
+ "name": "OutputTextMetadata",
4632
+ "source": "src/output/text-metadata.ts",
4633
+ "kind": "type"
4634
+ },
4615
4635
  {
4616
4636
  "name": "PACKED_SENSOR_RAW_LIMITS",
4617
4637
  "source": "src/capture/packed-sensor-raw.ts",
@@ -266,6 +266,12 @@ export interface SimulateCaptureOptions {
266
266
  borderHandling?: SimulateCaptureBorderHandling;
267
267
  /** DNG only: optional capture-event time (YYYY-MM-DDTHH:mm:ss.sssZ); without it no date tags are written. */
268
268
  dng?: { capturedAtUtc?: string };
269
+ /**
270
+ * Opt-in (2.4) text written into the file for formats "jpeg", "png" and "dng": the caller's description,
271
+ * software, copyright and comment, unchanged (JPEG XMP + COM, PNG tEXt/iTXt, DNG TIFF tags + XMP; see the
272
+ * quick start). Each field is 1–2048 UTF-8 bytes with no control characters except line feed. Default: none.
273
+ */
274
+ metadata?: OutputTextMetadata;
269
275
  /**
270
276
  * Opt-in large images: the most native sensor sites (the output plus the demosaic margin on each side) the
271
277
  * capture may allocate, an integer up to `NATIVE_RAW_LIMITS.ceilingPixels` (2^27). With it, `width` and
@@ -286,7 +292,11 @@ export interface SimulateCaptureOptions {
286
292
  /** Camera motion and shutter type. Default: still camera, global electronic shutter. */
287
293
  motion?: SimulateCaptureMotion;
288
294
  sensor?: {
289
- /** Pixel pitch in micrometres. Default 6. */
295
+ /**
296
+ * Pixel pitch in micrometres, 0.5–1000. Default 6. The simulated sensor is output width × pitch wide. 2.5: above 100 µm the
297
+ * capture is a downsized view of a whole frame (for example 36 mm / 48 pixels = 750 µm), recorded in the assumption
298
+ * `sensor.wholeFrameView`: brightness is unchanged and the noise is that of a binned view, not of the real sensor's pixels.
299
+ */
290
300
  pixelPitchMicrometers?: number;
291
301
  /** Maximum spectral quadrature subinterval in nm. Default 20. */
292
302
  spectralStepNanometers?: number;
@@ -2,6 +2,39 @@
2
2
 
3
3
  The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
4
 
5
+ ## calculateChartFraming
6
+
7
+ How much of the frame a chart of a given physical size covers at any subject distance (2.5): magnification, framed plane size,
8
+ coverage fractions (above 1 when cropped), the chart's image size and the minimum-focus check. The same thin-lens framing as
9
+ `simulateCapture` (`scene.framing`, `result.scene.chart`) and `calculateSubjectDistanceForFraming`.
10
+
11
+ ```ts
12
+ export function calculateChartFraming(input: CalculateChartFramingInput): ChartFraming;
13
+ ```
14
+
15
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
16
+
17
+ ## CalculateChartFramingInput
18
+
19
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
20
+
21
+ ```ts
22
+ export interface CalculateChartFramingInput {
23
+ /** Active sensor (or simulated frame) size, mm: for `simulateCapture`, output width × height × pixel pitch. */
24
+ sensor: { widthMm: number; heightMm: number };
25
+ /** Physical focal length, mm. Give exactly one of this and `equivalentFocalLengthMm`. */
26
+ focalLengthMm?: number;
27
+ /** Full-frame-equivalent focal length (diagonal crop factor); a generic prime's value also reports that lens (as `lens.prime`). */
28
+ equivalentFocalLengthMm?: number;
29
+ /** Chart size as for `simulateCapture` `chartSize`: heightM defaults to widthM × the sensor aspect (height / width). */
30
+ chart: { widthM: number; heightM?: number };
31
+ /** Distance from the lens to the chart plane, metres; more than the focal length. */
32
+ subjectDistanceM: number;
33
+ /** Focus distance, metres (as `lens.focusDistanceM`); default the subject distance. More than the focal length. */
34
+ focusDistanceM?: number;
35
+ }
36
+ ```
37
+
5
38
  ## calculateSubjectDistanceForFraming
6
39
 
7
40
  The subject distance that fits a chart of a given physical size to the frame, with magnification, coverage and the minimum-focus check (2.3).
@@ -30,6 +63,40 @@ export interface CalculateSubjectDistanceForFramingInput {
30
63
  }
31
64
  ```
32
65
 
66
+ ## ChartFraming
67
+
68
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
69
+
70
+ ```ts
71
+ export interface ChartFraming {
72
+ subjectDistanceM: number;
73
+ focusDistanceM: number;
74
+ /** Thin-lens image distance at the focus distance, f·s/(s − f), mm. */
75
+ imageDistanceMm: number;
76
+ /** Image size ÷ object size on the chart plane, v/d: f/(d − f) when focused on the chart. */
77
+ magnification: number;
78
+ focalLengthMm: number;
79
+ equivalentFocalLengthMm: number;
80
+ cropFactor35Mm: number;
81
+ chart: { widthM: number; heightM: number };
82
+ /** Plane area the frame covers at the subject distance, metres: sensor side ÷ magnification. */
83
+ framedWidthM: number;
84
+ framedHeightM: number;
85
+ /** Fraction of the frame width and height the chart covers; above 1 means the chart is larger than the frame and cropped. */
86
+ coverage: { frameWidthFraction: number; frameHeightFraction: number };
87
+ /** The chart's image on the sensor, mm. */
88
+ chartImageMm: { width: number; height: number };
89
+ /** The generic prime of this equivalent focal length, when there is one. */
90
+ prime: GenericPrimeLens | null;
91
+ /** The generic prime rule max(0.10 m, f·(1 + 1/0.15)) at this focal length (the prime's own value when there is one). */
92
+ minimumFocusDistanceM: number;
93
+ /** True when the subject or focus distance is closer than the minimum focus distance (`simulateCapture` with `lens.prime` rejects it). */
94
+ belowMinimumFocus: boolean;
95
+ scientificStatus: "approximation";
96
+ assumptions: readonly SimulateCaptureAssumption[];
97
+ }
98
+ ```
99
+
33
100
  ## SubjectFraming
34
101
 
35
102
  The exact contract is declared below; use the domain guide for assumptions and staged integration.
@@ -71,7 +71,14 @@ export interface DiffractionLimitedPsfProfile {
71
71
  wavelengths: readonly DiffractionLimitedPsfWavelength[];
72
72
  /** Largest truncated energy over the wavelengths. */
73
73
  maximumTruncatedEnergyFraction: number;
74
- quadrature: { method: "polar-frequency-domain-trapezoid-gauss-legendre"; maximumRadialNodes: number; maximumAngularNodes: number };
74
+ /**
75
+ * How the cells were integrated. 2.5: wavelengths with β = p·f_c ≥ 192 (cells far larger than the pattern) use the real-space
76
+ * encircled-energy method; `realSpaceWavelengthsNm` lists them, and `method` is `"frequency-domain-and-real-space"` when both ran.
77
+ * `maximumRadialNodes` counts frequency-domain radial nodes (0 when only the real-space method ran); `maximumAngularNodes` the
78
+ * angular nodes of either method (for the real-space method, all θ nodes of one wavelength).
79
+ */
80
+ quadrature: { method: "polar-frequency-domain-trapezoid-gauss-legendre" | "real-space-encircled-energy-gauss-legendre" | "frequency-domain-and-real-space";
81
+ maximumRadialNodes: number; maximumAngularNodes: number; realSpaceWavelengthsNm?: readonly number[] };
75
82
  }
76
83
  ```
77
84
 
@@ -0,0 +1,20 @@
1
+ # output/text-metadata.ts public contracts
2
+
3
+ The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
+
5
+ ## OutputTextMetadata
6
+
7
+ Caller-supplied text written into JPEG, PNG and DNG outputs. Every field is optional; at least one must be set.
8
+
9
+ ```ts
10
+ export interface OutputTextMetadata {
11
+ /** What the image shows. JPEG/DNG `dc:description`, PNG Description, DNG ImageDescription (appended when ASCII). */
12
+ description?: string;
13
+ /** Name and version of the caller's software. JPEG/DNG `xmp:CreatorTool`, PNG Software. */
14
+ software?: string;
15
+ /** Rights statement. JPEG/DNG `dc:rights`; the PNG keyword and DNG tag 33432 (when ASCII) of the same name. */
16
+ copyright?: string;
17
+ /** Free-text comment. JPEG COM segment and `exif:UserComment`, PNG Comment, DNG `exif:UserComment`. */
18
+ comment?: string;
19
+ }
20
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@photivra/engine",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "private": false,
5
5
  "description": "Scientific TypeScript engine for camera optics, exposure, focus, motion, sensor sampling, diffraction, and digital imaging calculations.",
6
6
  "type": "module",