@aglyn/aglyn 1.0.0-beta.197 → 1.0.0-beta.199
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/package.json +11 -11
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +1 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +3 -3
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
- package/src/lib/app-utils/media-embedded-fields.js +619 -0
- package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
- package/src/lib/app-utils/name-search.d.ts +12 -0
- package/src/lib/app-utils/name-search.js +11 -0
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/server.d.ts +1 -0
- package/src/server.js +7 -0
- package/src/server.js.map +1 -1
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* JPEG metadata blocks: find them, and replace them without touching a
|
|
18
|
+
* single byte of the image (AGL-3331).
|
|
19
|
+
*
|
|
20
|
+
* A JPEG is a run of marker segments (ITU-T T.81, B.1.1.4): `0xFF`, a marker
|
|
21
|
+
* code, and — for every marker but the standalone ones — a 16-bit length that
|
|
22
|
+
* counts itself. Metadata lives in the APPn and COM segments of the header,
|
|
23
|
+
* all of which precede the first Start of Scan. The walk here stops at that
|
|
24
|
+
* SOS and never looks further: everything from SOS to EOI, and anything after
|
|
25
|
+
* EOI (an MPF preview, a motion photo's video), is copied through verbatim,
|
|
26
|
+
* which is the whole of the lossless guarantee.
|
|
27
|
+
*
|
|
28
|
+
* The blocks, by their identifying prefix:
|
|
29
|
+
*
|
|
30
|
+
* - APP1 `Exif\0\0` — EXIF (Exif 2.32, 4.5.4).
|
|
31
|
+
* - APP1 `http://ns.adobe.com/xap/1.0/\0` — the main XMP packet
|
|
32
|
+
* (XMP Specification Part 3, 1.1.3).
|
|
33
|
+
* - APP1 `http://ns.adobe.com/xmp/extension/\0` — extended XMP. Read by
|
|
34
|
+
* nobody here and never rewritten; its segments pass through untouched.
|
|
35
|
+
* - APP13 `Photoshop 3.0\0` — Photoshop Image Resource Blocks, which carry
|
|
36
|
+
* the IPTC-IIM record. Photoshop splits a large one across consecutive
|
|
37
|
+
* APP13 segments, each repeating the header.
|
|
38
|
+
* - APP2 `ICC_PROFILE\0` — the ICC profile, in numbered chunks
|
|
39
|
+
* (ICC.1:2010, Annex B.4). Read-only.
|
|
40
|
+
* - COM — free-text comments (T.81, B.2.4.5). Read-only.
|
|
41
|
+
*/ import { EmbeddedWriteError } from "./types.js";
|
|
42
|
+
import { FILE_DAMAGED, METADATA_TOO_LARGE, asciiBytes, bytesAt, concatBytes, decodeFreeText, readU16BE, readU32BE, trimNul, utf8Decode, utf8Encode, writeU16BE, writeU32BE } from "./image-blocks.js";
|
|
43
|
+
const SOI = 0xd8;
|
|
44
|
+
const EOI = 0xd9;
|
|
45
|
+
const SOS = 0xda;
|
|
46
|
+
const APP0 = 0xe0;
|
|
47
|
+
const APP1 = 0xe1;
|
|
48
|
+
const APP2 = 0xe2;
|
|
49
|
+
const APP13 = 0xed;
|
|
50
|
+
const COM = 0xfe;
|
|
51
|
+
/** `Exif\0` — the sixth byte is `\0` by the standard and ignored here. */ const EXIF_ID = asciiBytes('Exif\0');
|
|
52
|
+
const EXIF_HEADER = asciiBytes('Exif\0\0');
|
|
53
|
+
const XMP_ID = asciiBytes('http://ns.adobe.com/xap/1.0/\0');
|
|
54
|
+
const XMP_EXTENSION_ID = asciiBytes('http://ns.adobe.com/xmp/extension/\0');
|
|
55
|
+
const PHOTOSHOP_ID = asciiBytes('Photoshop 3.0\0');
|
|
56
|
+
const ICC_ID = asciiBytes('ICC_PROFILE\0');
|
|
57
|
+
const MPF_ID = asciiBytes('MPF\0');
|
|
58
|
+
/** A segment's length field counts itself, so its payload tops out here. */ const MAX_SEGMENT_PAYLOAD = 0xffff - 2;
|
|
59
|
+
/** `Exif\0\0` plus the TIFF structure must fit one APP1 (65535 − 2 − 6). */ const MAX_EXIF_BYTES = MAX_SEGMENT_PAYLOAD - EXIF_HEADER.length;
|
|
60
|
+
/**
|
|
61
|
+
* The standard XMP packet's ceiling (XMP Specification Part 3, 1.1.3.1).
|
|
62
|
+
* Anything larger belongs in extended XMP, which this writer does not
|
|
63
|
+
* produce, so a larger packet is refused rather than truncated.
|
|
64
|
+
*/ const MAX_XMP_BYTES = 65502;
|
|
65
|
+
/** Per-APP13 IRB payload once `Photoshop 3.0\0` is paid for (65535 − 16). */ const MAX_IRB_CHUNK = MAX_SEGMENT_PAYLOAD - PHOTOSHOP_ID.length;
|
|
66
|
+
/**
|
|
67
|
+
* A header past this many segments is not a photo; the walk stops and
|
|
68
|
+
* returns what it read. Real files carry tens — a 4 MB ICC profile split
|
|
69
|
+
* into 64 KB chunks is still under a hundred.
|
|
70
|
+
*/ const MAX_SEGMENTS = 20000;
|
|
71
|
+
/** MPF's `MPEntry` tag (CIPA DC-007, 5.2.3.3). */ const MPF_MP_ENTRY_TAG = 0xb002;
|
|
72
|
+
/**
|
|
73
|
+
* Markers that carry no length field (T.81, B.1.1.3 and Table B.1): TEM and
|
|
74
|
+
* RST0–RST7, plus a stray repeated SOI.
|
|
75
|
+
*/ function isStandalone(code) {
|
|
76
|
+
return code === 0x01 || code >= 0xd0 && code <= 0xd8;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Walks the header's marker segments up to SOS (or EOI). Returns `null` when
|
|
80
|
+
* the bytes do not open with SOI.
|
|
81
|
+
*
|
|
82
|
+
* Between segments it behaves like libjpeg's `next_marker`: bytes that are
|
|
83
|
+
* not `0xFF` are skipped, a run of `0xFF` is fill, and `0xFF 0x00` is not a
|
|
84
|
+
* marker. Every step moves forward, so the loop ends on any input.
|
|
85
|
+
*/ function walkJpeg(bytes) {
|
|
86
|
+
if (bytes.length < 2 || bytes[0] !== 0xff || bytes[1] !== SOI) return null;
|
|
87
|
+
const segments = [];
|
|
88
|
+
let offset = 2;
|
|
89
|
+
while(segments.length < MAX_SEGMENTS){
|
|
90
|
+
const start = offset;
|
|
91
|
+
let marker = -1;
|
|
92
|
+
let code = -1;
|
|
93
|
+
while(offset < bytes.length){
|
|
94
|
+
var _bytes_next;
|
|
95
|
+
if (bytes[offset] !== 0xff) {
|
|
96
|
+
offset += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
let next = offset + 1;
|
|
100
|
+
while(next < bytes.length && bytes[next] === 0xff)next += 1;
|
|
101
|
+
// Only fill bytes up to the end: no marker, so the header is cut.
|
|
102
|
+
if (next >= bytes.length) break;
|
|
103
|
+
const candidate = (_bytes_next = bytes[next]) != null ? _bytes_next : 0;
|
|
104
|
+
if (candidate === 0x00) {
|
|
105
|
+
offset = next + 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
marker = next - 1;
|
|
109
|
+
code = candidate;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (code < 0) return {
|
|
113
|
+
segments,
|
|
114
|
+
tailStart: start,
|
|
115
|
+
complete: false
|
|
116
|
+
};
|
|
117
|
+
if (code === SOS || code === EOI) {
|
|
118
|
+
return {
|
|
119
|
+
segments,
|
|
120
|
+
tailStart: start,
|
|
121
|
+
complete: true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (isStandalone(code)) {
|
|
125
|
+
segments.push({
|
|
126
|
+
start,
|
|
127
|
+
marker,
|
|
128
|
+
code,
|
|
129
|
+
dataStart: marker + 2,
|
|
130
|
+
end: marker + 2
|
|
131
|
+
});
|
|
132
|
+
offset = marker + 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (marker + 4 > bytes.length) {
|
|
136
|
+
return {
|
|
137
|
+
segments,
|
|
138
|
+
tailStart: start,
|
|
139
|
+
complete: false
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const length = readU16BE(bytes, marker + 2);
|
|
143
|
+
const end = marker + 2 + length;
|
|
144
|
+
if (length < 2 || end > bytes.length) {
|
|
145
|
+
return {
|
|
146
|
+
segments,
|
|
147
|
+
tailStart: start,
|
|
148
|
+
complete: false
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
segments.push({
|
|
152
|
+
start,
|
|
153
|
+
marker,
|
|
154
|
+
code,
|
|
155
|
+
dataStart: marker + 4,
|
|
156
|
+
end
|
|
157
|
+
});
|
|
158
|
+
offset = end;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
segments,
|
|
162
|
+
tailStart: offset,
|
|
163
|
+
complete: false
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Which metadata block a segment is, by its marker and identifying prefix. */ function classify(bytes, segment) {
|
|
167
|
+
const { code, dataStart, end } = segment;
|
|
168
|
+
const has = (id)=>end - dataStart >= id.length && bytesAt(bytes, dataStart, id);
|
|
169
|
+
if (code === APP1) {
|
|
170
|
+
if (end - dataStart >= EXIF_HEADER.length && has(EXIF_ID)) return 'exif';
|
|
171
|
+
if (has(XMP_ID)) return 'xmp';
|
|
172
|
+
if (has(XMP_EXTENSION_ID)) return 'xmpExtension';
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
if (code === APP13) return has(PHOTOSHOP_ID) ? 'irb' : null;
|
|
176
|
+
if (code === APP2) {
|
|
177
|
+
if (end - dataStart >= ICC_ID.length + 2 && has(ICC_ID)) return 'icc';
|
|
178
|
+
if (has(MPF_ID)) return 'mpf';
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (code === COM) return 'com';
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Reassembles the ICC profile from its APP2 chunks (ICC.1:2010, B.4): each
|
|
186
|
+
* carries a 1-based sequence number and the chunk count. A set with a gap,
|
|
187
|
+
* a duplicate or disagreeing counts is dropped — half a profile is worse
|
|
188
|
+
* than none — except that a lone chunk is taken as the whole profile.
|
|
189
|
+
*/ function assembleIcc(chunks) {
|
|
190
|
+
const first = chunks[0];
|
|
191
|
+
if (!first) return undefined;
|
|
192
|
+
if (chunks.length === 1) return first.data.slice();
|
|
193
|
+
const count = first.count;
|
|
194
|
+
if (count !== chunks.length) return undefined;
|
|
195
|
+
const ordered = new Array(count);
|
|
196
|
+
for (const chunk of chunks){
|
|
197
|
+
if (chunk.count !== count || chunk.seq < 1 || chunk.seq > count) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
if (ordered[chunk.seq - 1]) return undefined;
|
|
201
|
+
ordered[chunk.seq - 1] = chunk.data;
|
|
202
|
+
}
|
|
203
|
+
return concatBytes(ordered);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Reads the metadata blocks of a JPEG. Returns `null` when the bytes are not
|
|
207
|
+
* a JPEG (no SOI); a damaged or truncated header yields whatever blocks came
|
|
208
|
+
* before the damage.
|
|
209
|
+
*
|
|
210
|
+
* The first EXIF and first main-XMP segment win, as every reader does; all
|
|
211
|
+
* Photoshop APP13 payloads are concatenated in file order.
|
|
212
|
+
*/ export function readJpegBlocks(bytes) {
|
|
213
|
+
const layout = walkJpeg(bytes);
|
|
214
|
+
if (!layout) return null;
|
|
215
|
+
const blocks = {};
|
|
216
|
+
const irbParts = [];
|
|
217
|
+
const iccChunks = [];
|
|
218
|
+
const comments = [];
|
|
219
|
+
for (const segment of layout.segments){
|
|
220
|
+
const { dataStart, end } = segment;
|
|
221
|
+
switch(classify(bytes, segment)){
|
|
222
|
+
case 'exif':
|
|
223
|
+
if (!blocks.exif) {
|
|
224
|
+
blocks.exif = bytes.slice(dataStart + EXIF_HEADER.length, end);
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case 'xmp':
|
|
228
|
+
if (blocks.xmp === undefined) {
|
|
229
|
+
blocks.xmp = trimNul(utf8Decode(bytes.subarray(dataStart + XMP_ID.length, end)));
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
case 'irb':
|
|
233
|
+
irbParts.push(bytes.subarray(dataStart + PHOTOSHOP_ID.length, end));
|
|
234
|
+
break;
|
|
235
|
+
case 'icc':
|
|
236
|
+
var _bytes_, _bytes_1;
|
|
237
|
+
iccChunks.push({
|
|
238
|
+
seq: (_bytes_ = bytes[dataStart + ICC_ID.length]) != null ? _bytes_ : 0,
|
|
239
|
+
count: (_bytes_1 = bytes[dataStart + ICC_ID.length + 1]) != null ? _bytes_1 : 0,
|
|
240
|
+
data: bytes.subarray(dataStart + ICC_ID.length + 2, end)
|
|
241
|
+
});
|
|
242
|
+
break;
|
|
243
|
+
case 'com':
|
|
244
|
+
comments.push(trimNul(decodeFreeText(bytes.subarray(dataStart, end))));
|
|
245
|
+
break;
|
|
246
|
+
default:
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (irbParts.length) blocks.irb = concatBytes(irbParts);
|
|
251
|
+
const icc = assembleIcc(iccChunks);
|
|
252
|
+
if (icc) blocks.icc = icc;
|
|
253
|
+
if (comments.length) blocks.comments = comments;
|
|
254
|
+
return blocks;
|
|
255
|
+
}
|
|
256
|
+
/** Builds one marker segment: marker, length, identifier, payload. */ function buildSegment(code, id, payload) {
|
|
257
|
+
const length = 2 + id.length + payload.length;
|
|
258
|
+
const out = new Uint8Array(2 + length);
|
|
259
|
+
out[0] = 0xff;
|
|
260
|
+
out[1] = code;
|
|
261
|
+
writeU16BE(out, 2, length);
|
|
262
|
+
out.set(id, 4);
|
|
263
|
+
out.set(payload, 4 + id.length);
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/** The EXIF APP1 to write, or `null` to remove it. */ function exifSegment(exif) {
|
|
267
|
+
if (!exif || !exif.length) return null;
|
|
268
|
+
// The contract hands over TIFF bytes; a caller that left the APP1 prefix
|
|
269
|
+
// on would otherwise write it twice.
|
|
270
|
+
const tiff = exif.length >= EXIF_HEADER.length && bytesAt(exif, 0, EXIF_ID) ? exif.subarray(EXIF_HEADER.length) : exif;
|
|
271
|
+
if (tiff.length > MAX_EXIF_BYTES) {
|
|
272
|
+
throw new EmbeddedWriteError(METADATA_TOO_LARGE);
|
|
273
|
+
}
|
|
274
|
+
return buildSegment(APP1, EXIF_HEADER, tiff);
|
|
275
|
+
}
|
|
276
|
+
/** The main XMP APP1 to write, or `null` to remove it. */ function xmpSegment(xmp) {
|
|
277
|
+
if (!xmp) return null;
|
|
278
|
+
const packet = utf8Encode(xmp);
|
|
279
|
+
if (packet.length > MAX_XMP_BYTES) {
|
|
280
|
+
throw new EmbeddedWriteError(METADATA_TOO_LARGE);
|
|
281
|
+
}
|
|
282
|
+
return buildSegment(APP1, XMP_ID, packet);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The APP13 segments to write, or `null` to remove them. An IRB larger than
|
|
286
|
+
* one segment is split at the 64 KB boundary into consecutive segments, each
|
|
287
|
+
* repeating `Photoshop 3.0\0`, as Photoshop itself writes them; readers
|
|
288
|
+
* (this one included) concatenate the payloads back.
|
|
289
|
+
*/ function irbSegments(irb) {
|
|
290
|
+
if (!irb || !irb.length) return null;
|
|
291
|
+
const out = [];
|
|
292
|
+
for(let offset = 0; offset < irb.length; offset += MAX_IRB_CHUNK){
|
|
293
|
+
out.push(buildSegment(APP13, PHOTOSHOP_ID, irb.subarray(offset, offset + MAX_IRB_CHUNK)));
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Applies one block's update to the piece list: replace the first piece of
|
|
299
|
+
* `kind` and drop the rest (a second copy is a stale one the next reader
|
|
300
|
+
* may prefer), insert at `anchor()` when there is none, or remove them all.
|
|
301
|
+
*/ function applyBlock(pieces, kind, replacement, anchor) {
|
|
302
|
+
if (replacement === undefined) return;
|
|
303
|
+
let replaced = false;
|
|
304
|
+
for (const piece of pieces){
|
|
305
|
+
if (piece.kind !== kind) continue;
|
|
306
|
+
if (replacement && !replaced) {
|
|
307
|
+
piece.body = replacement;
|
|
308
|
+
replaced = true;
|
|
309
|
+
} else {
|
|
310
|
+
piece.body = new Uint8Array(0);
|
|
311
|
+
piece.kind = null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (replacement && !replaced) {
|
|
315
|
+
pieces.splice(anchor(), 0, {
|
|
316
|
+
prefix: new Uint8Array(0),
|
|
317
|
+
body: replacement,
|
|
318
|
+
kind
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Shifts the MP Entry offsets of an MPF segment (CIPA DC-007, 5.2.3.3) by
|
|
324
|
+
* `delta`. MPF locates the secondary images after EOI — a camera's large
|
|
325
|
+
* preview, an Ultra HDR gain map — by offsets from its own TIFF header, so a
|
|
326
|
+
* resized segment between that header and the images would leave them
|
|
327
|
+
* pointing into the wrong bytes. Returns the patched payload, or `null` when
|
|
328
|
+
* the structure cannot be followed.
|
|
329
|
+
*/ function shiftMpfOffsets(segment, delta) {
|
|
330
|
+
const out = segment.slice();
|
|
331
|
+
// Marker (2) + length (2) + `MPF\0` (4); the TIFF header starts here.
|
|
332
|
+
const tiff = 8;
|
|
333
|
+
const little = out[tiff] === 0x49 && out[tiff + 1] === 0x49;
|
|
334
|
+
const big = out[tiff] === 0x4d && out[tiff + 1] === 0x4d;
|
|
335
|
+
if (!little && !big) return null;
|
|
336
|
+
const u16 = (at)=>{
|
|
337
|
+
var _out_at, _out_;
|
|
338
|
+
return little ? ((_out_at = out[at]) != null ? _out_at : 0) | ((_out_ = out[at + 1]) != null ? _out_ : 0) << 8 : readU16BE(out, at);
|
|
339
|
+
};
|
|
340
|
+
const u32 = (at)=>{
|
|
341
|
+
var _out_at, _out_, _out_1, _out_2;
|
|
342
|
+
return little ? (((_out_at = out[at]) != null ? _out_at : 0) | ((_out_ = out[at + 1]) != null ? _out_ : 0) << 8 | ((_out_1 = out[at + 2]) != null ? _out_1 : 0) << 16) + ((_out_2 = out[at + 3]) != null ? _out_2 : 0) * 0x1000000 : readU32BE(out, at);
|
|
343
|
+
};
|
|
344
|
+
const put32 = (at, value)=>{
|
|
345
|
+
if (little) {
|
|
346
|
+
out[at] = value & 0xff;
|
|
347
|
+
out[at + 1] = value >>> 8 & 0xff;
|
|
348
|
+
out[at + 2] = value >>> 16 & 0xff;
|
|
349
|
+
out[at + 3] = value >>> 24 & 0xff;
|
|
350
|
+
} else {
|
|
351
|
+
writeU32BE(out, at, value);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const ifd = tiff + u32(tiff + 4);
|
|
355
|
+
if (ifd + 2 > out.length) return null;
|
|
356
|
+
const count = u16(ifd);
|
|
357
|
+
for(let i = 0; i < count; i += 1){
|
|
358
|
+
const entry = ifd + 2 + i * 12;
|
|
359
|
+
if (entry + 12 > out.length) return null;
|
|
360
|
+
if (u16(entry) !== MPF_MP_ENTRY_TAG) continue;
|
|
361
|
+
// Type UNDEFINED, so the count is the table's size in bytes.
|
|
362
|
+
if (u16(entry + 2) !== 7) return null;
|
|
363
|
+
const size = u32(entry + 4);
|
|
364
|
+
const table = size > 4 ? tiff + u32(entry + 8) : entry + 8;
|
|
365
|
+
if (size % 16 !== 0 || table + size > out.length) return null;
|
|
366
|
+
for(let at = table; at < table + size; at += 16){
|
|
367
|
+
const offset = u32(at + 8);
|
|
368
|
+
// Offset 0 is the primary image itself (5.2.3.3.3).
|
|
369
|
+
if (offset === 0) continue;
|
|
370
|
+
const moved = offset + delta;
|
|
371
|
+
if (moved <= 0 || moved > 0xffffffff) return null;
|
|
372
|
+
put32(at + 8, moved);
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Writes an update into a JPEG and returns the new file. Only the header's
|
|
380
|
+
* metadata segments change; every other segment and the whole of SOS→EOI
|
|
381
|
+
* and beyond are copied byte for byte.
|
|
382
|
+
*
|
|
383
|
+
* - A block that exists is replaced in place (and any later duplicates of
|
|
384
|
+
* it removed); `null` removes every copy.
|
|
385
|
+
* - A new block goes to its standard position: EXIF right after SOI and a
|
|
386
|
+
* leading APP0 JFIF/JFXX run, XMP after EXIF, APP13 after XMP.
|
|
387
|
+
* - Extended XMP, ICC, COM and every other segment are never touched.
|
|
388
|
+
*
|
|
389
|
+
* Throws `EmbeddedWriteError` for a file that is not a JPEG or whose header
|
|
390
|
+
* is truncated, for a block too large for its segment (EXIF over 65527
|
|
391
|
+
* bytes, XMP over 65502), for an MPF index it cannot keep pointing at the
|
|
392
|
+
* right bytes, and for a PNG text update.
|
|
393
|
+
*/ export function writeJpegBlocks(bytes, update) {
|
|
394
|
+
const layout = walkJpeg(bytes);
|
|
395
|
+
if (!layout) throw new EmbeddedWriteError('This file is not a JPEG image.');
|
|
396
|
+
if (!layout.complete) throw new EmbeddedWriteError(FILE_DAMAGED);
|
|
397
|
+
if (update.pngText && Object.values(update.pngText).some((value)=>value !== null)) {
|
|
398
|
+
throw new EmbeddedWriteError('PNG text fields can only be written to a PNG.');
|
|
399
|
+
}
|
|
400
|
+
const exif = update.exif === undefined ? undefined : exifSegment(update.exif);
|
|
401
|
+
const xmp = update.xmp === undefined ? undefined : xmpSegment(update.xmp);
|
|
402
|
+
const irb = update.irb === undefined ? undefined : irbSegments(update.irb);
|
|
403
|
+
const pieces = layout.segments.map((segment)=>({
|
|
404
|
+
prefix: bytes.subarray(segment.start, segment.marker),
|
|
405
|
+
body: bytes.subarray(segment.marker, segment.end),
|
|
406
|
+
kind: classify(bytes, segment),
|
|
407
|
+
segment
|
|
408
|
+
}));
|
|
409
|
+
const afterJfif = ()=>{
|
|
410
|
+
var _pieces_index_segment, _pieces_index;
|
|
411
|
+
let index = 0;
|
|
412
|
+
while(((_pieces_index = pieces[index]) == null ? void 0 : (_pieces_index_segment = _pieces_index.segment) == null ? void 0 : _pieces_index_segment.code) === APP0)index += 1;
|
|
413
|
+
return index;
|
|
414
|
+
};
|
|
415
|
+
const after = (kind, fallback)=>()=>{
|
|
416
|
+
for(let i = pieces.length - 1; i >= 0; i -= 1){
|
|
417
|
+
var _pieces_i;
|
|
418
|
+
if (((_pieces_i = pieces[i]) == null ? void 0 : _pieces_i.kind) === kind) return i + 1;
|
|
419
|
+
}
|
|
420
|
+
return fallback();
|
|
421
|
+
};
|
|
422
|
+
const afterExif = after('exif', afterJfif);
|
|
423
|
+
applyBlock(pieces, 'exif', exif, afterJfif);
|
|
424
|
+
applyBlock(pieces, 'xmp', xmp, afterExif);
|
|
425
|
+
applyBlock(pieces, 'irb', irb === undefined ? undefined : irb && concatBytes(irb), after('xmp', afterExif));
|
|
426
|
+
// Only a size change BETWEEN the MPF segment and the images after EOI moves
|
|
427
|
+
// them relative to it; a change before MPF shifts both alike.
|
|
428
|
+
const mpfIndex = pieces.findIndex((piece)=>piece.kind === 'mpf');
|
|
429
|
+
const mpf = pieces[mpfIndex];
|
|
430
|
+
if (mpf == null ? void 0 : mpf.segment) {
|
|
431
|
+
const oldSpan = layout.tailStart - mpf.segment.end;
|
|
432
|
+
let newSpan = 0;
|
|
433
|
+
for (const piece of pieces.slice(mpfIndex + 1)){
|
|
434
|
+
newSpan += piece.prefix.length + piece.body.length;
|
|
435
|
+
}
|
|
436
|
+
const delta = newSpan - oldSpan;
|
|
437
|
+
if (delta !== 0) {
|
|
438
|
+
const patched = shiftMpfOffsets(mpf.body, delta);
|
|
439
|
+
if (!patched) {
|
|
440
|
+
throw new EmbeddedWriteError("This photo's index of its embedded preview images could not be updated, so its metadata cannot be edited safely.");
|
|
441
|
+
}
|
|
442
|
+
mpf.body = patched;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const parts = [
|
|
446
|
+
bytes.subarray(0, 2)
|
|
447
|
+
];
|
|
448
|
+
for (const piece of pieces){
|
|
449
|
+
if (piece.prefix.length) parts.push(piece.prefix);
|
|
450
|
+
if (piece.body.length) parts.push(piece.body);
|
|
451
|
+
}
|
|
452
|
+
parts.push(bytes.subarray(layout.tailStart));
|
|
453
|
+
return concatBytes(parts);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
//# sourceMappingURL=jpeg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/app-utils/media-embedded-metadata/jpeg.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * JPEG metadata blocks: find them, and replace them without touching a\n * single byte of the image (AGL-3331).\n *\n * A JPEG is a run of marker segments (ITU-T T.81, B.1.1.4): `0xFF`, a marker\n * code, and — for every marker but the standalone ones — a 16-bit length that\n * counts itself. Metadata lives in the APPn and COM segments of the header,\n * all of which precede the first Start of Scan. The walk here stops at that\n * SOS and never looks further: everything from SOS to EOI, and anything after\n * EOI (an MPF preview, a motion photo's video), is copied through verbatim,\n * which is the whole of the lossless guarantee.\n *\n * The blocks, by their identifying prefix:\n *\n * - APP1 `Exif\\0\\0` — EXIF (Exif 2.32, 4.5.4).\n * - APP1 `http://ns.adobe.com/xap/1.0/\\0` — the main XMP packet\n * (XMP Specification Part 3, 1.1.3).\n * - APP1 `http://ns.adobe.com/xmp/extension/\\0` — extended XMP. Read by\n * nobody here and never rewritten; its segments pass through untouched.\n * - APP13 `Photoshop 3.0\\0` — Photoshop Image Resource Blocks, which carry\n * the IPTC-IIM record. Photoshop splits a large one across consecutive\n * APP13 segments, each repeating the header.\n * - APP2 `ICC_PROFILE\\0` — the ICC profile, in numbered chunks\n * (ICC.1:2010, Annex B.4). Read-only.\n * - COM — free-text comments (T.81, B.2.4.5). Read-only.\n */\n\nimport { EmbeddedWriteError } from './types'\nimport {\n FILE_DAMAGED,\n METADATA_TOO_LARGE,\n asciiBytes,\n bytesAt,\n concatBytes,\n decodeFreeText,\n readU16BE,\n readU32BE,\n trimNul,\n utf8Decode,\n utf8Encode,\n writeU16BE,\n writeU32BE,\n type ImageBlockUpdate,\n type ImageBlocks,\n} from './image-blocks'\n\nconst SOI = 0xd8\nconst EOI = 0xd9\nconst SOS = 0xda\nconst APP0 = 0xe0\nconst APP1 = 0xe1\nconst APP2 = 0xe2\nconst APP13 = 0xed\nconst COM = 0xfe\n\n/** `Exif\\0` — the sixth byte is `\\0` by the standard and ignored here. */\nconst EXIF_ID = asciiBytes('Exif\\0')\nconst EXIF_HEADER = asciiBytes('Exif\\0\\0')\nconst XMP_ID = asciiBytes('http://ns.adobe.com/xap/1.0/\\0')\nconst XMP_EXTENSION_ID = asciiBytes('http://ns.adobe.com/xmp/extension/\\0')\nconst PHOTOSHOP_ID = asciiBytes('Photoshop 3.0\\0')\nconst ICC_ID = asciiBytes('ICC_PROFILE\\0')\nconst MPF_ID = asciiBytes('MPF\\0')\n\n/** A segment's length field counts itself, so its payload tops out here. */\nconst MAX_SEGMENT_PAYLOAD = 0xffff - 2\n/** `Exif\\0\\0` plus the TIFF structure must fit one APP1 (65535 − 2 − 6). */\nconst MAX_EXIF_BYTES = MAX_SEGMENT_PAYLOAD - EXIF_HEADER.length\n/**\n * The standard XMP packet's ceiling (XMP Specification Part 3, 1.1.3.1).\n * Anything larger belongs in extended XMP, which this writer does not\n * produce, so a larger packet is refused rather than truncated.\n */\nconst MAX_XMP_BYTES = 65502\n/** Per-APP13 IRB payload once `Photoshop 3.0\\0` is paid for (65535 − 16). */\nconst MAX_IRB_CHUNK = MAX_SEGMENT_PAYLOAD - PHOTOSHOP_ID.length\n\n/**\n * A header past this many segments is not a photo; the walk stops and\n * returns what it read. Real files carry tens — a 4 MB ICC profile split\n * into 64 KB chunks is still under a hundred.\n */\nconst MAX_SEGMENTS = 20000\n\n/** MPF's `MPEntry` tag (CIPA DC-007, 5.2.3.3). */\nconst MPF_MP_ENTRY_TAG = 0xb002\n\n/** One marker segment in the header, as byte offsets into the file. */\ninterface JpegSegment {\n /**\n * Where this segment's run begins: fill bytes (`0xFF` padding, T.81\n * B.1.1.2) and any stray bytes a lenient decoder skips come before the\n * marker and belong to it. They are copied through even when the segment\n * itself is replaced or removed.\n */\n start: number\n /** The `0xFF` that introduces the marker code. */\n marker: number\n code: number\n /** First payload byte, after the length field. */\n dataStart: number\n /** One past the last byte. */\n end: number\n}\n\ninterface JpegLayout {\n segments: JpegSegment[]\n /** Where the verbatim tail begins: the run holding SOS, or EOI. */\n tailStart: number\n /**\n * False when the header ran out — truncated, a length pointing past the\n * end, or more segments than any photo has. Reads still return what they\n * found; writes refuse.\n */\n complete: boolean\n}\n\n/**\n * Markers that carry no length field (T.81, B.1.1.3 and Table B.1): TEM and\n * RST0–RST7, plus a stray repeated SOI.\n */\nfunction isStandalone(code: number): boolean {\n return code === 0x01 || (code >= 0xd0 && code <= 0xd8)\n}\n\n/**\n * Walks the header's marker segments up to SOS (or EOI). Returns `null` when\n * the bytes do not open with SOI.\n *\n * Between segments it behaves like libjpeg's `next_marker`: bytes that are\n * not `0xFF` are skipped, a run of `0xFF` is fill, and `0xFF 0x00` is not a\n * marker. Every step moves forward, so the loop ends on any input.\n */\nfunction walkJpeg(bytes: Uint8Array): JpegLayout | null {\n if (bytes.length < 2 || bytes[0] !== 0xff || bytes[1] !== SOI) return null\n const segments: JpegSegment[] = []\n let offset = 2\n while (segments.length < MAX_SEGMENTS) {\n const start = offset\n let marker = -1\n let code = -1\n while (offset < bytes.length) {\n if (bytes[offset] !== 0xff) {\n offset += 1\n continue\n }\n let next = offset + 1\n while (next < bytes.length && bytes[next] === 0xff) next += 1\n // Only fill bytes up to the end: no marker, so the header is cut.\n if (next >= bytes.length) break\n const candidate = bytes[next] ?? 0\n if (candidate === 0x00) {\n offset = next + 1\n continue\n }\n marker = next - 1\n code = candidate\n break\n }\n if (code < 0) return { segments, tailStart: start, complete: false }\n if (code === SOS || code === EOI) {\n return { segments, tailStart: start, complete: true }\n }\n if (isStandalone(code)) {\n segments.push({\n start,\n marker,\n code,\n dataStart: marker + 2,\n end: marker + 2,\n })\n offset = marker + 2\n continue\n }\n if (marker + 4 > bytes.length) {\n return { segments, tailStart: start, complete: false }\n }\n const length = readU16BE(bytes, marker + 2)\n const end = marker + 2 + length\n if (length < 2 || end > bytes.length) {\n return { segments, tailStart: start, complete: false }\n }\n segments.push({ start, marker, code, dataStart: marker + 4, end })\n offset = end\n }\n return { segments, tailStart: offset, complete: false }\n}\n\ntype BlockKind = 'exif' | 'xmp' | 'xmpExtension' | 'irb' | 'icc' | 'mpf' | 'com'\n\n/** Which metadata block a segment is, by its marker and identifying prefix. */\nfunction classify(bytes: Uint8Array, segment: JpegSegment): BlockKind | null {\n const { code, dataStart, end } = segment\n const has = (id: Uint8Array) =>\n end - dataStart >= id.length && bytesAt(bytes, dataStart, id)\n if (code === APP1) {\n if (end - dataStart >= EXIF_HEADER.length && has(EXIF_ID)) return 'exif'\n if (has(XMP_ID)) return 'xmp'\n if (has(XMP_EXTENSION_ID)) return 'xmpExtension'\n return null\n }\n if (code === APP13) return has(PHOTOSHOP_ID) ? 'irb' : null\n if (code === APP2) {\n if (end - dataStart >= ICC_ID.length + 2 && has(ICC_ID)) return 'icc'\n if (has(MPF_ID)) return 'mpf'\n return null\n }\n if (code === COM) return 'com'\n return null\n}\n\n/**\n * Reassembles the ICC profile from its APP2 chunks (ICC.1:2010, B.4): each\n * carries a 1-based sequence number and the chunk count. A set with a gap,\n * a duplicate or disagreeing counts is dropped — half a profile is worse\n * than none — except that a lone chunk is taken as the whole profile.\n */\nfunction assembleIcc(\n chunks: { seq: number; count: number; data: Uint8Array }[],\n): Uint8Array | undefined {\n const first = chunks[0]\n if (!first) return undefined\n if (chunks.length === 1) return first.data.slice()\n const count = first.count\n if (count !== chunks.length) return undefined\n const ordered: Uint8Array[] = new Array(count)\n for (const chunk of chunks) {\n if (chunk.count !== count || chunk.seq < 1 || chunk.seq > count) {\n return undefined\n }\n if (ordered[chunk.seq - 1]) return undefined\n ordered[chunk.seq - 1] = chunk.data\n }\n return concatBytes(ordered)\n}\n\n/**\n * Reads the metadata blocks of a JPEG. Returns `null` when the bytes are not\n * a JPEG (no SOI); a damaged or truncated header yields whatever blocks came\n * before the damage.\n *\n * The first EXIF and first main-XMP segment win, as every reader does; all\n * Photoshop APP13 payloads are concatenated in file order.\n */\nexport function readJpegBlocks(bytes: Uint8Array): ImageBlocks | null {\n const layout = walkJpeg(bytes)\n if (!layout) return null\n const blocks: ImageBlocks = {}\n const irbParts: Uint8Array[] = []\n const iccChunks: { seq: number; count: number; data: Uint8Array }[] = []\n const comments: string[] = []\n for (const segment of layout.segments) {\n const { dataStart, end } = segment\n switch (classify(bytes, segment)) {\n case 'exif':\n if (!blocks.exif) {\n blocks.exif = bytes.slice(dataStart + EXIF_HEADER.length, end)\n }\n break\n case 'xmp':\n if (blocks.xmp === undefined) {\n blocks.xmp = trimNul(\n utf8Decode(bytes.subarray(dataStart + XMP_ID.length, end)),\n )\n }\n break\n case 'irb':\n irbParts.push(bytes.subarray(dataStart + PHOTOSHOP_ID.length, end))\n break\n case 'icc':\n iccChunks.push({\n seq: bytes[dataStart + ICC_ID.length] ?? 0,\n count: bytes[dataStart + ICC_ID.length + 1] ?? 0,\n data: bytes.subarray(dataStart + ICC_ID.length + 2, end),\n })\n break\n case 'com':\n comments.push(trimNul(decodeFreeText(bytes.subarray(dataStart, end))))\n break\n default:\n break\n }\n }\n if (irbParts.length) blocks.irb = concatBytes(irbParts)\n const icc = assembleIcc(iccChunks)\n if (icc) blocks.icc = icc\n if (comments.length) blocks.comments = comments\n return blocks\n}\n\n/** Builds one marker segment: marker, length, identifier, payload. */\nfunction buildSegment(\n code: number,\n id: Uint8Array,\n payload: Uint8Array,\n): Uint8Array {\n const length = 2 + id.length + payload.length\n const out = new Uint8Array(2 + length)\n out[0] = 0xff\n out[1] = code\n writeU16BE(out, 2, length)\n out.set(id, 4)\n out.set(payload, 4 + id.length)\n return out\n}\n\n/** The EXIF APP1 to write, or `null` to remove it. */\nfunction exifSegment(exif: Uint8Array | null): Uint8Array | null {\n if (!exif || !exif.length) return null\n // The contract hands over TIFF bytes; a caller that left the APP1 prefix\n // on would otherwise write it twice.\n const tiff =\n exif.length >= EXIF_HEADER.length && bytesAt(exif, 0, EXIF_ID)\n ? exif.subarray(EXIF_HEADER.length)\n : exif\n if (tiff.length > MAX_EXIF_BYTES) {\n throw new EmbeddedWriteError(METADATA_TOO_LARGE)\n }\n return buildSegment(APP1, EXIF_HEADER, tiff)\n}\n\n/** The main XMP APP1 to write, or `null` to remove it. */\nfunction xmpSegment(xmp: string | null): Uint8Array | null {\n if (!xmp) return null\n const packet = utf8Encode(xmp)\n if (packet.length > MAX_XMP_BYTES) {\n throw new EmbeddedWriteError(METADATA_TOO_LARGE)\n }\n return buildSegment(APP1, XMP_ID, packet)\n}\n\n/**\n * The APP13 segments to write, or `null` to remove them. An IRB larger than\n * one segment is split at the 64 KB boundary into consecutive segments, each\n * repeating `Photoshop 3.0\\0`, as Photoshop itself writes them; readers\n * (this one included) concatenate the payloads back.\n */\nfunction irbSegments(irb: Uint8Array | null): Uint8Array[] | null {\n if (!irb || !irb.length) return null\n const out: Uint8Array[] = []\n for (let offset = 0; offset < irb.length; offset += MAX_IRB_CHUNK) {\n out.push(\n buildSegment(\n APP13,\n PHOTOSHOP_ID,\n irb.subarray(offset, offset + MAX_IRB_CHUNK),\n ),\n )\n }\n return out\n}\n\n/**\n * One unit of the rewritten header. An original segment keeps its fill\n * prefix whatever happens to its body; an inserted one has no prefix.\n */\ninterface Piece {\n prefix: Uint8Array\n /** What is written after the prefix — the original, a replacement, or nothing. */\n body: Uint8Array\n kind: BlockKind | null\n /** The original segment, for pieces that came from the file. */\n segment?: JpegSegment\n}\n\n/**\n * Applies one block's update to the piece list: replace the first piece of\n * `kind` and drop the rest (a second copy is a stale one the next reader\n * may prefer), insert at `anchor()` when there is none, or remove them all.\n */\nfunction applyBlock(\n pieces: Piece[],\n kind: BlockKind,\n replacement: Uint8Array | null | undefined,\n anchor: () => number,\n): void {\n if (replacement === undefined) return\n let replaced = false\n for (const piece of pieces) {\n if (piece.kind !== kind) continue\n if (replacement && !replaced) {\n piece.body = replacement\n replaced = true\n } else {\n piece.body = new Uint8Array(0)\n piece.kind = null\n }\n }\n if (replacement && !replaced) {\n pieces.splice(anchor(), 0, {\n prefix: new Uint8Array(0),\n body: replacement,\n kind,\n })\n }\n}\n\n/**\n * Shifts the MP Entry offsets of an MPF segment (CIPA DC-007, 5.2.3.3) by\n * `delta`. MPF locates the secondary images after EOI — a camera's large\n * preview, an Ultra HDR gain map — by offsets from its own TIFF header, so a\n * resized segment between that header and the images would leave them\n * pointing into the wrong bytes. Returns the patched payload, or `null` when\n * the structure cannot be followed.\n */\nfunction shiftMpfOffsets(\n segment: Uint8Array,\n delta: number,\n): Uint8Array | null {\n const out = segment.slice()\n // Marker (2) + length (2) + `MPF\\0` (4); the TIFF header starts here.\n const tiff = 8\n const little = out[tiff] === 0x49 && out[tiff + 1] === 0x49\n const big = out[tiff] === 0x4d && out[tiff + 1] === 0x4d\n if (!little && !big) return null\n const u16 = (at: number) =>\n little ? (out[at] ?? 0) | ((out[at + 1] ?? 0) << 8) : readU16BE(out, at)\n const u32 = (at: number) =>\n little\n ? ((out[at] ?? 0) |\n ((out[at + 1] ?? 0) << 8) |\n ((out[at + 2] ?? 0) << 16)) +\n (out[at + 3] ?? 0) * 0x1000000\n : readU32BE(out, at)\n const put32 = (at: number, value: number) => {\n if (little) {\n out[at] = value & 0xff\n out[at + 1] = (value >>> 8) & 0xff\n out[at + 2] = (value >>> 16) & 0xff\n out[at + 3] = (value >>> 24) & 0xff\n } else {\n writeU32BE(out, at, value)\n }\n }\n const ifd = tiff + u32(tiff + 4)\n if (ifd + 2 > out.length) return null\n const count = u16(ifd)\n for (let i = 0; i < count; i += 1) {\n const entry = ifd + 2 + i * 12\n if (entry + 12 > out.length) return null\n if (u16(entry) !== MPF_MP_ENTRY_TAG) continue\n // Type UNDEFINED, so the count is the table's size in bytes.\n if (u16(entry + 2) !== 7) return null\n const size = u32(entry + 4)\n const table = size > 4 ? tiff + u32(entry + 8) : entry + 8\n if (size % 16 !== 0 || table + size > out.length) return null\n for (let at = table; at < table + size; at += 16) {\n const offset = u32(at + 8)\n // Offset 0 is the primary image itself (5.2.3.3.3).\n if (offset === 0) continue\n const moved = offset + delta\n if (moved <= 0 || moved > 0xffffffff) return null\n put32(at + 8, moved)\n }\n return out\n }\n return out\n}\n\n/**\n * Writes an update into a JPEG and returns the new file. Only the header's\n * metadata segments change; every other segment and the whole of SOS→EOI\n * and beyond are copied byte for byte.\n *\n * - A block that exists is replaced in place (and any later duplicates of\n * it removed); `null` removes every copy.\n * - A new block goes to its standard position: EXIF right after SOI and a\n * leading APP0 JFIF/JFXX run, XMP after EXIF, APP13 after XMP.\n * - Extended XMP, ICC, COM and every other segment are never touched.\n *\n * Throws `EmbeddedWriteError` for a file that is not a JPEG or whose header\n * is truncated, for a block too large for its segment (EXIF over 65527\n * bytes, XMP over 65502), for an MPF index it cannot keep pointing at the\n * right bytes, and for a PNG text update.\n */\nexport function writeJpegBlocks(\n bytes: Uint8Array,\n update: ImageBlockUpdate,\n): Uint8Array {\n const layout = walkJpeg(bytes)\n if (!layout) throw new EmbeddedWriteError('This file is not a JPEG image.')\n if (!layout.complete) throw new EmbeddedWriteError(FILE_DAMAGED)\n if (\n update.pngText &&\n Object.values(update.pngText).some((value) => value !== null)\n ) {\n throw new EmbeddedWriteError(\n 'PNG text fields can only be written to a PNG.',\n )\n }\n\n const exif = update.exif === undefined ? undefined : exifSegment(update.exif)\n const xmp = update.xmp === undefined ? undefined : xmpSegment(update.xmp)\n const irb = update.irb === undefined ? undefined : irbSegments(update.irb)\n\n const pieces: Piece[] = layout.segments.map((segment) => ({\n prefix: bytes.subarray(segment.start, segment.marker),\n body: bytes.subarray(segment.marker, segment.end),\n kind: classify(bytes, segment),\n segment,\n }))\n\n const afterJfif = () => {\n let index = 0\n while (pieces[index]?.segment?.code === APP0) index += 1\n return index\n }\n const after = (kind: BlockKind, fallback: () => number) => () => {\n for (let i = pieces.length - 1; i >= 0; i -= 1) {\n if (pieces[i]?.kind === kind) return i + 1\n }\n return fallback()\n }\n\n const afterExif = after('exif', afterJfif)\n applyBlock(pieces, 'exif', exif, afterJfif)\n applyBlock(pieces, 'xmp', xmp, afterExif)\n applyBlock(\n pieces,\n 'irb',\n irb === undefined ? undefined : irb && concatBytes(irb),\n after('xmp', afterExif),\n )\n\n // Only a size change BETWEEN the MPF segment and the images after EOI moves\n // them relative to it; a change before MPF shifts both alike.\n const mpfIndex = pieces.findIndex((piece) => piece.kind === 'mpf')\n const mpf = pieces[mpfIndex]\n if (mpf?.segment) {\n const oldSpan = layout.tailStart - mpf.segment.end\n let newSpan = 0\n for (const piece of pieces.slice(mpfIndex + 1)) {\n newSpan += piece.prefix.length + piece.body.length\n }\n const delta = newSpan - oldSpan\n if (delta !== 0) {\n const patched = shiftMpfOffsets(mpf.body, delta)\n if (!patched) {\n throw new EmbeddedWriteError(\n \"This photo's index of its embedded preview images could not be updated, so its metadata cannot be edited safely.\",\n )\n }\n mpf.body = patched\n }\n }\n\n const parts: Uint8Array[] = [bytes.subarray(0, 2)]\n for (const piece of pieces) {\n if (piece.prefix.length) parts.push(piece.prefix)\n if (piece.body.length) parts.push(piece.body)\n }\n parts.push(bytes.subarray(layout.tailStart))\n return concatBytes(parts)\n}\n"],"names":["EmbeddedWriteError","FILE_DAMAGED","METADATA_TOO_LARGE","asciiBytes","bytesAt","concatBytes","decodeFreeText","readU16BE","readU32BE","trimNul","utf8Decode","utf8Encode","writeU16BE","writeU32BE","SOI","EOI","SOS","APP0","APP1","APP2","APP13","COM","EXIF_ID","EXIF_HEADER","XMP_ID","XMP_EXTENSION_ID","PHOTOSHOP_ID","ICC_ID","MPF_ID","MAX_SEGMENT_PAYLOAD","MAX_EXIF_BYTES","length","MAX_XMP_BYTES","MAX_IRB_CHUNK","MAX_SEGMENTS","MPF_MP_ENTRY_TAG","isStandalone","code","walkJpeg","bytes","segments","offset","start","marker","next","candidate","tailStart","complete","push","dataStart","end","classify","segment","has","id","assembleIcc","chunks","first","undefined","data","slice","count","ordered","Array","chunk","seq","readJpegBlocks","layout","blocks","irbParts","iccChunks","comments","exif","xmp","subarray","irb","icc","buildSegment","payload","out","Uint8Array","set","exifSegment","tiff","xmpSegment","packet","irbSegments","applyBlock","pieces","kind","replacement","anchor","replaced","piece","body","splice","prefix","shiftMpfOffsets","delta","little","big","u16","at","u32","put32","value","ifd","i","entry","size","table","moved","writeJpegBlocks","update","pngText","Object","values","some","map","afterJfif","index","after","fallback","afterExif","mpfIndex","findIndex","mpf","oldSpan","newSpan","patched","parts"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { EmbeddedByteReader, EmbeddedCandidate } from './types';
|
|
18
|
+
/** Above this, `moov` is not read and the film reads as having no tags. */
|
|
19
|
+
export declare const MP4_MOOV_MAX_BYTES: number;
|
|
20
|
+
/**
|
|
21
|
+
* An ISO 6709 point (`+37.7749-122.4194+010.000/`) as `"lat,lon"` with six
|
|
22
|
+
* places. Latitude has 2, 4 or 6 integer digits (degrees, degrees and
|
|
23
|
+
* minutes, or degrees minutes seconds); longitude 3, 5 or 7 (ISO 6709
|
|
24
|
+
* Annex H). Null for anything else or out of range.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseIso6709(raw: string): string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Read a film's tags through random access.
|
|
29
|
+
*
|
|
30
|
+
* Null when the bytes do not open like an ISO BMFF or QuickTime file, have
|
|
31
|
+
* no `moov`, or have a `moov` over {@link MP4_MOOV_MAX_BYTES}. Every
|
|
32
|
+
* candidate is `editable: false`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readMp4(reader: EmbeddedByteReader): Promise<{
|
|
35
|
+
candidates: EmbeddedCandidate[];
|
|
36
|
+
xmp: string | null;
|
|
37
|
+
} | null>;
|