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