@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,619 @@
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
+ * The metadata a file carries INSIDE its own bytes (AGL-3331): EXIF, IPTC
18
+ * and XMP on a photo, the document info of a PDF, the core and custom
19
+ * properties of an Office file, the tags of a video container.
20
+ *
21
+ * ## Why this module is client-safe and the parsers are not
22
+ *
23
+ * The parsers and writers live in `media-embedded-metadata/` and are exposed
24
+ * only through `@aglyn/aglyn/server` — they need `node:zlib` and
25
+ * `node:crypto`, and nothing in a browser ever holds the bytes they read.
26
+ * What the console needs is the SHAPE of what they produced (it is stored on
27
+ * the media document) and the rules for presenting and editing it, which is
28
+ * everything here. Keeping the two apart keeps a PDF parser out of the
29
+ * console bundle.
30
+ *
31
+ * ## Canonical keys
32
+ *
33
+ * A photo's caption can be written in three places at once — XMP
34
+ * `dc:description`, IPTC 2:120 and EXIF `ImageDescription` — and a PDF's in
35
+ * two. The reader folds every spelling of one idea into one canonical key
36
+ * ({@link MEDIA_EMBEDDED_CATALOG}), so the drawer shows ONE "Description"
37
+ * rather than three rows that may or may not agree, and the writer fans an
38
+ * edit back out to every place that already holds it (the Metadata Working
39
+ * Group's rule: never leave a stale copy for the next reader to prefer).
40
+ *
41
+ * Anything a file carries that is not in the catalog — an XMP property from
42
+ * a DAM we have never heard of, a PDF custom property, a PNG text chunk —
43
+ * still surfaces, under a namespaced "other" key ({@link otherEmbeddedKey})
44
+ * that round-trips to the writer unchanged.
45
+ */ /** Where a value was read from. Several sources can back one field. */ import { _ as _extends } from "@swc/helpers/_/_extends";
46
+ export const MEDIA_EMBEDDED_GROUP_ORDER = [
47
+ 'description',
48
+ 'rights',
49
+ 'location',
50
+ 'capture',
51
+ 'document',
52
+ 'technical',
53
+ 'other'
54
+ ];
55
+ export const MEDIA_EMBEDDED_GROUP_LABELS = {
56
+ description: 'Description',
57
+ rights: 'Rights',
58
+ location: 'Location',
59
+ capture: 'Camera',
60
+ document: 'Document',
61
+ technical: 'Technical',
62
+ other: 'Other embedded fields'
63
+ };
64
+ /**
65
+ * The formats whose bytes can take an edit without being re-encoded.
66
+ *
67
+ * Every writer here rewrites METADATA containers only — JPEG APP segments,
68
+ * PNG ancillary chunks, WebP RIFF chunks, a TIFF's IFD, a PDF incremental
69
+ * update, an Office package's property parts — and copies the image data,
70
+ * page content and every other part through byte for byte. A format that
71
+ * cannot promise that (a video's `moov` would move its sample offsets; an
72
+ * SVG is sanitized on every ingress and is its own markup) is read-only.
73
+ */ export const MEDIA_EMBEDDED_WRITABLE_FORMATS = new Set([
74
+ 'jpeg',
75
+ 'png',
76
+ 'webp',
77
+ 'tiff',
78
+ 'pdf',
79
+ 'ooxml'
80
+ ]);
81
+ const IMAGE_WRITABLE = [
82
+ 'jpeg',
83
+ 'png',
84
+ 'webp',
85
+ 'tiff'
86
+ ];
87
+ const IMAGE_AND_PDF = [
88
+ ...IMAGE_WRITABLE,
89
+ 'pdf'
90
+ ];
91
+ const IMAGE_PDF_OOXML = [
92
+ ...IMAGE_AND_PDF,
93
+ 'ooxml'
94
+ ];
95
+ /**
96
+ * Every canonical key, in display order within its group.
97
+ *
98
+ * ## Value formats every reader and writer agrees on
99
+ *
100
+ * - `date`: ISO 8601 — `2021-05-03T10:11:12`, with `Z` or `±HH:MM` only when
101
+ * the source recorded a zone (EXIF 2.3's `OffsetTimeOriginal`, a PDF
102
+ * date's `+05'00'`). Never invented: a local camera time stays local.
103
+ * - `list`: an array of strings (creators, keywords).
104
+ * - `gps`: `"<lat>,<lon>"` in signed decimal degrees, six places.
105
+ * - `rating`: `"0"`–`"5"`, XMP's scale; `"-1"` is XMP's "rejected".
106
+ * - technical values are display strings already (`1/250`, `f/2.8`,
107
+ * `35 mm`) because nothing edits them.
108
+ */ export const MEDIA_EMBEDDED_CATALOG = {
109
+ title: {
110
+ label: 'Title',
111
+ group: 'description',
112
+ kind: 'text',
113
+ writable: IMAGE_PDF_OOXML
114
+ },
115
+ headline: {
116
+ label: 'Headline',
117
+ group: 'description',
118
+ kind: 'text',
119
+ writable: IMAGE_WRITABLE
120
+ },
121
+ description: {
122
+ label: 'Description',
123
+ group: 'description',
124
+ kind: 'longText',
125
+ writable: IMAGE_PDF_OOXML,
126
+ labels: {
127
+ pdf: 'Subject',
128
+ ooxml: 'Comments'
129
+ }
130
+ },
131
+ subject: {
132
+ label: 'Subject',
133
+ group: 'description',
134
+ kind: 'text',
135
+ writable: [
136
+ 'ooxml'
137
+ ]
138
+ },
139
+ keywords: {
140
+ label: 'Keywords',
141
+ group: 'description',
142
+ kind: 'list',
143
+ writable: IMAGE_PDF_OOXML
144
+ },
145
+ category: {
146
+ label: 'Category',
147
+ group: 'description',
148
+ kind: 'text',
149
+ writable: [
150
+ 'ooxml'
151
+ ]
152
+ },
153
+ comment: {
154
+ label: 'Comment',
155
+ group: 'description',
156
+ kind: 'longText',
157
+ writable: [
158
+ 'png'
159
+ ]
160
+ },
161
+ instructions: {
162
+ label: 'Instructions',
163
+ group: 'description',
164
+ kind: 'longText',
165
+ writable: IMAGE_WRITABLE
166
+ },
167
+ label: {
168
+ label: 'Label',
169
+ group: 'description',
170
+ kind: 'text',
171
+ writable: IMAGE_WRITABLE
172
+ },
173
+ rating: {
174
+ label: 'Rating',
175
+ group: 'description',
176
+ kind: 'rating',
177
+ writable: IMAGE_WRITABLE
178
+ },
179
+ creator: {
180
+ label: 'Creator',
181
+ group: 'rights',
182
+ kind: 'list',
183
+ writable: IMAGE_PDF_OOXML,
184
+ labels: {
185
+ pdf: 'Author',
186
+ ooxml: 'Author'
187
+ }
188
+ },
189
+ copyright: {
190
+ label: 'Copyright',
191
+ group: 'rights',
192
+ kind: 'text',
193
+ writable: IMAGE_WRITABLE
194
+ },
195
+ credit: {
196
+ label: 'Credit line',
197
+ group: 'rights',
198
+ kind: 'text',
199
+ writable: IMAGE_WRITABLE
200
+ },
201
+ source: {
202
+ label: 'Source',
203
+ group: 'rights',
204
+ kind: 'text',
205
+ writable: IMAGE_WRITABLE
206
+ },
207
+ usageTerms: {
208
+ label: 'Usage terms',
209
+ group: 'rights',
210
+ kind: 'longText',
211
+ writable: IMAGE_WRITABLE
212
+ },
213
+ webStatement: {
214
+ label: 'Copyright info URL',
215
+ group: 'rights',
216
+ kind: 'text',
217
+ writable: IMAGE_WRITABLE
218
+ },
219
+ city: {
220
+ label: 'City',
221
+ group: 'location',
222
+ kind: 'text',
223
+ writable: IMAGE_WRITABLE
224
+ },
225
+ state: {
226
+ label: 'State / province',
227
+ group: 'location',
228
+ kind: 'text',
229
+ writable: IMAGE_WRITABLE
230
+ },
231
+ country: {
232
+ label: 'Country',
233
+ group: 'location',
234
+ kind: 'text',
235
+ writable: IMAGE_WRITABLE
236
+ },
237
+ gps: {
238
+ label: 'GPS position',
239
+ group: 'location',
240
+ kind: 'gps',
241
+ writable: IMAGE_WRITABLE,
242
+ removeOnly: true
243
+ },
244
+ gpsAltitude: {
245
+ label: 'Altitude',
246
+ group: 'location',
247
+ kind: 'text'
248
+ },
249
+ createdAt: {
250
+ label: 'Date taken',
251
+ group: 'capture',
252
+ kind: 'date',
253
+ writable: IMAGE_AND_PDF,
254
+ labels: {
255
+ pdf: 'Created',
256
+ ooxml: 'Created',
257
+ svg: 'Created',
258
+ mp4: 'Recorded',
259
+ webm: 'Recorded'
260
+ },
261
+ groups: {
262
+ pdf: 'document',
263
+ ooxml: 'document',
264
+ svg: 'document'
265
+ }
266
+ },
267
+ make: {
268
+ label: 'Camera make',
269
+ group: 'capture',
270
+ kind: 'text',
271
+ writable: IMAGE_WRITABLE
272
+ },
273
+ model: {
274
+ label: 'Camera model',
275
+ group: 'capture',
276
+ kind: 'text',
277
+ writable: IMAGE_WRITABLE
278
+ },
279
+ lens: {
280
+ label: 'Lens',
281
+ group: 'capture',
282
+ kind: 'text',
283
+ writable: IMAGE_WRITABLE
284
+ },
285
+ exposure: {
286
+ label: 'Exposure',
287
+ group: 'capture',
288
+ kind: 'text'
289
+ },
290
+ aperture: {
291
+ label: 'Aperture',
292
+ group: 'capture',
293
+ kind: 'text'
294
+ },
295
+ iso: {
296
+ label: 'ISO',
297
+ group: 'capture',
298
+ kind: 'text'
299
+ },
300
+ focalLength: {
301
+ label: 'Focal length',
302
+ group: 'capture',
303
+ kind: 'text'
304
+ },
305
+ flash: {
306
+ label: 'Flash',
307
+ group: 'capture',
308
+ kind: 'text'
309
+ },
310
+ modifiedAt: {
311
+ label: 'Modified',
312
+ group: 'document',
313
+ kind: 'date'
314
+ },
315
+ software: {
316
+ label: 'Software',
317
+ group: 'document',
318
+ kind: 'text',
319
+ writable: IMAGE_AND_PDF,
320
+ labels: {
321
+ pdf: 'Created with',
322
+ ooxml: 'Application'
323
+ }
324
+ },
325
+ producer: {
326
+ label: 'PDF producer',
327
+ group: 'document',
328
+ kind: 'text',
329
+ writable: [
330
+ 'pdf'
331
+ ]
332
+ },
333
+ lastModifiedBy: {
334
+ label: 'Last modified by',
335
+ group: 'document',
336
+ kind: 'text'
337
+ },
338
+ company: {
339
+ label: 'Company',
340
+ group: 'document',
341
+ kind: 'text'
342
+ },
343
+ pageCount: {
344
+ label: 'Pages',
345
+ group: 'document',
346
+ kind: 'text',
347
+ labels: {
348
+ ooxml: 'Pages / slides'
349
+ }
350
+ },
351
+ pdfVersion: {
352
+ label: 'PDF version',
353
+ group: 'document',
354
+ kind: 'text'
355
+ },
356
+ orientation: {
357
+ label: 'Orientation',
358
+ group: 'technical',
359
+ kind: 'text'
360
+ },
361
+ colorProfile: {
362
+ label: 'Color profile',
363
+ group: 'technical',
364
+ kind: 'text'
365
+ },
366
+ encoder: {
367
+ label: 'Encoder',
368
+ group: 'technical',
369
+ kind: 'text'
370
+ }
371
+ };
372
+ export function otherEmbeddedKey(namespace, ...parts) {
373
+ return [
374
+ namespace,
375
+ ...parts
376
+ ].join('|');
377
+ }
378
+ export function parseOtherEmbeddedKey(key) {
379
+ const [namespace = '', ...parts] = key.split('|');
380
+ if (!parts.length) return null;
381
+ if (![
382
+ 'xmp',
383
+ 'png',
384
+ 'pdf',
385
+ 'ooxml',
386
+ 'mp4',
387
+ 'webm'
388
+ ].includes(namespace)) return null;
389
+ return {
390
+ namespace: namespace,
391
+ parts
392
+ };
393
+ }
394
+ /** The "other" namespaces a writer accepts edits for, by format. */ const OTHER_WRITABLE = {
395
+ xmp: IMAGE_WRITABLE,
396
+ png: [
397
+ 'png'
398
+ ],
399
+ pdf: [
400
+ 'pdf'
401
+ ],
402
+ ooxml: [
403
+ 'ooxml'
404
+ ]
405
+ };
406
+ /** The reader's output version. See {@link MediaEmbeddedMetadata.version}. */ export const MEDIA_EMBEDDED_METADATA_VERSION = 1;
407
+ /** Stored caps: one media document must never approach Firestore's 1 MiB. */ export const MEDIA_EMBEDDED_MAX_FIELDS = 150;
408
+ export const MEDIA_EMBEDDED_MAX_VALUE_LENGTH = 2000;
409
+ export const MEDIA_EMBEDDED_MAX_LIST_ITEMS = 100;
410
+ /** A single keyword or creator; IPTC's own limit for keywords is 64 bytes. */ export const MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH = 256;
411
+ export function isCanonicalEmbeddedKey(key) {
412
+ return Object.prototype.hasOwnProperty.call(MEDIA_EMBEDDED_CATALOG, key);
413
+ }
414
+ /** The label a key carries on a given format. */ export function embeddedFieldLabel(key, format, fallback) {
415
+ var _ref;
416
+ var _parseOtherEmbeddedKey;
417
+ if (isCanonicalEmbeddedKey(key)) {
418
+ var _ref1;
419
+ var _entry_labels;
420
+ const entry = MEDIA_EMBEDDED_CATALOG[key];
421
+ return (_ref1 = (_entry_labels = entry.labels) == null ? void 0 : _entry_labels[format]) != null ? _ref1 : entry.label;
422
+ }
423
+ return (_ref = fallback != null ? fallback : (_parseOtherEmbeddedKey = parseOtherEmbeddedKey(key)) == null ? void 0 : _parseOtherEmbeddedKey.parts.at(-1)) != null ? _ref : key;
424
+ }
425
+ /** The group a key is shown under on a given format. */ export function embeddedFieldGroup(key, format) {
426
+ var _ref;
427
+ var _entry_groups;
428
+ if (!isCanonicalEmbeddedKey(key)) return 'other';
429
+ const entry = MEDIA_EMBEDDED_CATALOG[key];
430
+ return (_ref = (_entry_groups = entry.groups) == null ? void 0 : _entry_groups[format]) != null ? _ref : entry.group;
431
+ }
432
+ /** How a key's value is edited. Other keys are free text. */ export function embeddedFieldKind(key) {
433
+ return isCanonicalEmbeddedKey(key) ? MEDIA_EMBEDDED_CATALOG[key].kind : 'text';
434
+ }
435
+ /**
436
+ * Whether an edit of `key` can be written into a file of `format`.
437
+ *
438
+ * The one place the answer lives: the reader stamps `editable` with it, the
439
+ * drawer offers the "add a field" choices from it, and the write route
440
+ * refuses anything it says no to — so the three cannot disagree about which
441
+ * fields a JPEG can take.
442
+ */ export function embeddedKeyWritable(key, format) {
443
+ var _OTHER_WRITABLE_other_namespace;
444
+ if (!MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) return false;
445
+ if (isCanonicalEmbeddedKey(key)) {
446
+ var _entry_writable;
447
+ const entry = MEDIA_EMBEDDED_CATALOG[key];
448
+ return Boolean((_entry_writable = entry.writable) == null ? void 0 : _entry_writable.includes(format));
449
+ }
450
+ const other = parseOtherEmbeddedKey(key);
451
+ return Boolean(other && ((_OTHER_WRITABLE_other_namespace = OTHER_WRITABLE[other.namespace]) == null ? void 0 : _OTHER_WRITABLE_other_namespace.includes(format)));
452
+ }
453
+ /**
454
+ * The canonical keys a file of this format can take, whether or not it
455
+ * holds them yet — what the drawer offers under "Add a field".
456
+ */ export function embeddedWritableKeys(format) {
457
+ return Object.keys(MEDIA_EMBEDDED_CATALOG).filter((key)=>embeddedKeyWritable(key, format));
458
+ }
459
+ /** A strict ISO 8601 date or date-time, the only date a writer accepts. */ const ISO_DATE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})?)?$/;
460
+ /**
461
+ * Validate an edit before it reaches a writer, or say why it cannot.
462
+ *
463
+ * Runs in the drawer (to explain) and in the write route (to refuse), so
464
+ * the messages are written for the person who typed the value.
465
+ */ export function sanitizeEmbeddedPatch(format, patch) {
466
+ if (!MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) {
467
+ return {
468
+ error: 'This kind of file cannot take metadata edits.'
469
+ };
470
+ }
471
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
472
+ return {
473
+ error: 'Nothing to change.'
474
+ };
475
+ }
476
+ const out = {};
477
+ for (const [rawKey, raw] of Object.entries(patch)){
478
+ const key = rawKey.trim();
479
+ if (!embeddedKeyWritable(key, format)) {
480
+ return {
481
+ error: `${embeddedFieldLabel(key, format)} cannot be written into this file.`
482
+ };
483
+ }
484
+ const label = embeddedFieldLabel(key, format);
485
+ if (raw === null) {
486
+ out[key] = null;
487
+ continue;
488
+ }
489
+ const entry = isCanonicalEmbeddedKey(key) ? MEDIA_EMBEDDED_CATALOG[key] : undefined;
490
+ if (entry == null ? void 0 : entry.removeOnly) {
491
+ return {
492
+ error: `${label} can only be removed.`
493
+ };
494
+ }
495
+ const kind = embeddedFieldKind(key);
496
+ if (kind === 'list') {
497
+ const items = (Array.isArray(raw) ? raw : String(raw).split(/[,;\n]/)).map((item)=>String(item).trim()).filter(Boolean);
498
+ if (items.length > MEDIA_EMBEDDED_MAX_LIST_ITEMS) {
499
+ return {
500
+ error: `${label} holds at most ${MEDIA_EMBEDDED_MAX_LIST_ITEMS} entries.`
501
+ };
502
+ }
503
+ if (items.some((item)=>item.length > MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH)) {
504
+ return {
505
+ error: `An entry in ${label} is too long.`
506
+ };
507
+ }
508
+ out[key] = items.length ? [
509
+ ...new Set(items)
510
+ ] : null;
511
+ continue;
512
+ }
513
+ if (typeof raw !== 'string') {
514
+ return {
515
+ error: `${label} must be text.`
516
+ };
517
+ }
518
+ const value = raw.trim();
519
+ if (!value) {
520
+ out[key] = null;
521
+ continue;
522
+ }
523
+ if (value.length > MEDIA_EMBEDDED_MAX_VALUE_LENGTH) {
524
+ return {
525
+ error: `${label} is too long.`
526
+ };
527
+ }
528
+ if (kind === 'date' && !ISO_DATE.test(value)) {
529
+ return {
530
+ error: `${label} must be a date.`
531
+ };
532
+ }
533
+ if (kind === 'rating' && !/^(?:-1|[0-5])$/.test(value)) {
534
+ return {
535
+ error: `${label} must be between 0 and 5.`
536
+ };
537
+ }
538
+ out[key] = value;
539
+ }
540
+ if (!Object.keys(out).length) return {
541
+ error: 'Nothing to change.'
542
+ };
543
+ return {
544
+ patch: out
545
+ };
546
+ }
547
+ /** The value of a field as one line of text. */ export function embeddedFieldText(field) {
548
+ var _field_value;
549
+ return field.values ? field.values.join(', ') : (_field_value = field.value) != null ? _field_value : '';
550
+ }
551
+ /**
552
+ * The stored content types a reader exists for — the drawer asks the
553
+ * server to read a file only when this says there is something to find, so
554
+ * a CSV never costs a round trip to learn it has no caption.
555
+ */ const READABLE_TYPES = new Set([
556
+ 'image/jpeg',
557
+ 'image/png',
558
+ 'image/webp',
559
+ 'image/gif',
560
+ 'image/tiff',
561
+ 'image/heic',
562
+ 'image/heif',
563
+ 'image/avif',
564
+ 'image/svg+xml',
565
+ 'application/pdf',
566
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
567
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
568
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
569
+ 'video/mp4',
570
+ 'video/quicktime',
571
+ 'video/webm'
572
+ ]);
573
+ export function embeddedMetadataReadable(contentType) {
574
+ var _ref;
575
+ var _String_toLowerCase_split_;
576
+ return READABLE_TYPES.has((_ref = (_String_toLowerCase_split_ = String(contentType != null ? contentType : '').toLowerCase().split(';')[0]) == null ? void 0 : _String_toLowerCase_split_.trim()) != null ? _ref : '');
577
+ }
578
+ /**
579
+ * A field's value as the drawer shows it — the stored value is the
580
+ * machine form every writer agrees on, and this is the only place it is
581
+ * dressed for a person.
582
+ */ export function embeddedFieldDisplay(field, locale) {
583
+ var _field_value;
584
+ if (field.values) return field.values.join(', ');
585
+ const value = (_field_value = field.value) != null ? _field_value : '';
586
+ switch(embeddedFieldKind(field.key)){
587
+ case 'date':
588
+ {
589
+ const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value);
590
+ // A date-only value parsed by `Date` is UTC midnight, which renders as
591
+ // the previous day west of Greenwich; noon keeps it on its own date.
592
+ const parsed = new Date(dateOnly ? `${value}T12:00:00` : value);
593
+ if (Number.isNaN(parsed.getTime())) return value;
594
+ return parsed.toLocaleString(locale, _extends({
595
+ dateStyle: 'medium'
596
+ }, dateOnly ? {} : {
597
+ timeStyle: 'short'
598
+ }));
599
+ }
600
+ case 'gps':
601
+ {
602
+ const [lat, lon] = value.split(',').map(Number);
603
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) return value;
604
+ const part = (n, positive, negative)=>`${Math.abs(n).toFixed(5)}° ${n < 0 ? negative : positive}`;
605
+ return `${part(lat, 'N', 'S')}, ${part(lon, 'E', 'W')}`;
606
+ }
607
+ case 'rating':
608
+ {
609
+ const stars = Number(value);
610
+ if (stars === -1) return 'Rejected';
611
+ if (!Number.isInteger(stars) || stars < 0 || stars > 5) return value;
612
+ return '★'.repeat(stars) + '☆'.repeat(5 - stars);
613
+ }
614
+ default:
615
+ return value;
616
+ }
617
+ }
618
+
619
+ //# sourceMappingURL=media-embedded-fields.js.map