@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,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
|