@aglyn/aglyn 1.0.0-beta.198 → 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 (77) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/docs-index.generated.js +1 -1
  3. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  4. package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
  5. package/src/lib/app-utils/media-embedded-fields.js +619 -0
  6. package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
  7. package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
  8. package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
  9. package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
  10. package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
  11. package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
  12. package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
  13. package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
  14. package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
  15. package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
  16. package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
  17. package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
  18. package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
  19. package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
  20. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
  21. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
  22. package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
  23. package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
  24. package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
  25. package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
  26. package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
  27. package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
  28. package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
  29. package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
  30. package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
  31. package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
  32. package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
  33. package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
  34. package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
  35. package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
  36. package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
  37. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
  38. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
  39. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
  40. package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
  41. package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
  42. package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
  43. package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
  44. package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
  45. package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
  46. package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
  47. package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
  48. package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
  49. package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
  50. package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
  51. package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
  52. package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
  53. package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
  54. package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
  55. package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
  56. package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
  57. package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
  58. package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
  59. package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
  60. package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
  61. package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
  62. package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
  63. package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
  64. package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
  65. package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
  66. package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
  67. package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
  68. package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
  69. package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
  70. package/src/lib/app-utils/name-search.d.ts +12 -0
  71. package/src/lib/app-utils/name-search.js +11 -0
  72. package/src/lib/app-utils/name-search.js.map +1 -1
  73. package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
  74. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  75. package/src/server.d.ts +1 -0
  76. package/src/server.js +7 -0
  77. package/src/server.js.map +1 -1
@@ -0,0 +1,1267 @@
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
+ * EXIF, and the TIFF structure that carries it (AGL-3331).
19
+ *
20
+ * A JPEG's APP1 `Exif\0\0` segment, a WebP `EXIF` chunk, a PNG `eXIf` chunk
21
+ * and a TIFF file all hold the same thing: a TIFF header followed by image
22
+ * file directories (IFDs), TIFF 6.0 §2, which EXIF 2.32 §4.5 adopts as is.
23
+ * Every function here takes the bytes from the byte-order mark (`II*\0` or
24
+ * `MM\0*`) on; containers strip their own framing first.
25
+ *
26
+ * ## Reading
27
+ *
28
+ * {@link readExif} maps IFD0, the Exif IFD (pointer 0x8769) and the GPS IFD
29
+ * (pointer 0x8825) onto the canonical keys of `media-embedded-fields.ts`.
30
+ * Strings are ASCII by the spec and UTF-8 in real files, so they decode as
31
+ * UTF-8 and fall back to Latin-1. Every offset and count is checked against
32
+ * the block, the walk refuses cycles, and a damaged block yields whatever it
33
+ * could read rather than an exception. BigTIFF (version 43) reads too.
34
+ *
35
+ * ## Writing without moving anything
36
+ *
37
+ * A TIFF block is full of absolute offsets this module cannot see: a
38
+ * MakerNote's private IFD points into itself by offset from the TIFF header,
39
+ * a thumbnail is found through IFD1, a TIFF file's pixels through
40
+ * StripOffsets. So the writer NEVER MOVES A BYTE IT DID NOT CREATE:
41
+ *
42
+ * - A value that fits its old slot (four bytes inside the entry, or the old
43
+ * out-of-line length) is overwritten in place and NUL-filled.
44
+ * - A larger value is appended at the end of the block and its entry
45
+ * repointed. An entry is a fixed 12 bytes, so nothing else shifts.
46
+ * - Adding or removing an entry relocates THAT IFD: a fresh copy, entries
47
+ * sorted by tag (TIFF 6.0 §2 "the entries must be sorted in ascending
48
+ * order by the Tag"), next-IFD pointer preserved, goes at the end, and
49
+ * whatever pointed at the old one (the header for IFD0, 0x8769, 0x8825)
50
+ * is repointed.
51
+ * - Bytes nothing references any more (a replaced value's old slot, a
52
+ * relocated IFD's old table, a scrubbed GPS IFD) are zeroed, so a removed
53
+ * value is gone from the file rather than merely unreferenced.
54
+ *
55
+ * Before overwriting or zeroing an old slot the writer checks that no other
56
+ * structure it knows of (another entry's value, an IFD table, strips, tiles,
57
+ * the thumbnail) claims any of those bytes; a shared slot is left alone and
58
+ * the new value appended instead.
59
+ */ import { EmbeddedWriteError } from "./types.js";
60
+ /**
61
+ * Field types: TIFF 6.0 §2 "Types", TIFF Technical Note 1 (IFD) and the
62
+ * BigTIFF extension (LONG8, SLONG8, IFD8).
63
+ */ export const TIFF_TYPE = {
64
+ BYTE: 1,
65
+ ASCII: 2,
66
+ SHORT: 3,
67
+ LONG: 4,
68
+ RATIONAL: 5,
69
+ SBYTE: 6,
70
+ UNDEFINED: 7,
71
+ SSHORT: 8,
72
+ SLONG: 9,
73
+ SRATIONAL: 10,
74
+ FLOAT: 11,
75
+ DOUBLE: 12,
76
+ IFD: 13,
77
+ LONG8: 16,
78
+ SLONG8: 17,
79
+ IFD8: 18
80
+ };
81
+ const T = TIFF_TYPE;
82
+ /** Bytes per value, by field type. An unknown type has no known extent. */ const TYPE_SIZE = {
83
+ [T.BYTE]: 1,
84
+ [T.ASCII]: 1,
85
+ [T.SHORT]: 2,
86
+ [T.LONG]: 4,
87
+ [T.RATIONAL]: 8,
88
+ [T.SBYTE]: 1,
89
+ [T.UNDEFINED]: 1,
90
+ [T.SSHORT]: 2,
91
+ [T.SLONG]: 4,
92
+ [T.SRATIONAL]: 8,
93
+ [T.FLOAT]: 4,
94
+ [T.DOUBLE]: 8,
95
+ [T.IFD]: 4,
96
+ [T.LONG8]: 8,
97
+ [T.SLONG8]: 8,
98
+ [T.IFD8]: 8
99
+ };
100
+ /** Tag numbers: TIFF 6.0 Appendix A and EXIF 2.32 §4.6. */ const TAG = {
101
+ imageDescription: 0x010e,
102
+ make: 0x010f,
103
+ model: 0x0110,
104
+ stripOffsets: 0x0111,
105
+ orientation: 0x0112,
106
+ stripByteCounts: 0x0117,
107
+ software: 0x0131,
108
+ dateTime: 0x0132,
109
+ artist: 0x013b,
110
+ tileOffsets: 0x0144,
111
+ tileByteCounts: 0x0145,
112
+ subIfds: 0x014a,
113
+ jpegInterchangeFormat: 0x0201,
114
+ jpegInterchangeFormatLength: 0x0202,
115
+ copyright: 0x8298,
116
+ exposureTime: 0x829a,
117
+ fNumber: 0x829d,
118
+ exifIfd: 0x8769,
119
+ gpsIfd: 0x8825,
120
+ iso: 0x8827,
121
+ dateTimeOriginal: 0x9003,
122
+ offsetTime: 0x9010,
123
+ offsetTimeOriginal: 0x9011,
124
+ flash: 0x9209,
125
+ focalLength: 0x920a,
126
+ makerNote: 0x927c,
127
+ userComment: 0x9286,
128
+ xpTitle: 0x9c9b,
129
+ xpComment: 0x9c9c,
130
+ xpAuthor: 0x9c9d,
131
+ xpKeywords: 0x9c9e,
132
+ interopIfd: 0xa005,
133
+ lensModel: 0xa434
134
+ };
135
+ /** GPS IFD tags, EXIF 2.32 §4.6.6. */ const GPS = {
136
+ latitudeRef: 0x0001,
137
+ latitude: 0x0002,
138
+ longitudeRef: 0x0003,
139
+ longitude: 0x0004,
140
+ altitudeRef: 0x0005,
141
+ altitude: 0x0006
142
+ };
143
+ /**
144
+ * Tags that locate other structures by offset. Rewriting one would orphan
145
+ * or corrupt what it points at, so {@link writeTiffTags} refuses them.
146
+ */ const STRUCTURAL_TAGS = new Set([
147
+ TAG.stripOffsets,
148
+ TAG.stripByteCounts,
149
+ TAG.tileOffsets,
150
+ TAG.tileByteCounts,
151
+ TAG.subIfds,
152
+ TAG.jpegInterchangeFormat,
153
+ TAG.jpegInterchangeFormatLength,
154
+ TAG.exifIfd,
155
+ TAG.gpsIfd,
156
+ TAG.interopIfd,
157
+ TAG.makerNote
158
+ ]);
159
+ /** Offset/length tag pairs whose data the writer must never touch. */ const DATA_PAIRS = [
160
+ [
161
+ TAG.stripOffsets,
162
+ TAG.stripByteCounts
163
+ ],
164
+ [
165
+ TAG.tileOffsets,
166
+ TAG.tileByteCounts
167
+ ],
168
+ [
169
+ TAG.jpegInterchangeFormat,
170
+ TAG.jpegInterchangeFormatLength
171
+ ]
172
+ ];
173
+ /** Bounds on the walk, far above anything a camera or editor writes. */ const MAX_IFDS = 64;
174
+ const MAX_DATA_RANGES = 65536;
175
+ /** EXIF 2.32 §4.6.5 Orientation, worded as ExifTool prints it. */ const ORIENTATION = {
176
+ 1: 'Horizontal (normal)',
177
+ 2: 'Mirror horizontal',
178
+ 3: 'Rotate 180',
179
+ 4: 'Mirror vertical',
180
+ 5: 'Mirror horizontal and rotate 270 CW',
181
+ 6: 'Rotate 90 CW',
182
+ 7: 'Mirror horizontal and rotate 90 CW',
183
+ 8: 'Rotate 270 CW'
184
+ };
185
+ // ---------------------------------------------------------------------------
186
+ // Bytes
187
+ // ---------------------------------------------------------------------------
188
+ function get16(b, at, little) {
189
+ var _b_at, _b_;
190
+ const x = (_b_at = b[at]) != null ? _b_at : 0;
191
+ const y = (_b_ = b[at + 1]) != null ? _b_ : 0;
192
+ return little ? x | y << 8 : x << 8 | y;
193
+ }
194
+ function get32(b, at, little) {
195
+ const lo = get16(b, little ? at : at + 2, little);
196
+ const hi = get16(b, little ? at + 2 : at, little);
197
+ return hi * 0x10000 + lo;
198
+ }
199
+ /**
200
+ * A 64-bit value as a double. Past 2^53 it loses precision, which never
201
+ * matters: anything that large fails every bounds check anyway.
202
+ */ function get64(b, at, little) {
203
+ const lo = get32(b, little ? at : at + 4, little);
204
+ const hi = get32(b, little ? at + 4 : at, little);
205
+ return hi * 0x100000000 + lo;
206
+ }
207
+ function put16(b, at, value, little) {
208
+ b[little ? at : at + 1] = value & 0xff;
209
+ b[little ? at + 1 : at] = value >>> 8 & 0xff;
210
+ }
211
+ function put32(b, at, value, little) {
212
+ put16(b, little ? at : at + 2, value & 0xffff, little);
213
+ put16(b, little ? at + 2 : at, Math.floor(value / 0x10000) & 0xffff, little);
214
+ }
215
+ function encode32(value, little) {
216
+ const out = new Uint8Array(4);
217
+ put32(out, 0, value, little);
218
+ return out;
219
+ }
220
+ function concat(...parts) {
221
+ const out = new Uint8Array(parts.reduce((n, part)=>n + part.length, 0));
222
+ let at = 0;
223
+ for (const part of parts){
224
+ out.set(part, at);
225
+ at += part.length;
226
+ }
227
+ return out;
228
+ }
229
+ function sameBytes(a, b) {
230
+ if (a.length !== b.length) return false;
231
+ for(let i = 0; i < a.length; i++)if (a[i] !== b[i]) return false;
232
+ return true;
233
+ }
234
+ // ---------------------------------------------------------------------------
235
+ // Text
236
+ // ---------------------------------------------------------------------------
237
+ const utf8Strict = new TextDecoder('utf-8', {
238
+ fatal: true
239
+ });
240
+ const utf16le = new TextDecoder('utf-16le');
241
+ const encoder = new TextEncoder();
242
+ /**
243
+ * Windows-1252's 0x80–0x9F, the only range where it differs from ISO
244
+ * 8859-1. "Latin-1" text in the wild is Windows-1252, and the WHATWG
245
+ * Encoding standard decodes the `latin1` label the same way.
246
+ */ const CP1252_HIGH = [
247
+ 0x20ac,
248
+ 0x81,
249
+ 0x201a,
250
+ 0x192,
251
+ 0x201e,
252
+ 0x2026,
253
+ 0x2020,
254
+ 0x2021,
255
+ 0x2c6,
256
+ 0x2030,
257
+ 0x160,
258
+ 0x2039,
259
+ 0x152,
260
+ 0x8d,
261
+ 0x17d,
262
+ 0x8f,
263
+ 0x90,
264
+ 0x2018,
265
+ 0x2019,
266
+ 0x201c,
267
+ 0x201d,
268
+ 0x2022,
269
+ 0x2013,
270
+ 0x2014,
271
+ 0x2dc,
272
+ 0x2122,
273
+ 0x161,
274
+ 0x203a,
275
+ 0x153,
276
+ 0x9d,
277
+ 0x17e,
278
+ 0x178
279
+ ];
280
+ function decodeLatin1(bytes) {
281
+ let out = '';
282
+ for (const byte of bytes){
283
+ var _CP1252_HIGH_;
284
+ out += String.fromCharCode(byte >= 0x80 && byte < 0xa0 ? (_CP1252_HIGH_ = CP1252_HIGH[byte - 0x80]) != null ? _CP1252_HIGH_ : byte : byte);
285
+ }
286
+ return out;
287
+ }
288
+ /** Whether the bytes are well-formed UTF-8. */ export function isUtf8(bytes) {
289
+ try {
290
+ utf8Strict.decode(bytes);
291
+ return true;
292
+ } catch (unused) {
293
+ return false;
294
+ }
295
+ }
296
+ /**
297
+ * Text in a field whose declared charset nobody honors: UTF-8 when the
298
+ * bytes are valid UTF-8 (pure ASCII is), Latin-1 otherwise. Shared with the
299
+ * IPTC reader, which has the same problem.
300
+ */ export function decodeLegacyText(bytes) {
301
+ try {
302
+ return utf8Strict.decode(bytes);
303
+ } catch (unused) {
304
+ return decodeLatin1(bytes);
305
+ }
306
+ }
307
+ /** Text up to the first NUL, decoded, with surrounding whitespace removed. */ function cleanText(bytes) {
308
+ const nul = bytes.indexOf(0);
309
+ return decodeLegacyText(nul >= 0 ? bytes.subarray(0, nul) : bytes).trim();
310
+ }
311
+ function decodeUcs2(bytes, little) {
312
+ const even = bytes.subarray(0, bytes.length - bytes.length % 2);
313
+ if (little) return utf16le.decode(even);
314
+ const swapped = new Uint8Array(even.length);
315
+ for(let i = 0; i + 1 < even.length; i += 2){
316
+ var _even_, _even_i;
317
+ swapped[i] = (_even_ = even[i + 1]) != null ? _even_ : 0;
318
+ swapped[i + 1] = (_even_i = even[i]) != null ? _even_i : 0;
319
+ }
320
+ return utf16le.decode(swapped);
321
+ }
322
+ function cleanUcs2(bytes, little) {
323
+ const text = decodeUcs2(bytes, little);
324
+ const nul = text.indexOf('\0');
325
+ return (nul >= 0 ? text.slice(0, nul) : text).trim();
326
+ }
327
+ function encodeUcs2(text, little) {
328
+ const out = new Uint8Array(text.length * 2);
329
+ for(let i = 0; i < text.length; i++)put16(out, i * 2, text.charCodeAt(i), little);
330
+ return out;
331
+ }
332
+ function isAscii(text) {
333
+ for(let i = 0; i < text.length; i++)if (text.charCodeAt(i) > 0x7f) return false;
334
+ return true;
335
+ }
336
+ /** An ASCII-typed value: UTF-8, as real files hold it, NUL-terminated. */ function asciiValue(text) {
337
+ return concat(encoder.encode(text), [
338
+ 0
339
+ ]);
340
+ }
341
+ /** A Windows XP tag: UCS-2 little-endian whatever the block's order. */ function xpValue(text) {
342
+ return concat(encodeUcs2(text, true), [
343
+ 0,
344
+ 0
345
+ ]);
346
+ }
347
+ function splitList(text) {
348
+ if (!text) return null;
349
+ const items = [
350
+ ...new Set(text.split(';').map((item)=>item.trim()))
351
+ ];
352
+ const kept = items.filter(Boolean);
353
+ return kept.length ? kept : null;
354
+ }
355
+ /** A number with at most `places` decimals and no trailing zeros. */ function trimNumber(value, places) {
356
+ const fixed = value.toFixed(places);
357
+ return fixed.includes('.') ? fixed.replace(/\.?0+$/, '') : fixed;
358
+ }
359
+ function readHeader(b) {
360
+ if (b.length < 8) return null;
361
+ const little = b[0] === 0x49 && b[1] === 0x49;
362
+ if (!little && !(b[0] === 0x4d && b[1] === 0x4d)) return null;
363
+ const version = get16(b, 2, little);
364
+ if (version === 42) return {
365
+ little,
366
+ big: false,
367
+ ifd0: get32(b, 4, little)
368
+ };
369
+ // BigTIFF: the offset size (always 8) and a reserved zero follow.
370
+ if (version !== 43 || b.length < 16) return null;
371
+ if (get16(b, 4, little) !== 8 || get16(b, 6, little) !== 0) return null;
372
+ return {
373
+ little,
374
+ big: true,
375
+ ifd0: get64(b, 8, little)
376
+ };
377
+ }
378
+ /** Whether the bytes start with a TIFF header this module can read. */ export function isTiff(bytes) {
379
+ return readHeader(bytes) !== null;
380
+ }
381
+ function parseIfd(b, h, at) {
382
+ const countSize = h.big ? 8 : 2;
383
+ const entrySize = h.big ? 20 : 12;
384
+ const pointerSize = h.big ? 8 : 4;
385
+ const headerSize = h.big ? 16 : 8;
386
+ if (!Number.isSafeInteger(at) || at < headerSize) return null;
387
+ if (at + countSize > b.length) return null;
388
+ const n = h.big ? get64(b, at, h.little) : get16(b, at, h.little);
389
+ const entriesEnd = at + countSize + n * entrySize;
390
+ if (n > 0xffff || entriesEnd > b.length) return null;
391
+ const next = entriesEnd + pointerSize <= b.length ? h.big ? get64(b, entriesEnd, h.little) : get32(b, entriesEnd, h.little) : 0;
392
+ const entries = [];
393
+ for(let i = 0; i < n; i++){
394
+ const e = at + countSize + i * entrySize;
395
+ const type = get16(b, e + 2, h.little);
396
+ const count = h.big ? get64(b, e + 4, h.little) : get32(b, e + 4, h.little);
397
+ const field = e + (h.big ? 12 : 8);
398
+ const unit = TYPE_SIZE[type];
399
+ const size = unit === undefined ? -1 : count * unit;
400
+ const inline = size >= 0 && size <= pointerSize;
401
+ const valueAt = inline ? field : h.big ? get64(b, field, h.little) : get32(b, field, h.little);
402
+ entries.push({
403
+ tag: get16(b, e, h.little),
404
+ type,
405
+ count,
406
+ at: e,
407
+ valueAt,
408
+ size,
409
+ inline,
410
+ ok: size >= 0 && valueAt + size <= b.length
411
+ });
412
+ }
413
+ return {
414
+ at,
415
+ entries,
416
+ next,
417
+ length: Math.min(entriesEnd + pointerSize, b.length) - at
418
+ };
419
+ }
420
+ /** The index-th value of an integer-typed entry, or null. */ function uintAt(b, h, e, index) {
421
+ var _TYPE_SIZE_e_type;
422
+ if (!(e == null ? void 0 : e.ok) || index < 0 || index >= e.count) return null;
423
+ const at = e.valueAt + index * ((_TYPE_SIZE_e_type = TYPE_SIZE[e.type]) != null ? _TYPE_SIZE_e_type : 0);
424
+ switch(e.type){
425
+ case T.BYTE:
426
+ case T.UNDEFINED:
427
+ var _b_at;
428
+ return (_b_at = b[at]) != null ? _b_at : null;
429
+ case T.SBYTE:
430
+ var _b_at1;
431
+ return ((_b_at1 = b[at]) != null ? _b_at1 : 0) << 24 >> 24;
432
+ case T.SHORT:
433
+ return get16(b, at, h.little);
434
+ case T.SSHORT:
435
+ return get16(b, at, h.little) << 16 >> 16;
436
+ case T.LONG:
437
+ case T.IFD:
438
+ return get32(b, at, h.little);
439
+ case T.SLONG:
440
+ return get32(b, at, h.little) | 0;
441
+ case T.LONG8:
442
+ case T.IFD8:
443
+ return get64(b, at, h.little);
444
+ default:
445
+ return null;
446
+ }
447
+ }
448
+ /** The index-th value of a rational (or integer) entry, or null. */ function rationalAt(b, h, e, index) {
449
+ if (!(e == null ? void 0 : e.ok) || index < 0 || index >= e.count) return null;
450
+ if (e.type !== T.RATIONAL && e.type !== T.SRATIONAL) {
451
+ return uintAt(b, h, e, index);
452
+ }
453
+ const at = e.valueAt + index * 8;
454
+ let num = get32(b, at, h.little);
455
+ let den = get32(b, at + 4, h.little);
456
+ if (e.type === T.SRATIONAL) {
457
+ num |= 0;
458
+ den |= 0;
459
+ }
460
+ return den === 0 ? null : num / den;
461
+ }
462
+ /**
463
+ * Walk IFD0, the Exif and GPS IFDs, and, only so the writer knows which
464
+ * bytes they own, the Interop IFD, IFD0's chain (IFD1 holds the thumbnail)
465
+ * and any SubIFDs. Each IFD is visited once, so a cycle ends the walk.
466
+ */ function walk(b) {
467
+ const header = readHeader(b);
468
+ if (!header) return null;
469
+ const seen = new Set();
470
+ const all = [];
471
+ const visit = (at)=>{
472
+ if (!at || seen.has(at) || seen.size >= MAX_IFDS) return null;
473
+ seen.add(at);
474
+ const ifd = parseIfd(b, header, at);
475
+ if (ifd) all.push(ifd);
476
+ return ifd;
477
+ };
478
+ const pointer = (ifd, tag, index = 0)=>uintAt(b, header, ifd == null ? void 0 : ifd.entries.find((e)=>e.tag === tag), index);
479
+ const ifd0 = visit(header.ifd0);
480
+ const exif = visit(pointer(ifd0, TAG.exifIfd));
481
+ const gps = visit(pointer(ifd0, TAG.gpsIfd));
482
+ visit(pointer(exif, TAG.interopIfd));
483
+ const chain = [];
484
+ for(let ifd = ifd0; ifd && chain.length < MAX_IFDS; ifd = visit(ifd.next)){
485
+ chain.push(ifd);
486
+ }
487
+ for (const ifd of chain){
488
+ const subs = ifd.entries.find((e)=>e.tag === TAG.subIfds);
489
+ for(let i = 0; subs && i < Math.min(subs.count, MAX_IFDS); i++){
490
+ visit(pointer(ifd, TAG.subIfds, i));
491
+ }
492
+ }
493
+ return {
494
+ header,
495
+ ifd0,
496
+ exif,
497
+ gps,
498
+ all
499
+ };
500
+ }
501
+ function entryOf(ifd, tag) {
502
+ return ifd == null ? void 0 : ifd.entries.find((e)=>e.tag === tag);
503
+ }
504
+ function valueBytes(b, e) {
505
+ return (e == null ? void 0 : e.ok) ? b.subarray(e.valueAt, e.valueAt + e.size) : null;
506
+ }
507
+ // ---------------------------------------------------------------------------
508
+ // Reading
509
+ // ---------------------------------------------------------------------------
510
+ /**
511
+ * EXIF 2.32 §4.6.5: "YYYY:MM:DD HH:MM:SS", blank-filled when unknown. A
512
+ * date whose time is blank reads as a date alone, which is what the writer
513
+ * produces for a date-only edit.
514
+ */ function exifDateToIso(raw, offset) {
515
+ const m = /^(\d{4})[:-](\d{2})[:-](\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?/.exec(raw.trim());
516
+ if (!m) return null;
517
+ const [, year, month, day, hour, minute, second] = m;
518
+ if (!year || !month || !day || year === '0000') return null;
519
+ if (+month < 1 || +month > 12 || +day < 1 || +day > 31) return null;
520
+ let iso = `${year}-${month}-${day}`;
521
+ if (hour === undefined || minute === undefined) return iso;
522
+ if (+hour > 23 || +minute > 59 || +(second != null ? second : 0) > 60) return iso;
523
+ iso += `T${hour}:${minute}:${second != null ? second : '00'}`;
524
+ const zone = offset == null ? void 0 : offset.trim();
525
+ const z = zone ? /^([+-])(\d{2}):?(\d{2})$/.exec(zone) : null;
526
+ if (zone === 'Z') iso += 'Z';
527
+ else if (z) iso += `${z[1]}${z[2]}:${z[3]}`;
528
+ return iso;
529
+ }
530
+ /** ExifTool's rule: a quarter second and shorter reads as a fraction. */ function formatExposure(seconds) {
531
+ if (seconds === null || !Number.isFinite(seconds) || seconds <= 0) return null;
532
+ if (seconds < 0.25001) return `1/${Math.round(1 / seconds)} s`;
533
+ return `${trimNumber(seconds, 1)} s`;
534
+ }
535
+ /** EXIF 2.32 §4.6.5 Flash: bit 0 fired, 1–2 return, 3–4 mode, 5, 6. */ function describeFlash(value) {
536
+ if (value === null) return null;
537
+ const fired = (value & 1) === 1;
538
+ if (!fired && value & 0x20) return 'No flash function';
539
+ const parts = [
540
+ fired ? 'Fired' : 'Did not fire'
541
+ ];
542
+ const mode = value >> 3 & 3;
543
+ if (mode === 1) parts.push('compulsory flash mode');
544
+ else if (mode === 2) parts.push('flash suppressed');
545
+ else if (mode === 3) parts.push('auto mode');
546
+ const strobe = value >> 1 & 3;
547
+ if (strobe === 2) parts.push('return not detected');
548
+ else if (strobe === 3) parts.push('return detected');
549
+ if (value & 0x40) parts.push('red-eye reduction');
550
+ return parts.join(', ');
551
+ }
552
+ /**
553
+ * UserComment's 8-byte character code, EXIF 2.32 §4.6.5 Table 9. JIS and
554
+ * the undefined code are not decoded, and so are not written either.
555
+ */ function userCommentCode(bytes) {
556
+ const code = decodeLatin1(bytes.subarray(0, 8)).replace(/\0+$/, '');
557
+ return bytes.length >= 8 && (code === 'ASCII' || code === 'UNICODE') ? code : null;
558
+ }
559
+ /**
560
+ * The byte order of a UNICODE UserComment. The spec does not say; most
561
+ * writers use the block's own order, Microsoft's use little-endian whatever
562
+ * the block says, and a few add a BOM. Mostly-Latin text settles it: the
563
+ * zero bytes of the high halves all sit on one side.
564
+ */ function unicodeOrder(body, blockLittle) {
565
+ if (body[0] === 0xff && body[1] === 0xfe) return {
566
+ little: true,
567
+ bom: true
568
+ };
569
+ if (body[0] === 0xfe && body[1] === 0xff) return {
570
+ little: false,
571
+ bom: true
572
+ };
573
+ let highFirst = 0;
574
+ let highSecond = 0;
575
+ for(let i = 0; i + 1 < body.length && i < 4096; i += 2){
576
+ if (body[i] === 0 && body[i + 1] !== 0) highFirst++;
577
+ if (body[i + 1] === 0 && body[i] !== 0) highSecond++;
578
+ }
579
+ if (highSecond > highFirst) return {
580
+ little: true,
581
+ bom: false
582
+ };
583
+ if (highFirst > highSecond) return {
584
+ little: false,
585
+ bom: false
586
+ };
587
+ return {
588
+ little: blockLittle,
589
+ bom: false
590
+ };
591
+ }
592
+ function decodeUserComment(bytes, little) {
593
+ if (!bytes) return null;
594
+ const code = userCommentCode(bytes);
595
+ const body = bytes.subarray(8);
596
+ if (code === 'ASCII') return cleanText(body);
597
+ if (code !== 'UNICODE') return null;
598
+ const order = unicodeOrder(body, little);
599
+ return cleanUcs2(order.bom ? body.subarray(2) : body, order.little);
600
+ }
601
+ function gpsCoordinate(b, h, value, ref) {
602
+ var _rationalAt, _rationalAt1;
603
+ const degrees = rationalAt(b, h, value, 0);
604
+ if (degrees === null || !value) return null;
605
+ const minutes = value.count > 1 ? (_rationalAt = rationalAt(b, h, value, 1)) != null ? _rationalAt : 0 : 0;
606
+ const seconds = value.count > 2 ? (_rationalAt1 = rationalAt(b, h, value, 2)) != null ? _rationalAt1 : 0 : 0;
607
+ const decimal = Math.abs(degrees) + Math.abs(minutes) / 60 + Math.abs(seconds) / 3600;
608
+ if (!Number.isFinite(decimal)) return null;
609
+ const hemisphere = ref == null ? void 0 : ref.trim().charAt(0).toUpperCase();
610
+ return hemisphere === 'S' || hemisphere === 'W' ? -decimal : decimal;
611
+ }
612
+ function readInto(b, tree, out) {
613
+ var _ORIENTATION_orientation;
614
+ const { header: h, ifd0, exif, gps } = tree;
615
+ /** The first spelling found wins: standard tags are read before XP tags. */ const set = (key, value)=>{
616
+ if (value === null || out.has(key)) return;
617
+ if (Array.isArray(value) ? value.length : value) out.set(key, value);
618
+ };
619
+ const text = (ifd, tag)=>{
620
+ const bytes = valueBytes(b, entryOf(ifd, tag));
621
+ return bytes ? cleanText(bytes) : null;
622
+ };
623
+ const xp = (tag)=>{
624
+ const bytes = valueBytes(b, entryOf(ifd0, tag));
625
+ return bytes ? cleanUcs2(bytes, true) : null;
626
+ };
627
+ const numeric = (ifd, tag)=>rationalAt(b, h, entryOf(ifd, tag), 0);
628
+ set('title', xp(TAG.xpTitle));
629
+ set('description', text(ifd0, TAG.imageDescription));
630
+ set('keywords', splitList(xp(TAG.xpKeywords)));
631
+ set('comment', decodeUserComment(valueBytes(b, entryOf(exif, TAG.userComment)), h.little));
632
+ set('comment', xp(TAG.xpComment));
633
+ set('creator', splitList(text(ifd0, TAG.artist)));
634
+ set('creator', splitList(xp(TAG.xpAuthor)));
635
+ // EXIF 2.32 §4.6.5 Copyright: "photographer\0editor\0"; the first part.
636
+ set('copyright', text(ifd0, TAG.copyright));
637
+ if (gps) {
638
+ const lat = gpsCoordinate(b, h, entryOf(gps, GPS.latitude), text(gps, GPS.latitudeRef));
639
+ const lon = gpsCoordinate(b, h, entryOf(gps, GPS.longitude), text(gps, GPS.longitudeRef));
640
+ if (lat !== null && lon !== null && lat <= 90 && lat >= -90) {
641
+ if (lon <= 180 && lon >= -180) {
642
+ set('gps', `${lat.toFixed(6)},${lon.toFixed(6)}`);
643
+ }
644
+ }
645
+ const altitude = numeric(gps, GPS.altitude);
646
+ if (altitude !== null && Number.isFinite(altitude)) {
647
+ const below = uintAt(b, h, entryOf(gps, GPS.altitudeRef), 0) === 1;
648
+ const metres = below ? -Math.abs(altitude) : altitude;
649
+ set('gpsAltitude', `${trimNumber(metres, 1)} m`);
650
+ }
651
+ }
652
+ const original = text(exif, TAG.dateTimeOriginal);
653
+ if (original) {
654
+ set('createdAt', exifDateToIso(original, text(exif, TAG.offsetTimeOriginal)));
655
+ }
656
+ set('make', text(ifd0, TAG.make));
657
+ set('model', text(ifd0, TAG.model));
658
+ set('lens', text(exif, TAG.lensModel));
659
+ set('exposure', formatExposure(numeric(exif, TAG.exposureTime)));
660
+ const aperture = numeric(exif, TAG.fNumber);
661
+ if (aperture !== null && Number.isFinite(aperture) && aperture > 0) {
662
+ set('aperture', `f/${trimNumber(aperture, 1)}`);
663
+ }
664
+ const iso = uintAt(b, h, entryOf(exif, TAG.iso), 0);
665
+ if (iso) set('iso', String(iso));
666
+ const focal = numeric(exif, TAG.focalLength);
667
+ if (focal !== null && Number.isFinite(focal) && focal > 0) {
668
+ set('focalLength', `${trimNumber(focal, 1)} mm`);
669
+ }
670
+ set('flash', describeFlash(uintAt(b, h, entryOf(exif, TAG.flash), 0)));
671
+ const modified = text(ifd0, TAG.dateTime);
672
+ if (modified) {
673
+ set('modifiedAt', exifDateToIso(modified, text(exif, TAG.offsetTime)));
674
+ }
675
+ set('software', text(ifd0, TAG.software));
676
+ const orientation = uintAt(b, h, entryOf(ifd0, TAG.orientation), 0);
677
+ set('orientation', orientation === null ? null : (_ORIENTATION_orientation = ORIENTATION[orientation]) != null ? _ORIENTATION_orientation : null);
678
+ }
679
+ /**
680
+ * Read the EXIF of a TIFF-structured block. Never throws: a damaged block
681
+ * yields what could be read, a block that is not TIFF yields nothing.
682
+ */ export function readExif(tiff) {
683
+ const values = new Map();
684
+ let big = false;
685
+ try {
686
+ const tree = walk(tiff);
687
+ if (tree) {
688
+ big = tree.header.big;
689
+ readInto(tiff, tree, values);
690
+ }
691
+ } catch (unused) {
692
+ // Every read is bounded; this only keeps a bug from failing an upload.
693
+ }
694
+ return [
695
+ ...values
696
+ ].map(([key, value])=>_extends({
697
+ key,
698
+ value,
699
+ source: 'exif'
700
+ }, big ? {
701
+ editable: false
702
+ } : {}));
703
+ }
704
+ /**
705
+ * The raw value of one tag, as stored: `bytes` is a view into `tiff`, in
706
+ * the block's byte order. Null when the block, the IFD or the tag is
707
+ * missing, or when the value runs past the end of the block.
708
+ */ export function readTiffTagBytes(tiff, ifd, tag) {
709
+ try {
710
+ var _ref;
711
+ const tree = walk(tiff);
712
+ const entry = entryOf((_ref = tree == null ? void 0 : tree[ifd]) != null ? _ref : null, tag);
713
+ const bytes = valueBytes(tiff, entry);
714
+ return entry && bytes ? {
715
+ type: entry.type,
716
+ count: entry.count,
717
+ bytes
718
+ } : null;
719
+ } catch (unused) {
720
+ return null;
721
+ }
722
+ }
723
+ // ---------------------------------------------------------------------------
724
+ // Writing
725
+ // ---------------------------------------------------------------------------
726
+ /** A strict ISO 8601 date, as `sanitizeEmbeddedPatch` admits it. */ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/;
727
+ const IFD0_TEXT = [
728
+ [
729
+ 'description',
730
+ TAG.imageDescription
731
+ ],
732
+ [
733
+ 'make',
734
+ TAG.make
735
+ ],
736
+ [
737
+ 'model',
738
+ TAG.model
739
+ ],
740
+ [
741
+ 'software',
742
+ TAG.software
743
+ ]
744
+ ];
745
+ /** A patch value as one line; `undefined` when the key is not patched. */ function patchText(value, separator) {
746
+ if (value === undefined) return undefined;
747
+ if (value === null) return null;
748
+ const text = Array.isArray(value) ? value.map((item)=>item.trim()).filter(Boolean).join(separator) : value.trim();
749
+ return text || null;
750
+ }
751
+ function userCommentValue(current, text, blockLittle) {
752
+ const code = current ? userCommentCode(current) : null;
753
+ if (!current || !code) return undefined;
754
+ if (text === null) return null;
755
+ if (code === 'ASCII' && isAscii(text)) {
756
+ return concat(encoder.encode('ASCII\0\0\0'), encoder.encode(text));
757
+ }
758
+ // Non-ASCII text in an ASCII comment switches it to UNICODE, as ExifTool
759
+ // does, in the block's order; an existing UNICODE one keeps its order.
760
+ const order = code === 'UNICODE' ? unicodeOrder(current.subarray(8), blockLittle) : {
761
+ little: blockLittle,
762
+ bom: false
763
+ };
764
+ const bom = order.bom ? order.little ? [
765
+ 0xff,
766
+ 0xfe
767
+ ] : [
768
+ 0xfe,
769
+ 0xff
770
+ ] : [];
771
+ return concat(encoder.encode('UNICODE\0'), bom, encodeUcs2(text, order.little));
772
+ }
773
+ /**
774
+ * EXIF 2.32 §4.6.5 Copyright holds `photographer\0editor\0`. An edit
775
+ * replaces the photographer's part and keeps the editor's; with only an
776
+ * editor's part left the photographer's is one space, as the spec says.
777
+ */ function copyrightValue(current, text) {
778
+ const raw = current != null ? current : new Uint8Array();
779
+ const nul = raw.indexOf(0);
780
+ let editor = nul >= 0 ? raw.subarray(nul + 1) : new Uint8Array();
781
+ const end = editor.indexOf(0);
782
+ if (end >= 0) editor = editor.subarray(0, end);
783
+ if (!decodeLegacyText(editor).trim()) {
784
+ return text === null ? null : asciiValue(text);
785
+ }
786
+ return concat(encoder.encode(text != null ? text : ' '), [
787
+ 0
788
+ ], editor, [
789
+ 0
790
+ ]);
791
+ }
792
+ /**
793
+ * Turn a canonical patch into writes of tags that ALREADY EXIST. A value
794
+ * the file does not hold yet goes to XMP (the Metadata Working Group's
795
+ * rule), which is the XMP writer's job, not this one's.
796
+ */ function planPatch(b, tree, patch) {
797
+ const little = tree.header.little;
798
+ const writes = [];
799
+ const patched = (key)=>Object.prototype.hasOwnProperty.call(patch, key) && patch[key] !== undefined;
800
+ const entry = (ifd, tag)=>entryOf(tree[ifd], tag);
801
+ const put = (ifd, tag, type, value)=>{
802
+ if (value !== undefined && entry(ifd, tag)) {
803
+ writes.push({
804
+ ifd,
805
+ tag,
806
+ type,
807
+ value
808
+ });
809
+ }
810
+ };
811
+ const ascii = (ifd, tag, text)=>put(ifd, tag, T.ASCII, text === null ? null : asciiValue(text));
812
+ const xp = (tag, text)=>{
813
+ var _entry;
814
+ const type = ((_entry = entry('ifd0', tag)) == null ? void 0 : _entry.type) === T.UNDEFINED ? T.UNDEFINED : T.BYTE;
815
+ put('ifd0', tag, type, text === null ? null : xpValue(text));
816
+ };
817
+ const text = (key, separator = ', ')=>{
818
+ var _patchText;
819
+ return (_patchText = patchText(patch[key], separator)) != null ? _patchText : null;
820
+ };
821
+ for (const [key, tag] of IFD0_TEXT){
822
+ if (patched(key)) ascii('ifd0', tag, text(key));
823
+ }
824
+ if (patched('lens')) ascii('exif', TAG.lensModel, text('lens'));
825
+ if (patched('creator')) {
826
+ ascii('ifd0', TAG.artist, text('creator', '; '));
827
+ xp(TAG.xpAuthor, text('creator', '; '));
828
+ }
829
+ if (patched('title')) xp(TAG.xpTitle, text('title'));
830
+ if (patched('keywords')) xp(TAG.xpKeywords, text('keywords', ';'));
831
+ if (patched('comment')) {
832
+ xp(TAG.xpComment, text('comment'));
833
+ const current = valueBytes(b, entry('exif', TAG.userComment));
834
+ put('exif', TAG.userComment, T.UNDEFINED, userCommentValue(current, text('comment'), little));
835
+ }
836
+ if (patched('copyright')) {
837
+ const current = valueBytes(b, entry('ifd0', TAG.copyright));
838
+ put('ifd0', TAG.copyright, T.ASCII, copyrightValue(current, text('copyright')));
839
+ }
840
+ if (patched('createdAt')) {
841
+ const raw = patch['createdAt'];
842
+ const iso = typeof raw === 'string' ? ISO_DATE.exec(raw.trim()) : null;
843
+ if (raw === null || iso) {
844
+ const [, year, month, day, hour, minute, second, zone] = iso != null ? iso : [];
845
+ // A date without a time keeps the time blank-filled, the spec's way
846
+ // of saying "unknown", rather than inventing midnight.
847
+ const value = iso ? hour ? `${year}:${month}:${day} ${hour}:${minute}:${second != null ? second : '00'}` : `${year}:${month}:${day} : : ` : null;
848
+ ascii('exif', TAG.dateTimeOriginal, value);
849
+ if (entry('exif', TAG.dateTimeOriginal)) {
850
+ // The offset belongs to the old time; a new time without a zone
851
+ // leaves it blank-filled ("unknown") rather than wrong.
852
+ const offset = zone && hour ? zone === 'Z' ? '+00:00' : zone : ' : ';
853
+ ascii('exif', TAG.offsetTimeOriginal, value === null ? null : offset);
854
+ }
855
+ }
856
+ }
857
+ return {
858
+ writes,
859
+ scrubGps: Object.prototype.hasOwnProperty.call(patch, 'gps') && patch['gps'] === null
860
+ };
861
+ }
862
+ const HEADER_OWNER = {
863
+ owner: 'header'
864
+ };
865
+ const DATA_OWNER = {
866
+ owner: 'data'
867
+ };
868
+ const POINTER_TAG = {
869
+ exif: TAG.exifIfd,
870
+ gps: TAG.gpsIfd
871
+ };
872
+ let TiffEditor = class TiffEditor {
873
+ dir(ifd) {
874
+ return {
875
+ ifd,
876
+ moved: false,
877
+ slots: ifd.entries.map((e)=>({
878
+ tag: e.tag,
879
+ type: e.type,
880
+ count: e.count,
881
+ field: this.source.slice(e.at + 8, e.at + 12),
882
+ from: e,
883
+ dirty: false
884
+ }))
885
+ };
886
+ }
887
+ directory(kind) {
888
+ return kind === 'ifd0' ? this.ifd0 : this.subs[kind];
889
+ }
890
+ /** Whether no structure outside `owners` claims any byte of the span. */ exclusive(start, end, owners) {
891
+ for (const claim of this.claims){
892
+ if (claim.start < end && start < claim.end && !owners.has(claim.owner)) {
893
+ return false;
894
+ }
895
+ }
896
+ return true;
897
+ }
898
+ /** The out-of-line bytes of an entry that lie inside the block. */ range(e) {
899
+ if (!e || e.inline || e.size <= 0 || e.valueAt >= this.source.length) {
900
+ return null;
901
+ }
902
+ return [
903
+ e.valueAt,
904
+ Math.min(e.valueAt + e.size, this.source.length)
905
+ ];
906
+ }
907
+ zero(start, end) {
908
+ const from = Math.max(0, start);
909
+ const to = Math.min(end, this.work.length);
910
+ if (to > from) {
911
+ this.work.fill(0, from, to);
912
+ this.changed = true;
913
+ }
914
+ }
915
+ /** Zero an entry's old out-of-line value, unless something shares it. */ release(e) {
916
+ const range = this.range(e);
917
+ if (e && range && this.exclusive(range[0], range[1], new Set([
918
+ e
919
+ ]))) {
920
+ this.zero(range[0], range[1]);
921
+ }
922
+ }
923
+ /** Append at the next even offset (TIFF 6.0 §2: offsets are word-aligned). */ append(bytes) {
924
+ if (this.end % 2) this.end += 1;
925
+ const at = this.end;
926
+ this.tail.push({
927
+ at,
928
+ bytes
929
+ });
930
+ this.end += bytes.length;
931
+ this.changed = true;
932
+ return at;
933
+ }
934
+ /** Put a value where it may go and return the entry's 4-byte field. */ place(from, bytes) {
935
+ if (bytes.length <= 4) {
936
+ this.release(from);
937
+ const field = new Uint8Array(4);
938
+ field.set(bytes);
939
+ return field;
940
+ }
941
+ const range = this.range(from);
942
+ if ((from == null ? void 0 : from.ok) && range && bytes.length <= from.size && this.exclusive(range[0], range[1], new Set([
943
+ from
944
+ ]))) {
945
+ this.work.set(bytes, from.valueAt);
946
+ this.work.fill(0, from.valueAt + bytes.length, from.valueAt + from.size);
947
+ this.changed = true;
948
+ return encode32(from.valueAt, this.little);
949
+ }
950
+ this.release(from);
951
+ return encode32(this.append(bytes), this.little);
952
+ }
953
+ move(dir) {
954
+ if (dir.moved) return;
955
+ if (!this.exclusive(dir.ifd.at, dir.ifd.at + dir.ifd.length, new Set([
956
+ dir.ifd
957
+ ]))) {
958
+ throw new EmbeddedWriteError('This file’s EXIF directories overlap other data, so they cannot be edited safely.');
959
+ }
960
+ dir.moved = true;
961
+ this.changed = true;
962
+ }
963
+ set(write, allowAdd) {
964
+ var _write_count, _TYPE_SIZE_write_type, _slot_from;
965
+ const dir = this.directory(write.ifd);
966
+ const slot = dir == null ? void 0 : dir.slots.find((s)=>s.tag === write.tag);
967
+ if (write.value === null) {
968
+ if (!dir || !slot) return;
969
+ this.move(dir);
970
+ dir.slots = dir.slots.filter((s)=>s !== slot);
971
+ this.release(slot.from);
972
+ return;
973
+ }
974
+ const count = (_write_count = write.count) != null ? _write_count : write.value.length / ((_TYPE_SIZE_write_type = TYPE_SIZE[write.type]) != null ? _TYPE_SIZE_write_type : 1);
975
+ if (!slot) {
976
+ if (!allowAdd) return;
977
+ if (!dir) {
978
+ throw new EmbeddedWriteError(`This file has no ${write.ifd === 'gps' ? 'GPS' : 'Exif'} directory to add a tag to.`);
979
+ }
980
+ if (dir.slots.length >= 0xffff) {
981
+ throw new EmbeddedWriteError('This file’s EXIF directory is full.');
982
+ }
983
+ this.move(dir);
984
+ const field = this.place(null, write.value);
985
+ dir.slots.push({
986
+ tag: write.tag,
987
+ type: write.type,
988
+ count,
989
+ field,
990
+ from: null,
991
+ dirty: true
992
+ });
993
+ return;
994
+ }
995
+ const current = valueBytes(this.source, (_slot_from = slot.from) != null ? _slot_from : undefined);
996
+ if (slot.type === write.type && slot.count === count && current && sameBytes(current, write.value)) {
997
+ return;
998
+ }
999
+ slot.field = this.place(slot.from, write.value);
1000
+ slot.type = write.type;
1001
+ slot.count = count;
1002
+ slot.dirty = true;
1003
+ this.changed = true;
1004
+ }
1005
+ /**
1006
+ * Remove the GPS IFD entirely: zero its table and every out-of-line value
1007
+ * it holds, then drop IFD0's pointer to it. Unreferencing it would leave
1008
+ * the coordinates in the file for anyone with a hex editor.
1009
+ */ scrubGps() {
1010
+ const pointer = this.ifd0.slots.find((s)=>s.tag === TAG.gpsIfd);
1011
+ if (!pointer) return;
1012
+ const gps = this.gpsIfd;
1013
+ if (!gps) {
1014
+ // A pointer to nothing (0, or past the end) holds no location, and is
1015
+ // simply dropped. One into the block that did not read as an IFD, or
1016
+ // reached an IFD already walked, may hide coordinates: refuse.
1017
+ const from = pointer.from;
1018
+ const target = (from == null ? void 0 : from.inline) && from.size === 4 ? get32(this.source, from.valueAt, this.little) : -1;
1019
+ if (target !== 0 && target < this.source.length) {
1020
+ throw new EmbeddedWriteError('This file’s location data is damaged, so it cannot be removed safely.');
1021
+ }
1022
+ this.move(this.ifd0);
1023
+ this.ifd0.slots = this.ifd0.slots.filter((s)=>s !== pointer);
1024
+ return;
1025
+ }
1026
+ const owners = new Set([
1027
+ gps,
1028
+ ...gps.entries
1029
+ ]);
1030
+ const spans = [
1031
+ [
1032
+ gps.at,
1033
+ gps.at + gps.length
1034
+ ]
1035
+ ];
1036
+ for (const e of gps.entries){
1037
+ const range = this.range(e);
1038
+ if (range) spans.push(range);
1039
+ }
1040
+ for (const [start, end] of spans){
1041
+ if (!this.exclusive(start, end, owners)) {
1042
+ throw new EmbeddedWriteError('This file’s location data shares its bytes with other data, so it cannot be removed safely.');
1043
+ }
1044
+ }
1045
+ this.move(this.ifd0);
1046
+ for (const [start, end] of spans)this.zero(start, end);
1047
+ this.ifd0.slots = this.ifd0.slots.filter((s)=>s !== pointer);
1048
+ delete this.subs.gps;
1049
+ }
1050
+ entryBytes(slot) {
1051
+ const out = new Uint8Array(12);
1052
+ put16(out, 0, slot.tag, this.little);
1053
+ put16(out, 2, slot.type, this.little);
1054
+ put32(out, 4, slot.count, this.little);
1055
+ out.set(slot.field, 8);
1056
+ return out;
1057
+ }
1058
+ /** Write a fresh copy of the IFD at the end and zero the old table. */ relocate(dir) {
1059
+ const slots = [
1060
+ ...dir.slots
1061
+ ].sort((a, b)=>a.tag - b.tag);
1062
+ const table = new Uint8Array(2 + slots.length * 12 + 4);
1063
+ put16(table, 0, slots.length, this.little);
1064
+ slots.forEach((slot, i)=>table.set(this.entryBytes(slot), 2 + i * 12));
1065
+ put32(table, 2 + slots.length * 12, dir.ifd.next, this.little);
1066
+ const at = this.append(table);
1067
+ this.zero(dir.ifd.at, dir.ifd.at + dir.ifd.length);
1068
+ return at;
1069
+ }
1070
+ finish(maxLength) {
1071
+ for (const kind of [
1072
+ 'exif',
1073
+ 'gps'
1074
+ ]){
1075
+ const dir = this.subs[kind];
1076
+ if (!(dir == null ? void 0 : dir.moved)) continue;
1077
+ const at = this.relocate(dir);
1078
+ const pointer = this.ifd0.slots.find((s)=>s.tag === POINTER_TAG[kind]);
1079
+ if (pointer) {
1080
+ pointer.field = encode32(at, this.little);
1081
+ pointer.dirty = true;
1082
+ }
1083
+ }
1084
+ if (this.ifd0.moved) put32(this.work, 4, this.relocate(this.ifd0), this.little);
1085
+ for (const dir of [
1086
+ this.ifd0,
1087
+ this.subs.exif,
1088
+ this.subs.gps
1089
+ ]){
1090
+ if (!dir || dir.moved) continue;
1091
+ for (const slot of dir.slots){
1092
+ if (slot.dirty && slot.from) this.work.set(this.entryBytes(slot), slot.from.at);
1093
+ }
1094
+ }
1095
+ if (!this.changed) return this.source;
1096
+ if (maxLength !== undefined && this.end > maxLength) {
1097
+ throw new EmbeddedWriteError(`This edit would make the file’s EXIF block larger than the ${maxLength} bytes it can hold.`);
1098
+ }
1099
+ if (this.end > 0xffffffff) {
1100
+ throw new EmbeddedWriteError('This edit would make the file larger than 4 GB.');
1101
+ }
1102
+ const out = new Uint8Array(this.end);
1103
+ out.set(this.work);
1104
+ for (const { at, bytes } of this.tail)out.set(bytes, at);
1105
+ return out;
1106
+ }
1107
+ constructor(source, tree, ifd0){
1108
+ this.tail = [];
1109
+ this.changed = false;
1110
+ this.subs = {};
1111
+ this.source = source;
1112
+ this.work = source.slice();
1113
+ this.end = source.length;
1114
+ this.little = tree.header.little;
1115
+ this.claims = claimMap(source, tree);
1116
+ this.gpsIfd = tree.gps;
1117
+ this.ifd0 = this.dir(ifd0);
1118
+ if (tree.exif) this.subs.exif = this.dir(tree.exif);
1119
+ if (tree.gps) this.subs.gps = this.dir(tree.gps);
1120
+ }
1121
+ };
1122
+ /** Every span of the block some known structure owns. */ function claimMap(b, tree) {
1123
+ const claims = [
1124
+ {
1125
+ start: 0,
1126
+ end: 8,
1127
+ owner: HEADER_OWNER
1128
+ }
1129
+ ];
1130
+ const h = tree.header;
1131
+ for (const ifd of tree.all){
1132
+ claims.push({
1133
+ start: ifd.at,
1134
+ end: ifd.at + ifd.length,
1135
+ owner: ifd
1136
+ });
1137
+ for (const e of ifd.entries){
1138
+ if (!e.inline && e.size > 0 && e.valueAt < b.length) {
1139
+ claims.push({
1140
+ start: e.valueAt,
1141
+ end: Math.min(e.valueAt + e.size, b.length),
1142
+ owner: e
1143
+ });
1144
+ }
1145
+ }
1146
+ for (const [offsetTag, lengthTag] of DATA_PAIRS){
1147
+ const offsets = entryOf(ifd, offsetTag);
1148
+ const lengths = entryOf(ifd, lengthTag);
1149
+ if (!offsets || !lengths) continue;
1150
+ const n = Math.min(offsets.count, lengths.count);
1151
+ let low = Infinity;
1152
+ let high = 0;
1153
+ for(let i = 0; i < n; i++){
1154
+ const start = uintAt(b, h, offsets, i);
1155
+ const size = uintAt(b, h, lengths, i);
1156
+ if (start === null || size === null) break;
1157
+ if (i < MAX_DATA_RANGES) {
1158
+ claims.push({
1159
+ start,
1160
+ end: start + size,
1161
+ owner: DATA_OWNER
1162
+ });
1163
+ } else {
1164
+ low = Math.min(low, start);
1165
+ high = Math.max(high, start + size);
1166
+ }
1167
+ }
1168
+ // Past the cap the rest is claimed as one envelope: conservative,
1169
+ // since a claimed span is only ever left alone.
1170
+ if (high > low) claims.push({
1171
+ start: low,
1172
+ end: high,
1173
+ owner: DATA_OWNER
1174
+ });
1175
+ }
1176
+ }
1177
+ return claims;
1178
+ }
1179
+ function validateWrite(write) {
1180
+ var _write_count;
1181
+ if (![
1182
+ 'ifd0',
1183
+ 'exif',
1184
+ 'gps'
1185
+ ].includes(write.ifd)) {
1186
+ throw new EmbeddedWriteError(`There is no ${String(write.ifd)} directory.`);
1187
+ }
1188
+ if (!Number.isInteger(write.tag) || write.tag < 0 || write.tag > 0xffff) {
1189
+ throw new EmbeddedWriteError(`Tag ${write.tag} is not a TIFF tag number.`);
1190
+ }
1191
+ if (write.ifd !== 'gps' && STRUCTURAL_TAGS.has(write.tag)) {
1192
+ throw new EmbeddedWriteError(`Tag 0x${write.tag.toString(16)} locates other data in the file and cannot be edited.`);
1193
+ }
1194
+ if (write.value === null) return;
1195
+ const unit = TYPE_SIZE[write.type];
1196
+ if (unit === undefined || write.type > T.IFD) {
1197
+ throw new EmbeddedWriteError(`TIFF field type ${write.type} cannot be written.`);
1198
+ }
1199
+ const count = (_write_count = write.count) != null ? _write_count : write.value.length / unit;
1200
+ if (!write.value.length || !Number.isInteger(count) || count < 1 || count > 0xffffffff || count * unit !== write.value.length) {
1201
+ throw new EmbeddedWriteError(`The value for tag 0x${write.tag.toString(16)} does not match its type and count.`);
1202
+ }
1203
+ }
1204
+ /**
1205
+ * The one writer behind {@link writeExif}, {@link writeTiffTags} and the
1206
+ * TIFF file writer: a canonical patch and raw tag writes, in a single pass
1207
+ * so an IFD is relocated at most once. Returns `tiff` itself when nothing
1208
+ * needed to change. Throws {@link EmbeddedWriteError} when the block cannot
1209
+ * take the edit safely; it never returns a corrupt block.
1210
+ */ export function editTiff(tiff, edit) {
1211
+ var _edit_writes, _edit_allowAdd;
1212
+ const header = readHeader(tiff);
1213
+ if (!header) {
1214
+ throw new EmbeddedWriteError('The EXIF data in this file cannot be read, so it cannot be edited.');
1215
+ }
1216
+ if (header.big) {
1217
+ throw new EmbeddedWriteError('Metadata in a BigTIFF file can be read here but not edited.');
1218
+ }
1219
+ const tree = walk(tiff);
1220
+ if (!(tree == null ? void 0 : tree.ifd0)) {
1221
+ throw new EmbeddedWriteError('The EXIF data in this file is damaged, so it cannot be edited.');
1222
+ }
1223
+ const plan = edit.patch ? planPatch(tiff, tree, edit.patch) : {
1224
+ writes: [],
1225
+ scrubGps: false
1226
+ };
1227
+ // One write per tag, the last one given.
1228
+ const latest = new Map();
1229
+ for (const write of [
1230
+ ...plan.writes,
1231
+ ...(_edit_writes = edit.writes) != null ? _edit_writes : []
1232
+ ]){
1233
+ validateWrite(write);
1234
+ const key = `${write.ifd}:${write.tag}`;
1235
+ latest.delete(key);
1236
+ latest.set(key, write);
1237
+ }
1238
+ const editor = new TiffEditor(tiff, tree, tree.ifd0);
1239
+ if (plan.scrubGps) editor.scrubGps();
1240
+ for (const write of latest.values())editor.set(write, (_edit_allowAdd = edit.allowAdd) != null ? _edit_allowAdd : false);
1241
+ return editor.finish(edit.maxLength);
1242
+ }
1243
+ /**
1244
+ * Write a canonical patch into the EXIF tags that ALREADY EXIST — see the
1245
+ * module doc for which keys map where — and ignore every key this block
1246
+ * does not hold. `null` removes the tag; `gps: null` scrubs the GPS IFD.
1247
+ * `maxLength` is the block's ceiling: 65527 inside a JPEG APP1.
1248
+ */ export function writeExif(tiff, patch, options) {
1249
+ return editTiff(tiff, {
1250
+ patch,
1251
+ maxLength: options == null ? void 0 : options.maxLength
1252
+ });
1253
+ }
1254
+ /**
1255
+ * Set or remove raw tags. A tag that is absent is added only with
1256
+ * `allowAdd`, and is otherwise skipped; a removal of an absent tag is a
1257
+ * no-op. Tags that locate other data (strips, tiles, the thumbnail, the
1258
+ * Exif/GPS/Interop pointers, SubIFDs, the MakerNote) are refused.
1259
+ */ export function writeTiffTags(tiff, writes, options) {
1260
+ return editTiff(tiff, {
1261
+ writes,
1262
+ allowAdd: options == null ? void 0 : options.allowAdd,
1263
+ maxLength: options == null ? void 0 : options.maxLength
1264
+ });
1265
+ }
1266
+
1267
+ //# sourceMappingURL=exif.js.map