@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.
Files changed (80) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
  3. package/src/lib/app-utils/docs-help.generated.js +1 -0
  4. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  5. package/src/lib/app-utils/docs-index.generated.js +3 -3
  6. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  7. package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
  8. package/src/lib/app-utils/media-embedded-fields.js +619 -0
  9. package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
  10. package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
  11. package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
  12. package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
  13. package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
  14. package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
  15. package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
  16. package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
  17. package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
  18. package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
  19. package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
  20. package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
  21. package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
  22. package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
  23. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
  24. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
  25. package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
  26. package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
  27. package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
  28. package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
  29. package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
  30. package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
  31. package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
  32. package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
  33. package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
  34. package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
  35. package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
  36. package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
  37. package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
  38. package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
  39. package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
  40. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
  41. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
  42. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
  43. package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
  44. package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
  45. package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
  46. package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
  47. package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
  48. package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
  49. package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
  50. package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
  51. package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
  52. package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
  53. package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
  54. package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
  55. package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
  56. package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
  57. package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
  58. package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
  59. package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
  60. package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
  61. package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
  62. package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
  63. package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
  64. package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
  65. package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
  66. package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
  67. package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
  68. package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
  69. package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
  70. package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
  71. package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
  72. package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
  73. package/src/lib/app-utils/name-search.d.ts +12 -0
  74. package/src/lib/app-utils/name-search.js +11 -0
  75. package/src/lib/app-utils/name-search.js.map +1 -1
  76. package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
  77. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  78. package/src/server.d.ts +1 -0
  79. package/src/server.js +7 -0
  80. package/src/server.js.map +1 -1
@@ -0,0 +1,744 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * Reads the metadata a media file carries inside its own bytes, and writes
19
+ * edits back into them without re-encoding anything (AGL-3331).
20
+ *
21
+ * This file is the dispatcher. It sniffs the container, hands the bytes to
22
+ * the format module, and folds what comes back — up to three copies of a
23
+ * photo's caption, from XMP, IPTC and EXIF — into the one list of fields a
24
+ * media document stores. Writing is the same trip in reverse: one edit fans
25
+ * out to every block that already holds that idea, so no stale copy is left
26
+ * for the next reader to prefer.
27
+ *
28
+ * Server-only: exposed through `@aglyn/aglyn/server` and never the client
29
+ * barrel. The client half — the catalog, the value formats, the edit rules —
30
+ * is `../media-embedded-fields`.
31
+ */ import { embeddedFieldGroup, embeddedFieldLabel, embeddedKeyWritable, isCanonicalEmbeddedKey, MEDIA_EMBEDDED_CATALOG, MEDIA_EMBEDDED_GROUP_ORDER, MEDIA_EMBEDDED_MAX_FIELDS, MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH, MEDIA_EMBEDDED_MAX_LIST_ITEMS, MEDIA_EMBEDDED_MAX_VALUE_LENGTH, MEDIA_EMBEDDED_METADATA_VERSION, MEDIA_EMBEDDED_WRITABLE_FORMATS, otherEmbeddedKey, parseOtherEmbeddedKey, sanitizeEmbeddedPatch } from "../media-embedded-fields.js";
32
+ import { readExif, writeExif } from "./exif.js";
33
+ import { readGifBlocks } from "./gif.js";
34
+ import { readHeifBlocks } from "./heif.js";
35
+ import { readIccDescription } from "./icc.js";
36
+ import { irbHasIptc, readIim, readIrbIptc, writeIim, writeIrbIptc } from "./iptc.js";
37
+ import { readJpegBlocks, writeJpegBlocks } from "./jpeg.js";
38
+ import { readMp4 } from "./mp4.js";
39
+ import { readOoxml, writeOoxml } from "./ooxml.js";
40
+ import { readPdf, writePdf } from "./pdf.js";
41
+ import { readPngBlocks, writePngBlocks } from "./png.js";
42
+ import { readSvg } from "./svg.js";
43
+ import { readTiffFile, writeTiffFile } from "./tiff.js";
44
+ import { bytesReader, EmbeddedWriteError } from "./types.js";
45
+ import { readWebm } from "./webm.js";
46
+ import { readWebpBlocks, writeWebpBlocks } from "./webp.js";
47
+ import { readXmp, writeXmp } from "./xmp.js";
48
+ export { bytesReader, EmbeddedWriteError };
49
+ /**
50
+ * The most of a file the reader will pull into memory, by format.
51
+ *
52
+ * Every one is the upload ceiling for the family, so an asset the DAM
53
+ * accepted is always readable; the numbers exist so a future raise of a
54
+ * ceiling does not silently turn this into a 500 MB download. Video is read
55
+ * by range — its metadata lives in one box, not across the file — and its
56
+ * cap is on that box (see `mp4.ts`).
57
+ */ export const MEDIA_EMBEDDED_READ_MAX_BYTES = {
58
+ jpeg: 15 * 1024 * 1024,
59
+ png: 15 * 1024 * 1024,
60
+ webp: 15 * 1024 * 1024,
61
+ gif: 15 * 1024 * 1024,
62
+ tiff: 15 * 1024 * 1024,
63
+ heif: 15 * 1024 * 1024,
64
+ svg: 15 * 1024 * 1024,
65
+ pdf: 25 * 1024 * 1024,
66
+ ooxml: 50 * 1024 * 1024
67
+ };
68
+ const OOXML_TYPES = new Set([
69
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
70
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
71
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
72
+ ]);
73
+ const startsWith = (bytes, signature, at = 0)=>bytes.length >= at + signature.length && signature.every((byte, index)=>bytes[at + index] === byte);
74
+ const ascii = (bytes, start, end)=>String.fromCharCode(...bytes.subarray(start, end));
75
+ /**
76
+ * Which reader a file gets, from its first bytes — the stored content type
77
+ * only breaks the ties the bytes cannot (an ISOBMFF `ftyp` is a HEIC photo
78
+ * or an MP4 film; a ZIP is a Word file or a ZIP). A type label is the
79
+ * uploader's claim and the signature is the file's, so the signature wins
80
+ * wherever it can speak.
81
+ */ export function embeddedFormatFor(contentType, head) {
82
+ var _ref;
83
+ var _contentType_toLowerCase_split_;
84
+ const type = (_ref = (_contentType_toLowerCase_split_ = contentType.toLowerCase().split(';')[0]) == null ? void 0 : _contentType_toLowerCase_split_.trim()) != null ? _ref : '';
85
+ if (startsWith(head, [
86
+ 0xff,
87
+ 0xd8,
88
+ 0xff
89
+ ])) return 'jpeg';
90
+ if (startsWith(head, [
91
+ 0x89,
92
+ 0x50,
93
+ 0x4e,
94
+ 0x47,
95
+ 0x0d,
96
+ 0x0a,
97
+ 0x1a,
98
+ 0x0a
99
+ ])) return 'png';
100
+ if (ascii(head, 0, 4) === 'RIFF' && ascii(head, 8, 12) === 'WEBP') return 'webp';
101
+ if (ascii(head, 0, 4) === 'GIF8') return 'gif';
102
+ if (startsWith(head, [
103
+ 0x49,
104
+ 0x49,
105
+ 0x2a,
106
+ 0x00
107
+ ]) || startsWith(head, [
108
+ 0x4d,
109
+ 0x4d,
110
+ 0x00,
111
+ 0x2a
112
+ ])) return 'tiff';
113
+ if (ascii(head, 0, 5) === '%PDF-') return 'pdf';
114
+ if (startsWith(head, [
115
+ 0x1a,
116
+ 0x45,
117
+ 0xdf,
118
+ 0xa3
119
+ ])) return 'webm';
120
+ if (ascii(head, 4, 8) === 'ftyp') {
121
+ return type.startsWith('image/') ? 'heif' : 'mp4';
122
+ }
123
+ if (startsWith(head, [
124
+ 0x50,
125
+ 0x4b,
126
+ 0x03,
127
+ 0x04
128
+ ]) && OOXML_TYPES.has(type)) return 'ooxml';
129
+ if (type === 'image/svg+xml') return 'svg';
130
+ // A QuickTime file may open on `wide`, `free` or `mdat` rather than `ftyp`.
131
+ if (type === 'video/quicktime' || type === 'video/mp4') return 'mp4';
132
+ return null;
133
+ }
134
+ /**
135
+ * The order sources are believed in when they disagree, per format.
136
+ *
137
+ * Images follow the Metadata Working Group: XMP is the modern carrier and
138
+ * the one every editor updates, IIM and EXIF are its legacy mirrors. A PDF's
139
+ * document-info dictionary goes first because it is what every viewer's
140
+ * Properties dialog shows — and this module's writer keeps the two in step,
141
+ * so after one edit through Aglyn the question stops mattering.
142
+ */ const SOURCE_PRECEDENCE = {
143
+ jpeg: [
144
+ 'xmp',
145
+ 'iptc',
146
+ 'exif',
147
+ 'comment',
148
+ 'icc'
149
+ ],
150
+ png: [
151
+ 'xmp',
152
+ 'png',
153
+ 'exif',
154
+ 'icc'
155
+ ],
156
+ webp: [
157
+ 'xmp',
158
+ 'exif',
159
+ 'icc'
160
+ ],
161
+ gif: [
162
+ 'xmp',
163
+ 'comment'
164
+ ],
165
+ tiff: [
166
+ 'xmp',
167
+ 'iptc',
168
+ 'exif',
169
+ 'icc'
170
+ ],
171
+ heif: [
172
+ 'xmp',
173
+ 'exif',
174
+ 'icc'
175
+ ],
176
+ svg: [
177
+ 'svg',
178
+ 'xmp'
179
+ ],
180
+ pdf: [
181
+ 'pdf',
182
+ 'xmp'
183
+ ],
184
+ ooxml: [
185
+ 'ooxml'
186
+ ],
187
+ mp4: [
188
+ 'mp4',
189
+ 'xmp'
190
+ ],
191
+ webm: [
192
+ 'webm'
193
+ ]
194
+ };
195
+ const CATALOG_ORDER = new Map(Object.keys(MEDIA_EMBEDDED_CATALOG).map((key, index)=>[
196
+ key,
197
+ index
198
+ ]));
199
+ const clip = (value)=>value.length > MEDIA_EMBEDDED_MAX_VALUE_LENGTH ? {
200
+ text: value.slice(0, MEDIA_EMBEDDED_MAX_VALUE_LENGTH),
201
+ clipped: true
202
+ } : {
203
+ text: value,
204
+ clipped: false
205
+ };
206
+ /**
207
+ * Fold every block's candidates into the stored field list.
208
+ *
209
+ * One field per key, valued from the most-believed source that holds it and
210
+ * naming every source that does. A value clipped to the stored cap is shown
211
+ * and NOT offered for editing: saving it back would write the clipped text
212
+ * over the whole one, which is data loss the person never asked for.
213
+ */ export function mergeEmbeddedCandidates(format, candidates) {
214
+ const precedence = SOURCE_PRECEDENCE[format];
215
+ const rank = (source)=>{
216
+ const index = precedence.indexOf(source);
217
+ return index === -1 ? precedence.length : index;
218
+ };
219
+ const byKey = new Map();
220
+ for (const candidate of candidates){
221
+ var _byKey_get;
222
+ const empty = Array.isArray(candidate.value) ? !candidate.value.some((item)=>item.trim()) : !candidate.value.trim();
223
+ if (!candidate.key || empty) continue;
224
+ const list = (_byKey_get = byKey.get(candidate.key)) != null ? _byKey_get : [];
225
+ list.push(candidate);
226
+ byKey.set(candidate.key, list);
227
+ }
228
+ let truncated = false;
229
+ const fields = [];
230
+ for (const [key, list] of byKey){
231
+ list.sort((a, b)=>rank(a.source) - rank(b.source));
232
+ const winner = list[0];
233
+ if (!winner) continue;
234
+ const canonical = isCanonicalEmbeddedKey(key);
235
+ const entry = canonical ? MEDIA_EMBEDDED_CATALOG[key] : undefined;
236
+ const listValued = entry ? entry.kind === 'list' : Array.isArray(winner.value);
237
+ let clipped = false;
238
+ const field = {
239
+ key,
240
+ label: embeddedFieldLabel(key, format, winner.label),
241
+ group: embeddedFieldGroup(key, format),
242
+ sources: [
243
+ ...new Set(list.map((candidate)=>candidate.source))
244
+ ],
245
+ editable: false
246
+ };
247
+ if (listValued) {
248
+ const raw = Array.isArray(winner.value) ? winner.value : [
249
+ winner.value
250
+ ];
251
+ const items = [
252
+ ...new Set(raw.map((item)=>item.trim()).filter(Boolean))
253
+ ];
254
+ if (items.length > MEDIA_EMBEDDED_MAX_LIST_ITEMS) clipped = true;
255
+ field.values = items.slice(0, MEDIA_EMBEDDED_MAX_LIST_ITEMS).map((item)=>{
256
+ if (item.length <= MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH) return item;
257
+ clipped = true;
258
+ return item.slice(0, MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH);
259
+ });
260
+ } else {
261
+ const text = Array.isArray(winner.value) ? winner.value.join(', ') : winner.value;
262
+ const result = clip(text.trim());
263
+ clipped = result.clipped;
264
+ field.value = result.text;
265
+ }
266
+ truncated || (truncated = clipped);
267
+ field.editable = !clipped && embeddedKeyWritable(key, format) && list.every((candidate)=>candidate.editable !== false);
268
+ fields.push(field);
269
+ }
270
+ const groupRank = (group)=>MEDIA_EMBEDDED_GROUP_ORDER.indexOf(group);
271
+ fields.sort((a, b)=>{
272
+ var _CATALOG_ORDER_get, _CATALOG_ORDER_get1;
273
+ return groupRank(a.group) - groupRank(b.group) || ((_CATALOG_ORDER_get = CATALOG_ORDER.get(a.key)) != null ? _CATALOG_ORDER_get : Infinity) - ((_CATALOG_ORDER_get1 = CATALOG_ORDER.get(b.key)) != null ? _CATALOG_ORDER_get1 : Infinity) || a.label.localeCompare(b.label);
274
+ });
275
+ if (fields.length > MEDIA_EMBEDDED_MAX_FIELDS) {
276
+ truncated = true;
277
+ fields.length = MEDIA_EMBEDDED_MAX_FIELDS;
278
+ }
279
+ return {
280
+ fields,
281
+ truncated
282
+ };
283
+ }
284
+ /**
285
+ * PNG's registered text keywords (PNG spec §11.3.4.2) that name a catalog
286
+ * idea. Every other keyword surfaces as an "other" field of its own.
287
+ */ const PNG_KEYWORDS = {
288
+ Title: 'title',
289
+ Author: 'creator',
290
+ Description: 'description',
291
+ Copyright: 'copyright',
292
+ 'Creation Time': 'createdAt',
293
+ Software: 'software',
294
+ Comment: 'comment'
295
+ };
296
+ const PNG_KEYWORD_FOR = Object.fromEntries(Object.entries(PNG_KEYWORDS).map(([keyword, key])=>[
297
+ key,
298
+ keyword
299
+ ]));
300
+ function pngTextCandidates(chunks) {
301
+ return chunks.map((chunk)=>{
302
+ const key = PNG_KEYWORDS[chunk.keyword];
303
+ if (key === 'creator') {
304
+ return {
305
+ key,
306
+ value: [
307
+ chunk.text
308
+ ],
309
+ source: 'png'
310
+ };
311
+ }
312
+ if (key === 'createdAt') {
313
+ // The spec recommends RFC 1123; writers put anything. A string no date
314
+ // parser understands is still shown, under its own keyword.
315
+ const parsed = Date.parse(chunk.text);
316
+ return Number.isFinite(parsed) ? {
317
+ key,
318
+ value: new Date(parsed).toISOString().replace('.000Z', 'Z'),
319
+ source: 'png'
320
+ } : {
321
+ key: otherEmbeddedKey('png', chunk.keyword),
322
+ label: chunk.keyword,
323
+ value: chunk.text,
324
+ source: 'png'
325
+ };
326
+ }
327
+ return key ? {
328
+ key,
329
+ value: chunk.text,
330
+ source: 'png'
331
+ } : {
332
+ key: otherEmbeddedKey('png', chunk.keyword),
333
+ label: chunk.keyword,
334
+ value: chunk.text,
335
+ source: 'png'
336
+ };
337
+ });
338
+ }
339
+ function imageBlockCandidates(blocks) {
340
+ var _blocks_comments;
341
+ const candidates = [];
342
+ if (blocks.xmp) candidates.push(...readXmp(blocks.xmp));
343
+ if (blocks.irb) candidates.push(...readIrbIptc(blocks.irb));
344
+ if (blocks.exif) candidates.push(...readExif(blocks.exif));
345
+ if (blocks.pngText) candidates.push(...pngTextCandidates(blocks.pngText));
346
+ for (const comment of (_blocks_comments = blocks.comments) != null ? _blocks_comments : []){
347
+ candidates.push({
348
+ key: 'comment',
349
+ value: comment,
350
+ source: 'comment'
351
+ });
352
+ }
353
+ if (blocks.icc) {
354
+ const description = readIccDescription(blocks.icc);
355
+ if (description) {
356
+ candidates.push({
357
+ key: 'colorProfile',
358
+ value: description,
359
+ source: 'icc'
360
+ });
361
+ }
362
+ }
363
+ return candidates;
364
+ }
365
+ /** The PDF document-info names each catalog key is written to. */ const PDF_INFO_NAMES = {
366
+ title: 'Title',
367
+ description: 'Subject',
368
+ creator: 'Author',
369
+ keywords: 'Keywords',
370
+ software: 'Creator',
371
+ producer: 'Producer',
372
+ createdAt: 'CreationDate',
373
+ modifiedAt: 'ModDate'
374
+ };
375
+ const PDF_INFO_KEYS = Object.fromEntries(Object.entries(PDF_INFO_NAMES).map(([key, name])=>[
376
+ name,
377
+ key
378
+ ]));
379
+ function pdfInfoCandidates(info) {
380
+ return info.map(({ name, value, editable })=>{
381
+ const key = PDF_INFO_KEYS[name];
382
+ if (key === 'creator') {
383
+ return {
384
+ key,
385
+ value: splitList(value, /;/),
386
+ source: 'pdf',
387
+ editable
388
+ };
389
+ }
390
+ if (key === 'keywords') {
391
+ return {
392
+ key,
393
+ value: splitList(value, /[,;]/),
394
+ source: 'pdf',
395
+ editable
396
+ };
397
+ }
398
+ return key ? {
399
+ key,
400
+ value,
401
+ source: 'pdf',
402
+ editable
403
+ } : {
404
+ key: otherEmbeddedKey('pdf', name),
405
+ label: name,
406
+ value,
407
+ source: 'pdf',
408
+ editable
409
+ };
410
+ });
411
+ }
412
+ const splitList = (value, separator)=>value.split(separator).map((item)=>item.trim()).filter(Boolean);
413
+ async function readAll(reader, format) {
414
+ if (reader.size > MEDIA_EMBEDDED_READ_MAX_BYTES[format]) return null;
415
+ return reader.read(0, reader.size);
416
+ }
417
+ /** Why a format that can normally be written cannot take this file's edits. */ function readOnlyReason(format, pdf) {
418
+ if (pdf == null ? void 0 : pdf.encrypted) return 'This PDF is encrypted, so its details can only be read.';
419
+ if (pdf == null ? void 0 : pdf.signed) {
420
+ return 'This PDF is digitally signed. Changing its details would invalidate ' + 'the signature, so they can only be read.';
421
+ }
422
+ if (MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) return undefined;
423
+ return format === 'mp4' || format === 'webm' ? 'Details inside a video file can be read here but not changed.' : 'Details inside this kind of file can be read here but not changed.';
424
+ }
425
+ /**
426
+ * Read a stored file's embedded metadata.
427
+ *
428
+ * Returns `null` when the format has no reader or the file is past the read
429
+ * cap — the drawer then simply has no "In the file" section, which is the
430
+ * honest thing to show about a CSV. Never throws on the bytes: every format
431
+ * module is written to return what it could read from a hostile file.
432
+ */ export async function readMediaEmbeddedMetadata(options) {
433
+ const { contentType, reader, contentSha256 } = options;
434
+ if (!reader.size) return null;
435
+ const head = await reader.read(0, Math.min(reader.size, 64));
436
+ const format = embeddedFormatFor(contentType, head);
437
+ if (!format) return null;
438
+ const candidates = [];
439
+ let pdfFlags;
440
+ if (format === 'mp4') {
441
+ const read = await readMp4(reader);
442
+ if (!read) return null;
443
+ candidates.push(...read.candidates);
444
+ if (read.xmp) candidates.push(...readXmp(read.xmp));
445
+ } else if (format === 'webm') {
446
+ const read = await readWebm(reader);
447
+ if (!read) return null;
448
+ candidates.push(...read.candidates);
449
+ } else {
450
+ const bytes = await readAll(reader, format);
451
+ if (!bytes) return null;
452
+ switch(format){
453
+ case 'jpeg':
454
+ case 'png':
455
+ case 'webp':
456
+ case 'gif':
457
+ case 'heif':
458
+ {
459
+ const blocks = ({
460
+ jpeg: readJpegBlocks,
461
+ png: readPngBlocks,
462
+ webp: readWebpBlocks,
463
+ gif: readGifBlocks,
464
+ heif: readHeifBlocks
465
+ })[format](bytes);
466
+ if (!blocks) return null;
467
+ candidates.push(...imageBlockCandidates(blocks));
468
+ break;
469
+ }
470
+ case 'tiff':
471
+ {
472
+ const file = readTiffFile(bytes);
473
+ if (!file) return null;
474
+ candidates.push(...readExif(bytes));
475
+ if (file.xmp) candidates.push(...readXmp(file.xmp));
476
+ if (file.irb) candidates.push(...readIrbIptc(file.irb));
477
+ else if (file.iim) candidates.push(...readIim(file.iim));
478
+ break;
479
+ }
480
+ case 'svg':
481
+ {
482
+ const read = readSvg(bytes);
483
+ if (!read) return null;
484
+ candidates.push(...read.candidates);
485
+ if (read.xmp) candidates.push(...readXmp(read.xmp));
486
+ break;
487
+ }
488
+ case 'pdf':
489
+ {
490
+ const read = readPdf(bytes);
491
+ if (!read) return null;
492
+ pdfFlags = {
493
+ encrypted: read.encrypted,
494
+ signed: read.signed
495
+ };
496
+ // An encrypted file's strings are ciphertext without the key, so
497
+ // nothing in its dictionary is worth showing but the facts below.
498
+ if (!read.encrypted) {
499
+ candidates.push(...pdfInfoCandidates(read.info));
500
+ if (read.xmp) {
501
+ candidates.push(...readXmp(read.xmp, {
502
+ profile: 'pdf'
503
+ }));
504
+ }
505
+ }
506
+ if (read.pageCount) {
507
+ candidates.push({
508
+ key: 'pageCount',
509
+ value: String(read.pageCount),
510
+ source: 'pdf'
511
+ });
512
+ }
513
+ if (read.version) {
514
+ candidates.push({
515
+ key: 'pdfVersion',
516
+ value: read.version,
517
+ source: 'pdf'
518
+ });
519
+ }
520
+ break;
521
+ }
522
+ case 'ooxml':
523
+ {
524
+ const read = readOoxml(bytes);
525
+ if (!read) return null;
526
+ candidates.push(...read.candidates);
527
+ break;
528
+ }
529
+ }
530
+ }
531
+ const reason = readOnlyReason(format, pdfFlags);
532
+ const { fields, truncated } = mergeEmbeddedCandidates(format, reason ? candidates.map((c)=>_extends({}, c, {
533
+ editable: false
534
+ })) : candidates);
535
+ return _extends({
536
+ version: MEDIA_EMBEDDED_METADATA_VERSION,
537
+ format,
538
+ fields,
539
+ writable: !reason
540
+ }, reason ? {
541
+ readOnlyReason: reason
542
+ } : {}, truncated ? {
543
+ truncated
544
+ } : {}, contentSha256 ? {
545
+ contentSha256
546
+ } : {});
547
+ }
548
+ /**
549
+ * The canonical keys the XMP writer maps, per profile — every other key in
550
+ * a patch is some other block's business and is kept out of the packet.
551
+ */ const XMP_IMAGE_KEYS = new Set([
552
+ 'title',
553
+ 'headline',
554
+ 'description',
555
+ 'keywords',
556
+ 'instructions',
557
+ 'label',
558
+ 'rating',
559
+ 'creator',
560
+ 'copyright',
561
+ 'credit',
562
+ 'source',
563
+ 'usageTerms',
564
+ 'webStatement',
565
+ 'city',
566
+ 'state',
567
+ 'country',
568
+ 'gps',
569
+ 'createdAt',
570
+ 'software',
571
+ 'make',
572
+ 'model',
573
+ 'lens'
574
+ ]);
575
+ const XMP_PDF_KEYS = new Set([
576
+ 'title',
577
+ 'description',
578
+ 'creator',
579
+ 'keywords',
580
+ 'software',
581
+ 'producer',
582
+ 'createdAt'
583
+ ]);
584
+ function pick(patch, keep) {
585
+ return Object.fromEntries(Object.entries(patch).filter(([key])=>keep(key)));
586
+ }
587
+ const hasEntries = (patch)=>Object.keys(patch).length > 0;
588
+ /** A text keyword's value for a canonical PNG field, or `null` to remove. */ function pngTextValue(key, value) {
589
+ if (value === null) return null;
590
+ if (key === 'createdAt') {
591
+ const parsed = Date.parse(String(value));
592
+ return Number.isFinite(parsed) ? new Date(parsed).toUTCString() : String(value);
593
+ }
594
+ return Array.isArray(value) ? value.join('; ') : value;
595
+ }
596
+ /**
597
+ * The image write: XMP always (creating a packet if the file has none), and
598
+ * the legacy mirrors — IIM, EXIF, PNG text — only where the file already
599
+ * carries them, which is the MWG's rule for not planting a second, older
600
+ * copy of a value in a file that never had one.
601
+ */ function writeImage(format, bytes, patch, now) {
602
+ const xmpPatch = pick(patch, (key)=>{
603
+ var _parseOtherEmbeddedKey;
604
+ return XMP_IMAGE_KEYS.has(key) || ((_parseOtherEmbeddedKey = parseOtherEmbeddedKey(key)) == null ? void 0 : _parseOtherEmbeddedKey.namespace) === 'xmp';
605
+ });
606
+ if (format === 'tiff') {
607
+ const file = readTiffFile(bytes);
608
+ if (!file) throw new EmbeddedWriteError('This TIFF file could not be read.');
609
+ const xmp = hasEntries(xmpPatch) ? writeXmp(file.xmp, xmpPatch, {
610
+ profile: 'image',
611
+ now
612
+ }) : undefined;
613
+ return writeTiffFile(bytes, _extends({
614
+ exifPatch: patch
615
+ }, xmp !== undefined ? {
616
+ xmp
617
+ } : {}, file.iim ? {
618
+ iim: writeIim(file.iim, patch)
619
+ } : {}));
620
+ }
621
+ const read = {
622
+ jpeg: readJpegBlocks,
623
+ png: readPngBlocks,
624
+ webp: readWebpBlocks
625
+ }[format];
626
+ const blocks = read(bytes);
627
+ if (!blocks) {
628
+ throw new EmbeddedWriteError('This file could not be read, so it was left as it was.');
629
+ }
630
+ const update = {};
631
+ if (hasEntries(xmpPatch)) {
632
+ var _blocks_xmp;
633
+ const xmp = writeXmp((_blocks_xmp = blocks.xmp) != null ? _blocks_xmp : null, xmpPatch, {
634
+ profile: 'image',
635
+ now
636
+ });
637
+ if (xmp !== null || blocks.xmp) update.xmp = xmp;
638
+ }
639
+ if (blocks.exif) {
640
+ update.exif = writeExif(blocks.exif, patch, _extends({}, format === 'jpeg' ? {
641
+ maxLength: 65527
642
+ } : {}));
643
+ }
644
+ if (blocks.irb && irbHasIptc(blocks.irb)) {
645
+ update.irb = writeIrbIptc(blocks.irb, patch);
646
+ }
647
+ if (format === 'png') {
648
+ var _blocks_pngText;
649
+ const existing = new Set(((_blocks_pngText = blocks.pngText) != null ? _blocks_pngText : []).map((chunk)=>chunk.keyword));
650
+ const text = {};
651
+ for (const [key, value] of Object.entries(patch)){
652
+ const other = parseOtherEmbeddedKey(key);
653
+ if ((other == null ? void 0 : other.namespace) === 'png') {
654
+ const keyword = other.parts.join('|');
655
+ if (!existing.has(keyword)) {
656
+ throw new EmbeddedWriteError(`${keyword} is not in this file.`);
657
+ }
658
+ text[keyword] = pngTextValue(key, value);
659
+ continue;
660
+ }
661
+ const keyword = PNG_KEYWORD_FOR[key];
662
+ // A comment has nowhere else to live in a PNG, so it is the one text
663
+ // field written whether or not the file had it.
664
+ if (keyword && (existing.has(keyword) || key === 'comment')) {
665
+ text[keyword] = pngTextValue(key, value);
666
+ }
667
+ }
668
+ if (hasEntries(text)) update.pngText = text;
669
+ }
670
+ const write = {
671
+ jpeg: writeJpegBlocks,
672
+ png: writePngBlocks,
673
+ webp: writeWebpBlocks
674
+ }[format];
675
+ return write(bytes, update);
676
+ }
677
+ function writePdfFile(bytes, patch, now) {
678
+ var _writeXmp;
679
+ const read = readPdf(bytes);
680
+ if (!read) {
681
+ throw new EmbeddedWriteError('This PDF could not be read, so it was left as it was.');
682
+ }
683
+ const info = {};
684
+ for (const [key, value] of Object.entries(patch)){
685
+ const other = parseOtherEmbeddedKey(key);
686
+ const name = (other == null ? void 0 : other.namespace) === 'pdf' ? other.parts.join('|') : PDF_INFO_NAMES[key];
687
+ if (!name) continue;
688
+ if (other && !read.info.some((entry)=>entry.name === name)) {
689
+ throw new EmbeddedWriteError(`${name} is not in this PDF.`);
690
+ }
691
+ info[name] = value === null ? null : Array.isArray(value) ? value.join(key === 'creator' ? '; ' : ', ') : value;
692
+ }
693
+ // Every PDF writer stamps the modification date; a viewer's "Modified"
694
+ // that predates a change it can see is a small lie worth not telling.
695
+ info['ModDate'] = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
696
+ const xmpPatch = pick(patch, (key)=>XMP_PDF_KEYS.has(key));
697
+ // XMP is updated where the file has it and never planted where it does
698
+ // not: the document-info dictionary is what a PDF 1.x reader looks at.
699
+ const xmp = read.xmp ? (_writeXmp = writeXmp(read.xmp, xmpPatch, {
700
+ profile: 'pdf',
701
+ now
702
+ })) != null ? _writeXmp : undefined : undefined;
703
+ return writePdf(bytes, _extends({
704
+ info
705
+ }, xmp !== undefined ? {
706
+ xmp
707
+ } : {}));
708
+ }
709
+ /**
710
+ * Write an edit into a file's bytes and return the new bytes.
711
+ *
712
+ * The patch is validated against the catalog first (the route has already
713
+ * done so for the person's benefit; this is for every other caller), then
714
+ * written by the format module. Throws `EmbeddedWriteError` — a sentence for
715
+ * the person who asked — when the file cannot take it safely; anything else
716
+ * thrown is a fault.
717
+ */ export function writeMediaEmbeddedMetadata(options) {
718
+ var _options_now;
719
+ const { contentType, bytes } = options;
720
+ const now = (_options_now = options.now) != null ? _options_now : new Date();
721
+ const format = embeddedFormatFor(contentType, bytes.subarray(0, 64));
722
+ if (!format || !MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) {
723
+ throw new EmbeddedWriteError('Details inside this kind of file can be read here but not changed.');
724
+ }
725
+ const checked = sanitizeEmbeddedPatch(format, options.patch);
726
+ if ('error' in checked) throw new EmbeddedWriteError(checked.error);
727
+ switch(format){
728
+ case 'jpeg':
729
+ case 'png':
730
+ case 'webp':
731
+ case 'tiff':
732
+ return writeImage(format, bytes, checked.patch, now);
733
+ case 'pdf':
734
+ return writePdfFile(bytes, checked.patch, now);
735
+ case 'ooxml':
736
+ return writeOoxml(bytes, checked.patch, {
737
+ now
738
+ });
739
+ default:
740
+ throw new EmbeddedWriteError('Details inside this kind of file can be read here but not changed.');
741
+ }
742
+ }
743
+
744
+ //# sourceMappingURL=index.js.map