@photivra/engine 2.3.0 → 2.4.1
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 +23 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/capture/native-raw-dng.d.ts +3 -0
- package/dist/capture/native-raw-dng.js +23 -3
- package/dist/capture/native-raw-dng.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/facade/simulate-capture.d.ts +7 -0
- package/dist/facade/simulate-capture.js +30 -3
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/output/text-metadata.d.ts +32 -0
- package/dist/output/text-metadata.js +201 -0
- package/dist/output/text-metadata.js.map +1 -0
- package/docs/API_REFERENCE.md +9 -1
- package/docs/QUICK_START.md +61 -1
- package/docs/api/capture-native-raw-dng.md +2 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +7 -2
- package/docs/api/facade-simulate-capture.md +6 -0
- package/docs/api/output-text-metadata.md +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Optional caller-supplied text metadata for encoded images (2.4): a description, the creating software, a
|
|
4
|
+
* copyright notice and a comment. The engine writes the caller's text unchanged and adds no text of its own
|
|
5
|
+
* (in a DNG the description follows the existing simulated-RAW statement).
|
|
6
|
+
* Independently written from these free specifications; no third-party source was used:
|
|
7
|
+
*
|
|
8
|
+
* - JPEG: the existing XMP packet in APP1 (Adobe XMP Specification Part 3, 2020, §1.1.3: identifier
|
|
9
|
+
* "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
|
|
10
|
+
* as one more top-level `rdf:Description` of the same resource (`rdf:about=""`): XMP Part 1 (2012, identical
|
|
11
|
+
* to ISO 16684-1:2011) §7.4 lets properties be "arbitrarily apportioned among the rdf:Description elements"
|
|
12
|
+
* of the single `rdf:RDF`. Mapping: `dc:description` and `dc:rights` as language alternatives with
|
|
13
|
+
* `x-default` (Part 1 §8.3), `xmp:CreatorTool` as text (Part 1 §8.4) and `exif:UserComment` as a language
|
|
14
|
+
* alternative (Part 2, 2022, §3.4, the Exif namespace). The comment is also a COM marker segment (ITU-T T.81
|
|
15
|
+
* §B.2.4.5, "the interpretation is left to the application"; written as UTF-8) after the APPn segments.
|
|
16
|
+
* - PNG: one text chunk per field with the predefined keywords Description, Software, Copyright and Comment
|
|
17
|
+
* (W3C PNG Specification, Third Edition, §11.3.3.1, Table 21). `tEXt` (§11.3.3.2; printable Latin-1 plus line
|
|
18
|
+
* feed) when every character fits, otherwise an uncompressed `iTXt` (§11.3.3.4; UTF-8, empty language tag and
|
|
19
|
+
* translated keyword). The chunks go before the first IDAT, as §12.9 recommends for small text.
|
|
20
|
+
* - DNG: `serializeNativeRawDng` writes the same XMP fields as a packet in TIFF tag 700 (XMP Part 3 §1.1.6,
|
|
21
|
+
* Table 13) plus the TIFF ASCII tags that XMP Part 3 §3.2.3 maps to them.
|
|
22
|
+
*
|
|
23
|
+
* Validation: each given field is a non-empty string of at most `OUTPUT_TEXT_METADATA_MAXIMUM_BYTES` UTF-8
|
|
24
|
+
* bytes, with no unpaired UTF-16 surrogate (so it encodes to valid UTF-8 without replacement), no NUL or other
|
|
25
|
+
* C0/C1 control character except line feed, and no U+FFFE/U+FFFF (not XML 1.0 characters). Line breaks are a
|
|
26
|
+
* single line feed, as the PNG text chunks require.
|
|
27
|
+
* @see docs/QUICK_START.md "Text metadata (2.4)".
|
|
28
|
+
*/
|
|
29
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
30
|
+
import { pngCrc32 } from "./png-encoder.js";
|
|
31
|
+
/** Largest UTF-8 length of one metadata field, in bytes. Four escaped fields still fit one JPEG APP1 segment. */
|
|
32
|
+
export const OUTPUT_TEXT_METADATA_MAXIMUM_BYTES = 2048;
|
|
33
|
+
const FIELDS = ["description", "software", "copyright", "comment"];
|
|
34
|
+
/** PNG Third Edition §11.3.3.1 Table 21 keyword for each field: the field name with an initial capital. */
|
|
35
|
+
const pngKeyword = (field) => field[0].toUpperCase() + field.slice(1);
|
|
36
|
+
const XMP_IDENTIFIER = "http://ns.adobe.com/xap/1.0/\0";
|
|
37
|
+
/** Largest XMP packet in one JPEG APP1 segment (XMP Part 3 §1.1.3.1). */
|
|
38
|
+
const JPEG_XMP_MAXIMUM_PACKET_BYTES = 65502;
|
|
39
|
+
const encoder = new TextEncoder();
|
|
40
|
+
/**
|
|
41
|
+
* Checks caller text metadata. Returns undefined when `value` is undefined, otherwise a frozen copy with only
|
|
42
|
+
* the given fields. `path` prefixes the error messages, for example "output.metadata".
|
|
43
|
+
*/
|
|
44
|
+
export function parseOutputTextMetadata(value, path) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
48
|
+
throw new InvalidConfigurationError(path + " must be an object.");
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const key of Object.keys(value))
|
|
51
|
+
if (!FIELDS.includes(key))
|
|
52
|
+
throw new InvalidConfigurationError("Unknown " + path + " field: " + key + ".");
|
|
53
|
+
for (const field of FIELDS) {
|
|
54
|
+
const text = value[field];
|
|
55
|
+
if (text === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
const name = path + "." + field;
|
|
58
|
+
if (typeof text !== "string" || text.length === 0)
|
|
59
|
+
throw new InvalidConfigurationError(name + " must be a non-empty string.");
|
|
60
|
+
if (/\p{Cs}/u.test(text))
|
|
61
|
+
throw new InvalidConfigurationError(name + " must be valid Unicode text (it has an unpaired UTF-16 surrogate).");
|
|
62
|
+
if (text.includes("\0"))
|
|
63
|
+
throw new InvalidConfigurationError(name + " must not contain NUL characters.");
|
|
64
|
+
// eslint-disable-next-line no-control-regex
|
|
65
|
+
if (/[\u0001-\u0009\u000b-\u001f\u007f-\u009f]/u.test(text))
|
|
66
|
+
throw new InvalidConfigurationError(name + " must not contain control characters other than line feed (\\n), or U+FFFE/U+FFFF.");
|
|
67
|
+
const bytes = encoder.encode(text).length;
|
|
68
|
+
if (bytes > OUTPUT_TEXT_METADATA_MAXIMUM_BYTES)
|
|
69
|
+
throw new InvalidConfigurationError(name + " must be at most " + OUTPUT_TEXT_METADATA_MAXIMUM_BYTES + " UTF-8 bytes (it has " + bytes + ").");
|
|
70
|
+
out[field] = text;
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(out).length === 0)
|
|
73
|
+
throw new InvalidConfigurationError(path + " must set at least one of description, software, copyright or comment.");
|
|
74
|
+
return Object.freeze(out);
|
|
75
|
+
}
|
|
76
|
+
function xmlText(text) {
|
|
77
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
78
|
+
}
|
|
79
|
+
const alt = (text) => '<rdf:Alt><rdf:li xml:lang="x-default">' + xmlText(text) + "</rdf:li></rdf:Alt>";
|
|
80
|
+
/** One `rdf:Description` of the current resource with the given fields, declaring only the namespaces it uses. */
|
|
81
|
+
export function xmpTextMetadataDescription(metadata) {
|
|
82
|
+
const dc = metadata.description !== undefined || metadata.copyright !== undefined;
|
|
83
|
+
return '<rdf:Description rdf:about=""' + (dc ? ' xmlns:dc="http://purl.org/dc/elements/1.1/"' : "") +
|
|
84
|
+
(metadata.software !== undefined ? ' xmlns:xmp="http://ns.adobe.com/xap/1.0/"' : "") +
|
|
85
|
+
(metadata.comment !== undefined ? ' xmlns:exif="http://ns.adobe.com/exif/1.0/"' : "") + ">" +
|
|
86
|
+
(metadata.description !== undefined ? "<dc:description>" + alt(metadata.description) + "</dc:description>" : "") +
|
|
87
|
+
(metadata.copyright !== undefined ? "<dc:rights>" + alt(metadata.copyright) + "</dc:rights>" : "") +
|
|
88
|
+
(metadata.software !== undefined ? "<xmp:CreatorTool>" + xmlText(metadata.software) + "</xmp:CreatorTool>" : "") +
|
|
89
|
+
(metadata.comment !== undefined ? "<exif:UserComment>" + alt(metadata.comment) + "</exif:UserComment>" : "") + "</rdf:Description>";
|
|
90
|
+
}
|
|
91
|
+
/** A complete XMP packet (`x:xmpmeta` wrapper, no `xpacket` processing instructions) holding only the text fields. */
|
|
92
|
+
export function xmpTextMetadataPacket(metadata) {
|
|
93
|
+
return encoder.encode('<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' +
|
|
94
|
+
xmpTextMetadataDescription(metadata) + "</rdf:RDF></x:xmpmeta>");
|
|
95
|
+
}
|
|
96
|
+
function concat(parts) {
|
|
97
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
98
|
+
let offset = 0;
|
|
99
|
+
for (const part of parts) {
|
|
100
|
+
out.set(part, offset);
|
|
101
|
+
offset += part.length;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function segment(marker, payload) {
|
|
106
|
+
return concat([Uint8Array.of(0xff, marker, (payload.length + 2) >> 8, (payload.length + 2) & 255), payload]);
|
|
107
|
+
}
|
|
108
|
+
function startsWith(bytes, offset, prefix) {
|
|
109
|
+
if (offset + prefix.length > bytes.length)
|
|
110
|
+
return false;
|
|
111
|
+
for (let i = 0; i < prefix.length; i++)
|
|
112
|
+
if (bytes[offset + i] !== prefix[i])
|
|
113
|
+
return false;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns a copy of a baseline JPEG with the text fields added: merged into its XMP APP1 packet (or a new packet
|
|
118
|
+
* 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.
|
|
120
|
+
*/
|
|
121
|
+
export function addJpegTextMetadata(jpeg, metadata) {
|
|
122
|
+
if (jpeg.length < 4 || jpeg[0] !== 0xff || jpeg[1] !== 0xd8)
|
|
123
|
+
throw new InvalidConfigurationError("JPEG text metadata needs a JPEG stream starting with SOI.");
|
|
124
|
+
const identifier = encoder.encode(XMP_IDENTIFIER);
|
|
125
|
+
let offset = 2, xmp, appEnd = 2, inLeadingApps = true;
|
|
126
|
+
for (;;) {
|
|
127
|
+
if (offset + 4 > jpeg.length || jpeg[offset] !== 0xff)
|
|
128
|
+
throw new InvalidConfigurationError("JPEG text metadata found a malformed marker segment.");
|
|
129
|
+
const marker = jpeg[offset + 1];
|
|
130
|
+
if (marker === 0xda)
|
|
131
|
+
break;
|
|
132
|
+
const end = offset + 2 + ((jpeg[offset + 2] << 8) | jpeg[offset + 3]);
|
|
133
|
+
if (end > jpeg.length)
|
|
134
|
+
throw new InvalidConfigurationError("JPEG text metadata found a truncated marker segment.");
|
|
135
|
+
if (marker === 0xe1 && !xmp && startsWith(jpeg, offset + 4, identifier))
|
|
136
|
+
xmp = { start: offset, end };
|
|
137
|
+
if (inLeadingApps && marker >= 0xe0 && marker <= 0xef)
|
|
138
|
+
appEnd = end;
|
|
139
|
+
else
|
|
140
|
+
inLeadingApps = false;
|
|
141
|
+
offset = end;
|
|
142
|
+
}
|
|
143
|
+
const description = xmpTextMetadataDescription(metadata);
|
|
144
|
+
let xmpPacket;
|
|
145
|
+
if (xmp) {
|
|
146
|
+
const packet = new TextDecoder("utf-8", { fatal: true }).decode(jpeg.subarray(xmp.start + 4 + identifier.length, xmp.end));
|
|
147
|
+
const close = packet.lastIndexOf("</rdf:RDF>");
|
|
148
|
+
if (close < 0)
|
|
149
|
+
throw new InvalidConfigurationError("JPEG text metadata found an XMP packet without rdf:RDF.");
|
|
150
|
+
xmpPacket = encoder.encode(packet.slice(0, close) + description + packet.slice(close));
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
xmpPacket = xmpTextMetadataPacket(metadata);
|
|
154
|
+
if (xmpPacket.length > JPEG_XMP_MAXIMUM_PACKET_BYTES)
|
|
155
|
+
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
|
+
const xmpSegment = segment(0xe1, concat([identifier, xmpPacket]));
|
|
157
|
+
const com = metadata.comment === undefined ? new Uint8Array(0) : segment(0xfe, encoder.encode(metadata.comment));
|
|
158
|
+
if (xmp)
|
|
159
|
+
return concat([jpeg.subarray(0, xmp.start), xmpSegment, jpeg.subarray(xmp.end, appEnd), com, jpeg.subarray(appEnd)]);
|
|
160
|
+
return concat([jpeg.subarray(0, appEnd), xmpSegment, com, jpeg.subarray(appEnd)]);
|
|
161
|
+
}
|
|
162
|
+
function latin1Text(text) {
|
|
163
|
+
for (const ch of text) {
|
|
164
|
+
const c = ch.codePointAt(0);
|
|
165
|
+
if (!(c === 10 || (c >= 0x20 && c <= 0x7e) || (c >= 0xa1 && c <= 0xff)))
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
function pngChunk(type, data) {
|
|
171
|
+
const out = new Uint8Array(12 + data.length), view = new DataView(out.buffer), typeBytes = Uint8Array.from(type, (c) => c.charCodeAt(0));
|
|
172
|
+
view.setUint32(0, data.length);
|
|
173
|
+
out.set(typeBytes, 4);
|
|
174
|
+
out.set(data, 8);
|
|
175
|
+
view.setUint32(8 + data.length, pngCrc32(data, pngCrc32(typeBytes)));
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/** The text chunks for the given fields, in the order description, software, copyright, comment. */
|
|
179
|
+
export function pngTextMetadataChunks(metadata) {
|
|
180
|
+
return FIELDS.filter((field) => metadata[field] !== undefined).map((field) => {
|
|
181
|
+
const keyword = Uint8Array.from(pngKeyword(field), (c) => c.charCodeAt(0)), text = metadata[field];
|
|
182
|
+
// 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.
|
|
183
|
+
return latin1Text(text) ? pngChunk("tEXt", concat([keyword, Uint8Array.of(0), Uint8Array.from(text, (c) => c.charCodeAt(0))]))
|
|
184
|
+
: pngChunk("iTXt", concat([keyword, Uint8Array.of(0, 0, 0, 0, 0), encoder.encode(text)]));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** Returns a copy of a PNG with one text chunk per field inserted before the first IDAT chunk. Other chunks are unchanged. */
|
|
188
|
+
export function addPngTextMetadata(png, metadata) {
|
|
189
|
+
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
190
|
+
if (png.length < 8 || signature.some((b, i) => png[i] !== b))
|
|
191
|
+
throw new InvalidConfigurationError("PNG text metadata needs a PNG signature.");
|
|
192
|
+
const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
|
|
193
|
+
for (let offset = 8; offset + 12 <= png.length;) {
|
|
194
|
+
const length = view.getUint32(offset), type = String.fromCharCode(...png.subarray(offset + 4, offset + 8));
|
|
195
|
+
if (type === "IDAT")
|
|
196
|
+
return concat([png.subarray(0, offset), ...pngTextMetadataChunks(metadata), png.subarray(offset)]);
|
|
197
|
+
offset += 12 + length;
|
|
198
|
+
}
|
|
199
|
+
throw new InvalidConfigurationError("PNG text metadata found no IDAT chunk.");
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=text-metadata.js.map
|
|
@@ -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(\"&\", \"&\").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"]}
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Public API reference
|
|
2
2
|
|
|
3
|
-
Generated from the exact root exports for **@photivra/engine 2.
|
|
3
|
+
Generated from the exact root exports for **@photivra/engine 2.4.1 / root contract 2.4.1** 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
|
|
|
@@ -2077,6 +2077,14 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
2077
2077
|
| [`CalculateSubjectFramingCropInput`](api/output-subject-framing-crop.md#calculatesubjectframingcropinput) | Type |
|
|
2078
2078
|
| [`SubjectFramingCrop`](api/output-subject-framing-crop.md#subjectframingcrop) | Type |
|
|
2079
2079
|
|
|
2080
|
+
## output/text-metadata.ts
|
|
2081
|
+
|
|
2082
|
+
[Detailed contracts](api/output-text-metadata.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/output/text-metadata.ts)
|
|
2083
|
+
|
|
2084
|
+
| Export | Kind |
|
|
2085
|
+
| --- | --- |
|
|
2086
|
+
| [`OutputTextMetadata`](api/output-text-metadata.md#outputtextmetadata) | Type |
|
|
2087
|
+
|
|
2080
2088
|
## processing/demosaic.ts
|
|
2081
2089
|
|
|
2082
2090
|
[Detailed contracts](api/processing-demosaic.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/processing/demosaic.ts)
|
package/docs/QUICK_START.md
CHANGED
|
@@ -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"`. |
|
|
@@ -463,7 +464,8 @@ The DNG states only what the capture has:
|
|
|
463
464
|
| ColorMatrix1 (D65), ForwardMatrix1, AnalogBalance, AsShotNeutral | The facade's diagonal camera→XYZ profile and the capture's white-balance intent |
|
|
464
465
|
| BaselineExposure | 0: the RAW white level maps to display white, as in the facade's development |
|
|
465
466
|
| 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 |
|
|
467
|
+
| 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 |
|
|
468
|
+
| Copyright, XMP (tag 700) | Only with `output.metadata` (2.4); see [Text metadata](#text-metadata-24) |
|
|
467
469
|
|
|
468
470
|
- Saturation flags of sites whose code is below WhiteLevel have no DNG field. They are counted in the
|
|
469
471
|
assumptions.
|
|
@@ -482,6 +484,59 @@ const dng = parseDngRawImage({ bytes: shot.image.dng! });
|
|
|
482
484
|
console.log(dng.width, dng.activeArea, dng.whiteLevel, dng.codes.length === shot.raw.codes.length);
|
|
483
485
|
```
|
|
484
486
|
|
|
487
|
+
### Text metadata (2.4)
|
|
488
|
+
|
|
489
|
+
`output.metadata` writes the caller's own text into the file. The engine writes the text exactly as
|
|
490
|
+
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
|
|
491
|
+
`simulateIdealReference`); other formats reject it. Without it, every file is byte-identical to 2.3.0
|
|
492
|
+
(hash locks in `test/output-text-metadata.test.ts`).
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
import { simulateCapture } from "@photivra/engine";
|
|
496
|
+
|
|
497
|
+
const shot = await simulateCapture({
|
|
498
|
+
scene: { preset: "siemens-star" },
|
|
499
|
+
output: { width: 8, height: 8, format: "png",
|
|
500
|
+
metadata: { description: "Siemens star at f/8", software: "My Viewer 1.0", copyright: "Rights held by the photographer", comment: "Test run 3" } }
|
|
501
|
+
});
|
|
502
|
+
console.log(shot.image.png?.length, shot.assumptions.find((a) => a.id === "output.metadata")?.value);
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
| Field | JPEG | PNG | DNG |
|
|
506
|
+
| --- | --- | --- | --- |
|
|
507
|
+
| `description` | XMP `dc:description` | `Description` | ImageDescription after the simulated-RAW statement (ASCII only); XMP `dc:description` (statement, then the text) |
|
|
508
|
+
| `software` | XMP `xmp:CreatorTool` | `Software` | XMP `xmp:CreatorTool` |
|
|
509
|
+
| `copyright` | XMP `dc:rights` | `Copyright` | Copyright tag (ASCII only); XMP `dc:rights` |
|
|
510
|
+
| `comment` | COM segment and XMP `exif:UserComment` | `Comment` | XMP `exif:UserComment` |
|
|
511
|
+
|
|
512
|
+
Each field is optional, but at least one must be set. A field is a non-empty string of at most
|
|
513
|
+
2048 UTF-8 bytes. Lone UTF-16 surrogates (which have no UTF-8 form), NUL, U+FFFE, U+FFFF and control
|
|
514
|
+
characters other than line feed (`\n`) are rejected with an `InvalidConfigurationError` that names the
|
|
515
|
+
field. Use `\n` for line breaks.
|
|
516
|
+
|
|
517
|
+
Why these containers:
|
|
518
|
+
|
|
519
|
+
- **JPEG: XMP plus COM.** The JPEG already carries one XMP packet in APP1 (the print plan). XMP allows only
|
|
520
|
+
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
|
|
521
|
+
that packet as a second `rdf:Description` of the same resource, which XMP allows (Part 1 §7.4). XMP is the
|
|
522
|
+
smallest standard JPEG container with labelled, UTF-8 fields (Part 1 §8.3–8.4). A COM segment (ITU-T T.81
|
|
523
|
+
§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
|
|
524
|
+
rewriting the Exif block would move every offset in it. The existing Exif Software tag keeps naming the
|
|
525
|
+
engine's JPEG writer.
|
|
526
|
+
- **PNG: tEXt or iTXt.** One chunk per field with the predefined keywords (PNG Third Edition §11.3.3.1).
|
|
527
|
+
`tEXt` holds printable Latin-1 and line feed (§11.3.3.2). Text outside that range goes into an
|
|
528
|
+
uncompressed `iTXt` (§11.3.3.4; UTF-8, no language tag). The specification prefers iTXt for new text
|
|
529
|
+
(§12.9); `tEXt` is kept for Latin-1 text because every PNG reader supports it. The chunks go before the
|
|
530
|
+
first IDAT, as §12.9 recommends for short text.
|
|
531
|
+
- **DNG: TIFF tags plus XMP.** TIFF ASCII fields hold only 7-bit codes (TIFF 6.0 §2), so the
|
|
532
|
+
description and copyright reach ImageDescription and Copyright only when they are printable ASCII.
|
|
533
|
+
An XMP packet in tag 700 (XMP Part 3 §1.1.6, Table 13) always carries every field in UTF-8. Its
|
|
534
|
+
`dc:description` repeats the full ImageDescription, statement first, because XMP maps tag 270 to
|
|
535
|
+
`dc:description` (Part 3 §3.2.3). The Software tag keeps naming the engine's DNG writer.
|
|
536
|
+
|
|
537
|
+
Pillow 12.3 and ImageMagick 6.9 read the PNG text chunks and the JPEG comment, and Python's XML parser
|
|
538
|
+
reads both XMP packets.
|
|
539
|
+
|
|
485
540
|
## Demosaic
|
|
486
541
|
|
|
487
542
|
`output.demosaic` selects:
|
|
@@ -1022,6 +1077,11 @@ provided, for rights reasons.
|
|
|
1022
1077
|
free at <https://www.w3.org/TR/css-color-4/>; ITU-R BT.709-6 and BT.2020-2 (primaries and luminance
|
|
1023
1078
|
coefficients), free at <https://www.itu.int/rec/R-REC-BT>; ITU-T H.273 (cICP code points); W3C PNG Specification, Third
|
|
1024
1079
|
Edition (2025).
|
|
1080
|
+
- Text metadata (2.4), all free: ITU-T T.81 (1992) | ISO/IEC 10918-1, §B.2.4.5 comment segment, at
|
|
1081
|
+
<https://www.w3.org/Graphics/JPEG/itu-t81.pdf>; Adobe XMP Specification Part 1 (2012, the text of
|
|
1082
|
+
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
|
|
1083
|
+
<https://github.com/adobe/XMP-Toolkit-SDK/tree/main/docs>; W3C PNG Specification, Third Edition (2025)
|
|
1084
|
+
§11.3.3 and §12.9, at <https://www.w3.org/TR/png-3/>; TIFF Revision 6.0 (1992) §2.
|
|
1025
1085
|
- Adobe DNG Specification 1.7.1.0 (2023); W3C CSS Color Module Level 4 §14 (free). Also see: J. Morovič,
|
|
1026
1086
|
*Color Gamut Mapping*, Wiley 2008, §10.
|
|
1027
1087
|
- 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
|
```
|
package/docs/api/core-version.md
CHANGED
|
@@ -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.
|
|
14
|
+
ENGINE_API_VERSION = "2.4.1" as const
|
|
15
15
|
```
|
package/docs/api/exports.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"packageVersion": "2.
|
|
3
|
-
"engineApiVersion": "2.
|
|
2
|
+
"packageVersion": "2.4.1",
|
|
3
|
+
"engineApiVersion": "2.4.1",
|
|
4
4
|
"exports": [
|
|
5
5
|
{
|
|
6
6
|
"name": "AbsoluteUncertaintyEstimate",
|
|
@@ -4612,6 +4612,11 @@
|
|
|
4612
4612
|
"source": "src/output/capture-geometry.ts",
|
|
4613
4613
|
"kind": "type"
|
|
4614
4614
|
},
|
|
4615
|
+
{
|
|
4616
|
+
"name": "OutputTextMetadata",
|
|
4617
|
+
"source": "src/output/text-metadata.ts",
|
|
4618
|
+
"kind": "type"
|
|
4619
|
+
},
|
|
4615
4620
|
{
|
|
4616
4621
|
"name": "PACKED_SENSOR_RAW_LIMITS",
|
|
4617
4622
|
"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
|
|
@@ -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
|
+
"version": "2.4.1",
|
|
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",
|