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