@su-engineering/heic 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1648 @@
1
+ import { HeicParseError, HeicUnsupportedError, HeicAbortError, HeicDecodeError } from './chunk-Y5FFG5J7.js';
2
+ export { HeicAbortError, HeicDecodeError, HeicError, HeicParseError, HeicUnsupportedError } from './chunk-Y5FFG5J7.js';
3
+
4
+ // src/bytes.ts
5
+ function toBlobPart(bytes) {
6
+ return bytes;
7
+ }
8
+ function toHeicBlob(bytes) {
9
+ return new Blob([toBlobPart(bytes)], { type: "image/heic" });
10
+ }
11
+
12
+ // src/render/canvas.ts
13
+ function createCanvas(width, height) {
14
+ if (typeof OffscreenCanvas === "undefined") {
15
+ throw new HeicDecodeError(
16
+ "OffscreenCanvas is not available; this environment cannot composite",
17
+ {}
18
+ );
19
+ }
20
+ return new OffscreenCanvas(width, height);
21
+ }
22
+ function throwIfAborted(signal) {
23
+ if (signal?.aborted) throw new HeicAbortError();
24
+ }
25
+
26
+ // src/decoders/native.ts
27
+ var nativeDecoderRejects = false;
28
+ async function decodeNative(blob, plan, signal) {
29
+ if (typeof createImageBitmap === "undefined") {
30
+ return { status: "unsupported", reason: "createImageBitmap is not available" };
31
+ }
32
+ if (nativeDecoderRejects) {
33
+ return { status: "unsupported", reason: "this browser has no HEIC image decoder" };
34
+ }
35
+ throwIfAborted(signal);
36
+ let bitmap;
37
+ try {
38
+ bitmap = await createImageBitmap(blob);
39
+ } catch {
40
+ nativeDecoderRejects = true;
41
+ return { status: "unsupported", reason: "createImageBitmap rejected the file" };
42
+ }
43
+ throwIfAborted(signal);
44
+ if (!dimensionsMatch(bitmap, plan)) {
45
+ const got = `${bitmap.width}x${bitmap.height}`;
46
+ bitmap.close();
47
+ return {
48
+ status: "wrong-image",
49
+ reason: `returned ${got}, expected ${plan.displayWidth}x${plan.displayHeight}`
50
+ };
51
+ }
52
+ return { status: "ok", bitmap };
53
+ }
54
+ function dimensionsMatch(bitmap, plan) {
55
+ const { displayWidth, displayHeight } = plan;
56
+ const upright = bitmap.width === displayWidth && bitmap.height === displayHeight;
57
+ const swapped = bitmap.width === displayHeight && bitmap.height === displayWidth;
58
+ return upright || swapped;
59
+ }
60
+ async function probeNativeSupport() {
61
+ if (typeof createImageBitmap === "undefined") return false;
62
+ try {
63
+ const bytes = decodeBase64(TINY_HEIC_BASE64);
64
+ const bitmap = await createImageBitmap(toHeicBlob(bytes));
65
+ const ok = bitmap.width === TINY_HEIC_WIDTH && bitmap.height === TINY_HEIC_HEIGHT;
66
+ bitmap.close();
67
+ return ok;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+ function decodeBase64(input) {
73
+ const binary = atob(input);
74
+ const out = new Uint8Array(binary.length);
75
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
76
+ return out;
77
+ }
78
+ var TINY_HEIC_WIDTH = 2;
79
+ var TINY_HEIC_HEIGHT = 2;
80
+ var TINY_HEIC_BASE64 = "AAAAHGZ0eXBoZWljAAAAAG1pZjFoZWljbWlhZgAAAXxtZXRhAAAAAAAAACFoZGxyAAAAAAAAAABwaWN0AAAAAAAAAAAAAAAAAAAAACJpbG9jAAAAAERAAAEAAQAAAAABoAABAAAAAAAAADcAAAAjaWluZgAAAAAAAQAAABVpbmZlAgAAAAABAABodmMxAAAAAA5waXRtAAAAAAABAAAA/GlwcnAAAADcaXBjbwAAAHVodmNDAQNwAAAAAAAAAAAAHvAA/P34+AAADwNgAAEAGEABDAH//wNwAAADAJAAAAMAAAMAHroCQGEAAQApQgEBA3AAAAMAkAAAAwAAAwAeoCCBBZbqrprm4CGgwIAAAAyAAAADAIRiAAEABkQBwXPBiQAAABNjb2xybmNseAABAA0ABoAAAAAUaXNwZQAAAAAAAABAAAAAQAAAAChjbGFwAAAAAgAAAAEAAAACAAAAAf///8IAAAAC////wgAAAAIAAAAQcGl4aQAAAAADCAgIAAAAGGlwbWEAAAAAAAAAAQABBYECAwWEAAAAP21kYXQAAAAzKAGvBjIWhzSJIPC/cov//8tX9l+i9qzWyeuEfoBjx+S3kJGe9F97GFLlPHQg9JxTuc2A";
81
+
82
+ // src/parser/reader.ts
83
+ var Reader = class _Reader {
84
+ bytes;
85
+ view;
86
+ /** Absolute offset of this window's start within the underlying ArrayBuffer. */
87
+ base;
88
+ /** Cursor, relative to the window start. */
89
+ pos = 0;
90
+ constructor(source, byteOffset = 0, byteLength) {
91
+ const u8 = source instanceof Uint8Array ? source : new Uint8Array(source);
92
+ const start = u8.byteOffset + byteOffset;
93
+ const length = byteLength ?? u8.byteLength - byteOffset;
94
+ if (byteOffset < 0 || length < 0 || byteOffset + length > u8.byteLength) {
95
+ throw new HeicParseError("Reader window is outside the source buffer", {
96
+ offset: byteOffset
97
+ });
98
+ }
99
+ this.bytes = new Uint8Array(u8.buffer, start, length);
100
+ this.view = new DataView(u8.buffer, start, length);
101
+ this.base = start;
102
+ }
103
+ get length() {
104
+ return this.bytes.byteLength;
105
+ }
106
+ get offset() {
107
+ return this.pos;
108
+ }
109
+ /** Absolute offset of the cursor in the underlying buffer, for error reports. */
110
+ get absoluteOffset() {
111
+ return this.base + this.pos;
112
+ }
113
+ get remaining() {
114
+ return this.length - this.pos;
115
+ }
116
+ get eof() {
117
+ return this.pos >= this.length;
118
+ }
119
+ seek(to) {
120
+ this.require(0, to);
121
+ this.pos = to;
122
+ }
123
+ skip(count) {
124
+ this.require(count);
125
+ this.pos += count;
126
+ }
127
+ /**
128
+ * Throws unless `count` bytes are readable at `at` (default: the cursor).
129
+ * Callers that are about to allocate should call this first with the declared
130
+ * size, so a hostile length field fails here rather than in the allocator.
131
+ */
132
+ require(count, at = this.pos) {
133
+ if (!Number.isFinite(count) || count < 0 || !Number.isFinite(at) || at < 0) {
134
+ throw new HeicParseError("Malformed read request", { offset: this.base + this.pos });
135
+ }
136
+ if (at + count > this.length) {
137
+ throw new HeicParseError(
138
+ `Read of ${count} bytes at ${at} exceeds the ${this.length}-byte window`,
139
+ { offset: this.base + at }
140
+ );
141
+ }
142
+ }
143
+ u8() {
144
+ this.require(1);
145
+ return this.view.getUint8(this.pos++);
146
+ }
147
+ u16() {
148
+ this.require(2);
149
+ const value = this.view.getUint16(this.pos);
150
+ this.pos += 2;
151
+ return value;
152
+ }
153
+ u24() {
154
+ this.require(3);
155
+ const value = this.view.getUint8(this.pos) << 16 | this.view.getUint8(this.pos + 1) << 8 | this.view.getUint8(this.pos + 2);
156
+ this.pos += 3;
157
+ return value >>> 0;
158
+ }
159
+ u32() {
160
+ this.require(4);
161
+ const value = this.view.getUint32(this.pos);
162
+ this.pos += 4;
163
+ return value >>> 0;
164
+ }
165
+ /**
166
+ * Returns a JS number, not a BigInt. Values above Number.MAX_SAFE_INTEGER are
167
+ * rejected rather than silently losing precision — a 9-petabyte box size is a
168
+ * malformed file, not something to accommodate.
169
+ */
170
+ u64() {
171
+ this.require(8);
172
+ const value = this.view.getBigUint64(this.pos);
173
+ this.pos += 8;
174
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
175
+ throw new HeicParseError("64-bit value exceeds the safe integer range", {
176
+ offset: this.base + this.pos - 8
177
+ });
178
+ }
179
+ return Number(value);
180
+ }
181
+ /** Reads a big-endian unsigned integer of 0, 1, 2, 4 or 8 bytes. `iloc` needs this. */
182
+ uint(byteCount) {
183
+ switch (byteCount) {
184
+ case 0:
185
+ return 0;
186
+ case 1:
187
+ return this.u8();
188
+ case 2:
189
+ return this.u16();
190
+ case 4:
191
+ return this.u32();
192
+ case 8:
193
+ return this.u64();
194
+ default:
195
+ throw new HeicParseError(`Unsupported integer width: ${byteCount} bytes`, {
196
+ offset: this.base + this.pos
197
+ });
198
+ }
199
+ }
200
+ /** Four-character box type. Non-printable bytes are escaped so error messages stay readable. */
201
+ fourCC() {
202
+ this.require(4);
203
+ let out = "";
204
+ for (let i = 0; i < 4; i++) {
205
+ const byte = this.view.getUint8(this.pos + i);
206
+ out += byte >= 32 && byte <= 126 ? String.fromCharCode(byte) : `\\x${byte.toString(16).padStart(2, "0")}`;
207
+ }
208
+ this.pos += 4;
209
+ return out;
210
+ }
211
+ /** NUL-terminated UTF-8 string. Stops at the window end if the NUL is missing. */
212
+ cString() {
213
+ const start = this.pos;
214
+ while (this.pos < this.length && this.bytes[this.pos] !== 0) this.pos++;
215
+ const raw = this.bytes.subarray(start, this.pos);
216
+ if (this.pos < this.length) this.pos++;
217
+ return new TextDecoder().decode(raw);
218
+ }
219
+ /** A view onto the next `count` bytes. No copy — do not retain past the buffer's life. */
220
+ view_(count) {
221
+ this.require(count);
222
+ const out = this.bytes.subarray(this.pos, this.pos + count);
223
+ this.pos += count;
224
+ return out;
225
+ }
226
+ /** A copy of the next `count` bytes. Use when the result outlives the source buffer. */
227
+ copy(count) {
228
+ return new Uint8Array(this.view_(count));
229
+ }
230
+ /** A sub-reader over `count` bytes, advancing this reader past them. */
231
+ sub(count) {
232
+ this.require(count);
233
+ const child = new _Reader(this.bytes, this.pos, count);
234
+ this.pos += count;
235
+ return child;
236
+ }
237
+ /** A sub-reader over the rest of the window, without advancing this reader. */
238
+ peekRest() {
239
+ return new _Reader(this.bytes, this.pos, this.remaining);
240
+ }
241
+ };
242
+ function readFullBoxHeader(reader) {
243
+ const version = reader.u8();
244
+ const flags = reader.u24();
245
+ return { version, flags };
246
+ }
247
+
248
+ // src/parser/hvcc.ts
249
+ var NAL_VPS = 32;
250
+ var NAL_SPS = 33;
251
+ var NAL_PPS = 34;
252
+ var MAX_HVCC_ARRAYS = 32;
253
+ var MAX_NALUS_PER_ARRAY = 256;
254
+ function parseHvcC(reader) {
255
+ const raw = reader.peekRest().bytes;
256
+ const configurationVersion = reader.u8();
257
+ if (configurationVersion !== 1) {
258
+ throw new HeicParseError(
259
+ `Unsupported HEVCDecoderConfigurationRecord version ${configurationVersion}`,
260
+ { box: "hvcC" }
261
+ );
262
+ }
263
+ const profileByte = reader.u8();
264
+ const generalProfileSpace = profileByte >> 6 & 3;
265
+ const generalTierFlag = profileByte >> 5 & 1;
266
+ const generalProfileIdc = profileByte & 31;
267
+ const generalProfileCompatibilityFlags = reader.u32();
268
+ const generalConstraintIndicatorFlags = reader.copy(6);
269
+ const generalLevelIdc = reader.u8();
270
+ const minSpatialSegmentationIdc = reader.u16() & 4095;
271
+ const parallelismType = reader.u8() & 3;
272
+ const chromaFormat = reader.u8() & 3;
273
+ const bitDepthLumaMinus8 = reader.u8() & 7;
274
+ const bitDepthChromaMinus8 = reader.u8() & 7;
275
+ const avgFrameRate = reader.u16();
276
+ const rateByte = reader.u8();
277
+ const constantFrameRate = rateByte >> 6 & 3;
278
+ const numTemporalLayers = rateByte >> 3 & 7;
279
+ const temporalIdNested = rateByte >> 2 & 1;
280
+ const lengthSizeMinusOne = rateByte & 3;
281
+ const numOfArrays = reader.u8();
282
+ if (numOfArrays > MAX_HVCC_ARRAYS) {
283
+ throw new HeicParseError(`hvcC declares ${numOfArrays} NAL arrays`, { box: "hvcC" });
284
+ }
285
+ const arrays = [];
286
+ for (let i = 0; i < numOfArrays; i++) {
287
+ const head = reader.u8();
288
+ const arrayCompleteness = (head >> 7 & 1) === 1;
289
+ const nalUnitType = head & 63;
290
+ const numNalus = reader.u16();
291
+ if (numNalus > MAX_NALUS_PER_ARRAY) {
292
+ throw new HeicParseError(`hvcC array declares ${numNalus} NAL units`, { box: "hvcC" });
293
+ }
294
+ const nalus = [];
295
+ for (let j = 0; j < numNalus; j++) {
296
+ const nalUnitLength = reader.u16();
297
+ nalus.push(reader.view_(nalUnitLength));
298
+ }
299
+ arrays.push({ arrayCompleteness, nalUnitType, nalus });
300
+ }
301
+ return {
302
+ configurationVersion,
303
+ generalProfileSpace,
304
+ generalTierFlag,
305
+ generalProfileIdc,
306
+ generalProfileCompatibilityFlags,
307
+ generalConstraintIndicatorFlags,
308
+ generalLevelIdc,
309
+ minSpatialSegmentationIdc,
310
+ parallelismType,
311
+ chromaFormat,
312
+ bitDepthLumaMinus8,
313
+ bitDepthChromaMinus8,
314
+ avgFrameRate,
315
+ constantFrameRate,
316
+ numTemporalLayers,
317
+ temporalIdNested,
318
+ lengthSizeMinusOne,
319
+ arrays,
320
+ raw
321
+ };
322
+ }
323
+ var PROFILE_SPACE_PREFIX = ["", "A", "B", "C"];
324
+ function hvccToCodecString(hvcc, fourCC = "hvc1") {
325
+ const space = PROFILE_SPACE_PREFIX[hvcc.generalProfileSpace] ?? "";
326
+ const profile = `${space}${hvcc.generalProfileIdc}`;
327
+ const compat = reverseBits32(hvcc.generalProfileCompatibilityFlags).toString(16);
328
+ const tier = hvcc.generalTierFlag === 1 ? "H" : "L";
329
+ const level = `${tier}${hvcc.generalLevelIdc}`;
330
+ const constraintBytes = [...hvcc.generalConstraintIndicatorFlags];
331
+ while (constraintBytes.length > 0 && constraintBytes[constraintBytes.length - 1] === 0) {
332
+ constraintBytes.pop();
333
+ }
334
+ const constraints = constraintBytes.map((b) => b.toString(16).padStart(2, "0").toUpperCase());
335
+ return [fourCC, profile, compat, level, ...constraints].join(".");
336
+ }
337
+ function reverseBits32(value) {
338
+ let v = value >>> 0;
339
+ v = (v & 1431655765) << 1 | v >>> 1 & 1431655765;
340
+ v = (v & 858993459) << 2 | v >>> 2 & 858993459;
341
+ v = (v & 252645135) << 4 | v >>> 4 & 252645135;
342
+ v = (v & 16711935) << 8 | v >>> 8 & 16711935;
343
+ v = v >>> 16 | v << 16;
344
+ return v >>> 0;
345
+ }
346
+ function hvccBitDepth(hvcc) {
347
+ return hvcc.bitDepthLumaMinus8 + 8;
348
+ }
349
+ function hvccToAnnexBPrologue(hvcc) {
350
+ const wanted = [NAL_VPS, NAL_SPS, NAL_PPS];
351
+ const selected = wanted.flatMap((type) => hvcc.arrays.filter((a) => a.nalUnitType === type)).flatMap((a) => a.nalus);
352
+ let total = 0;
353
+ for (const nalu of selected) total += 4 + nalu.byteLength;
354
+ const out = new Uint8Array(total);
355
+ let pos = 0;
356
+ for (const nalu of selected) {
357
+ out.set([0, 0, 0, 1], pos);
358
+ pos += 4;
359
+ out.set(nalu, pos);
360
+ pos += nalu.byteLength;
361
+ }
362
+ return out;
363
+ }
364
+ function lengthPrefixedToAnnexB(data, lengthSize) {
365
+ if (lengthSize < 1 || lengthSize > 4) {
366
+ throw new HeicParseError(`Invalid NAL length size ${lengthSize}`, { box: "hvcC" });
367
+ }
368
+ const out = new Uint8Array(data.byteLength + countNalUnits(data, lengthSize) * (4 - lengthSize));
369
+ let read = 0;
370
+ let write = 0;
371
+ while (read + lengthSize <= data.byteLength) {
372
+ let naluLength = 0;
373
+ for (let i = 0; i < lengthSize; i++) naluLength = naluLength << 8 | data[read + i];
374
+ read += lengthSize;
375
+ if (naluLength < 0 || read + naluLength > data.byteLength) {
376
+ throw new HeicParseError("NAL unit length runs past the end of the item payload", {
377
+ offset: read
378
+ });
379
+ }
380
+ out.set([0, 0, 0, 1], write);
381
+ write += 4;
382
+ out.set(data.subarray(read, read + naluLength), write);
383
+ write += naluLength;
384
+ read += naluLength;
385
+ }
386
+ return out.subarray(0, write);
387
+ }
388
+ function countNalUnits(data, lengthSize) {
389
+ let read = 0;
390
+ let count = 0;
391
+ while (read + lengthSize <= data.byteLength) {
392
+ let naluLength = 0;
393
+ for (let i = 0; i < lengthSize; i++) naluLength = naluLength << 8 | data[read + i];
394
+ read += lengthSize + naluLength;
395
+ if (naluLength < 0 || read > data.byteLength) break;
396
+ count++;
397
+ }
398
+ return count;
399
+ }
400
+
401
+ // src/parser/boxes.ts
402
+ var MAX_BOX_DEPTH = 32;
403
+ var MAX_SIBLING_BOXES = 65536;
404
+ function* walkBoxes(reader, options = {}) {
405
+ const { depth = 0, lenient = false } = typeof options === "number" ? { depth: options, lenient: false } : options;
406
+ if (depth > MAX_BOX_DEPTH) {
407
+ throw new HeicParseError(`Box nesting deeper than ${MAX_BOX_DEPTH}`, {
408
+ offset: reader.absoluteOffset
409
+ });
410
+ }
411
+ let count = 0;
412
+ while (reader.remaining >= 8) {
413
+ if (++count > MAX_SIBLING_BOXES) {
414
+ throw new HeicParseError(`More than ${MAX_SIBLING_BOXES} sibling boxes at one level`, {
415
+ offset: reader.absoluteOffset
416
+ });
417
+ }
418
+ const offset = reader.absoluteOffset;
419
+ const start = reader.offset;
420
+ let size = reader.u32();
421
+ const type = reader.fourCC();
422
+ let headerSize = 8;
423
+ if (size === 1) {
424
+ size = reader.u64();
425
+ headerSize = 16;
426
+ } else if (size === 0) {
427
+ size = reader.length - start;
428
+ }
429
+ if (size < headerSize) {
430
+ if (lenient) return;
431
+ throw new HeicParseError(`Box size ${size} is smaller than its ${headerSize}-byte header`, {
432
+ offset,
433
+ box: type
434
+ });
435
+ }
436
+ if (start + size > reader.length) {
437
+ if (lenient) return;
438
+ throw new HeicParseError(
439
+ `Box extends ${start + size - reader.length} bytes past its container`,
440
+ { offset, box: type }
441
+ );
442
+ }
443
+ const payloadSize = size - headerSize;
444
+ const body = reader.sub(payloadSize);
445
+ yield { type, offset, size, headerSize, body };
446
+ reader.seek(start + size);
447
+ }
448
+ }
449
+ function childBoxes(reader, options = {}) {
450
+ return [...walkBoxes(reader, options)];
451
+ }
452
+ function findBox(boxes, type) {
453
+ return boxes.find((box) => box.type === type);
454
+ }
455
+ function findBoxes(boxes, type) {
456
+ return boxes.filter((box) => box.type === type);
457
+ }
458
+
459
+ // src/parser/meta.ts
460
+ var MAX_ITEMS = 65536;
461
+ var MAX_EXTENTS_PER_ITEM = 4096;
462
+ var MAX_PROPERTIES = 4096;
463
+ var MAX_ASSOCIATIONS_PER_ITEM = 256;
464
+ var MAX_REFERENCES_PER_ITEM = 8192;
465
+ function parseInfe(box) {
466
+ const r = box.body;
467
+ const { version, flags } = readFullBoxHeader(r);
468
+ const hidden = (flags & 1) === 1;
469
+ if (version >= 2) {
470
+ const itemId2 = version === 2 ? r.u16() : r.u32();
471
+ const protectionIndex2 = r.u16();
472
+ const itemType = r.fourCC();
473
+ const itemName2 = r.cString();
474
+ const info = { itemId: itemId2, protectionIndex: protectionIndex2, itemType, itemName: itemName2, hidden };
475
+ if (itemType === "mime") info.contentType = r.cString();
476
+ return info;
477
+ }
478
+ const itemId = r.u16();
479
+ const protectionIndex = r.u16();
480
+ const itemName = r.cString();
481
+ const contentType = r.cString();
482
+ return { itemId, protectionIndex, itemType: "", itemName, contentType, hidden };
483
+ }
484
+ function parseIinf(box) {
485
+ const r = box.body;
486
+ const { version } = readFullBoxHeader(r);
487
+ const entryCount = version === 0 ? r.u16() : r.u32();
488
+ if (entryCount > MAX_ITEMS) {
489
+ throw new HeicParseError(`iinf declares ${entryCount} items`, { box: "iinf" });
490
+ }
491
+ const items = /* @__PURE__ */ new Map();
492
+ let seen = 0;
493
+ for (const child of walkBoxes(r.peekRest())) {
494
+ if (child.type !== "infe") continue;
495
+ if (++seen > MAX_ITEMS) break;
496
+ const info = parseInfe(child);
497
+ items.set(info.itemId, info);
498
+ }
499
+ return items;
500
+ }
501
+ function parseIloc(box) {
502
+ const r = box.body;
503
+ const { version } = readFullBoxHeader(r);
504
+ const sizesByte = r.u8();
505
+ const offsetSize = sizesByte >> 4 & 15;
506
+ const lengthSize = sizesByte & 15;
507
+ const baseByte = r.u8();
508
+ const baseOffsetSize = baseByte >> 4 & 15;
509
+ const indexSize = version === 1 || version === 2 ? baseByte & 15 : 0;
510
+ const itemCount = version < 2 ? r.u16() : r.u32();
511
+ if (itemCount > MAX_ITEMS) {
512
+ throw new HeicParseError(`iloc declares ${itemCount} items`, { box: "iloc" });
513
+ }
514
+ const locations = /* @__PURE__ */ new Map();
515
+ for (let i = 0; i < itemCount; i++) {
516
+ const itemId = version < 2 ? r.u16() : r.u32();
517
+ let constructionMethod = 0;
518
+ if (version === 1 || version === 2) {
519
+ constructionMethod = r.u16() & 15;
520
+ }
521
+ r.u16();
522
+ const baseOffset = r.uint(baseOffsetSize);
523
+ const extentCount = r.u16();
524
+ if (extentCount > MAX_EXTENTS_PER_ITEM) {
525
+ throw new HeicParseError(`Item ${itemId} declares ${extentCount} extents`, {
526
+ box: "iloc",
527
+ itemId
528
+ });
529
+ }
530
+ const extents = [];
531
+ for (let j = 0; j < extentCount; j++) {
532
+ if ((version === 1 || version === 2) && indexSize > 0) r.uint(indexSize);
533
+ const offset = r.uint(offsetSize);
534
+ const length = r.uint(lengthSize);
535
+ extents.push({ offset, length });
536
+ }
537
+ locations.set(itemId, { itemId, constructionMethod, baseOffset, extents });
538
+ }
539
+ return locations;
540
+ }
541
+ function parseProperty(box) {
542
+ const r = box.body;
543
+ switch (box.type) {
544
+ case "ispe": {
545
+ readFullBoxHeader(r);
546
+ return { type: "ispe", width: r.u32(), height: r.u32() };
547
+ }
548
+ case "hvcC":
549
+ return { type: "hvcC", hvcc: parseHvcC(r) };
550
+ case "irot": {
551
+ const angle = (r.u8() & 3) * 90;
552
+ return { type: "irot", angle };
553
+ }
554
+ case "imir": {
555
+ const axis = r.u8() & 1;
556
+ return { type: "imir", axis };
557
+ }
558
+ case "colr": {
559
+ const colorType = r.fourCC();
560
+ if (colorType === "nclx") {
561
+ const primaries = r.u16();
562
+ const transfer = r.u16();
563
+ const matrix = r.u16();
564
+ const fullRange = (r.u8() & 128) !== 0;
565
+ return { type: "colr", colorType: "nclx", primaries, transfer, matrix, fullRange };
566
+ }
567
+ if (colorType === "rICC" || colorType === "prof") {
568
+ return { type: "colr", colorType: "icc", profile: r.copy(r.remaining) };
569
+ }
570
+ return { type: "unknown", boxType: `colr:${colorType}` };
571
+ }
572
+ case "pixi": {
573
+ readFullBoxHeader(r);
574
+ const numChannels = r.u8();
575
+ const bitsPerChannel = [];
576
+ for (let i = 0; i < numChannels; i++) bitsPerChannel.push(r.u8());
577
+ return { type: "pixi", bitsPerChannel };
578
+ }
579
+ case "clap":
580
+ return {
581
+ type: "clap",
582
+ widthN: r.u32(),
583
+ widthD: r.u32(),
584
+ heightN: r.u32(),
585
+ heightD: r.u32(),
586
+ horizOffN: r.u32() | 0,
587
+ // stored as a signed 32-bit numerator
588
+ horizOffD: r.u32(),
589
+ vertOffN: r.u32() | 0,
590
+ vertOffD: r.u32()
591
+ };
592
+ case "auxC": {
593
+ readFullBoxHeader(r);
594
+ return { type: "auxC", auxType: r.cString() };
595
+ }
596
+ default:
597
+ return { type: "unknown", boxType: box.type };
598
+ }
599
+ }
600
+ function parseIpma(box, into) {
601
+ const r = box.body;
602
+ const { version, flags } = readFullBoxHeader(r);
603
+ const wideIndex = (flags & 1) === 1;
604
+ const entryCount = r.u32();
605
+ if (entryCount > MAX_ITEMS) {
606
+ throw new HeicParseError(`ipma declares ${entryCount} entries`, { box: "ipma" });
607
+ }
608
+ for (let i = 0; i < entryCount; i++) {
609
+ const itemId = version === 0 ? r.u16() : r.u32();
610
+ const associationCount = r.u8();
611
+ if (associationCount > MAX_ASSOCIATIONS_PER_ITEM) {
612
+ throw new HeicParseError(`Item ${itemId} declares ${associationCount} properties`, {
613
+ box: "ipma",
614
+ itemId
615
+ });
616
+ }
617
+ const associations = [];
618
+ for (let j = 0; j < associationCount; j++) {
619
+ if (wideIndex) {
620
+ const value = r.u16();
621
+ associations.push({ essential: (value & 32768) !== 0, index: value & 32767 });
622
+ } else {
623
+ const value = r.u8();
624
+ associations.push({ essential: (value & 128) !== 0, index: value & 127 });
625
+ }
626
+ }
627
+ const existing = into.get(itemId);
628
+ if (existing) existing.push(...associations);
629
+ else into.set(itemId, associations);
630
+ }
631
+ }
632
+ function parseIprp(box) {
633
+ const children = childBoxes(box.body);
634
+ const ipco = findBox(children, "ipco");
635
+ const properties = [];
636
+ if (ipco) {
637
+ for (const child of walkBoxes(ipco.body)) {
638
+ if (properties.length >= MAX_PROPERTIES) {
639
+ throw new HeicParseError(`ipco holds more than ${MAX_PROPERTIES} properties`, {
640
+ box: "ipco"
641
+ });
642
+ }
643
+ properties.push(parseProperty(child));
644
+ }
645
+ }
646
+ const associations = /* @__PURE__ */ new Map();
647
+ for (const ipma of findBoxes(children, "ipma")) parseIpma(ipma, associations);
648
+ return { properties, associations };
649
+ }
650
+ function parseIref(box) {
651
+ const r = box.body;
652
+ const { version } = readFullBoxHeader(r);
653
+ const refs = /* @__PURE__ */ new Map();
654
+ for (const child of walkBoxes(r.peekRest())) {
655
+ const cr = child.body;
656
+ const fromItemId = version === 0 ? cr.u16() : cr.u32();
657
+ const referenceCount = cr.u16();
658
+ if (referenceCount > MAX_REFERENCES_PER_ITEM) {
659
+ throw new HeicParseError(`Item ${fromItemId} declares ${referenceCount} references`, {
660
+ box: child.type,
661
+ itemId: fromItemId
662
+ });
663
+ }
664
+ const toItemIds = [];
665
+ for (let i = 0; i < referenceCount; i++) {
666
+ toItemIds.push(version === 0 ? cr.u16() : cr.u32());
667
+ }
668
+ let byType = refs.get(child.type);
669
+ if (!byType) refs.set(child.type, byType = /* @__PURE__ */ new Map());
670
+ byType.set(fromItemId, toItemIds);
671
+ }
672
+ return refs;
673
+ }
674
+ function parseHeif(input, options = {}) {
675
+ const source = input instanceof Uint8Array ? input : new Uint8Array(input);
676
+ const root = new Reader(source);
677
+ const boxes = childBoxes(root, { lenient: options.truncated === true });
678
+ const ftyp = findBox(boxes, "ftyp");
679
+ if (!ftyp) throw new HeicParseError("No 'ftyp' box: this is not an ISOBMFF file", { offset: 0 });
680
+ const majorBrand = ftyp.body.fourCC();
681
+ const minorVersion = ftyp.body.u32();
682
+ const compatibleBrands = [];
683
+ while (ftyp.body.remaining >= 4) compatibleBrands.push(ftyp.body.fourCC());
684
+ const meta = findBox(boxes, "meta");
685
+ if (!meta) {
686
+ throw new HeicParseError("No 'meta' box: not a HEIF image file", { brand: majorBrand });
687
+ }
688
+ readFullBoxHeader(meta.body);
689
+ const metaChildren = childBoxes(meta.body, 1);
690
+ const hdlr = findBox(metaChildren, "hdlr");
691
+ let handlerType = "";
692
+ if (hdlr) {
693
+ readFullBoxHeader(hdlr.body);
694
+ hdlr.body.u32();
695
+ handlerType = hdlr.body.fourCC();
696
+ }
697
+ if (handlerType && handlerType !== "pict") {
698
+ throw new HeicParseError(`meta handler is '${handlerType}', expected 'pict'`, {
699
+ brand: majorBrand
700
+ });
701
+ }
702
+ let primaryItemId = 0;
703
+ const pitm = findBox(metaChildren, "pitm");
704
+ if (pitm) {
705
+ const { version } = readFullBoxHeader(pitm.body);
706
+ primaryItemId = version === 0 ? pitm.body.u16() : pitm.body.u32();
707
+ }
708
+ const iinf = findBox(metaChildren, "iinf");
709
+ const items = iinf ? parseIinf(iinf) : /* @__PURE__ */ new Map();
710
+ const iloc = findBox(metaChildren, "iloc");
711
+ const locations = iloc ? parseIloc(iloc) : /* @__PURE__ */ new Map();
712
+ const iprp = findBox(metaChildren, "iprp");
713
+ const itemProperties = iprp ? parseIprp(iprp) : { properties: [], associations: /* @__PURE__ */ new Map() };
714
+ const iref = findBox(metaChildren, "iref");
715
+ const references = iref ? parseIref(iref) : /* @__PURE__ */ new Map();
716
+ const idat = findBox(metaChildren, "idat");
717
+ const itemData = idat ? idat.body.copy(idat.body.remaining) : void 0;
718
+ if (primaryItemId === 0) {
719
+ for (const [id, info] of items) {
720
+ if (info.itemType === "hvc1" || info.itemType === "hev1" || info.itemType === "grid") {
721
+ primaryItemId = id;
722
+ break;
723
+ }
724
+ }
725
+ }
726
+ return {
727
+ majorBrand,
728
+ minorVersion,
729
+ compatibleBrands,
730
+ primaryItemId,
731
+ handlerType,
732
+ items,
733
+ locations,
734
+ itemProperties,
735
+ references,
736
+ itemData,
737
+ source
738
+ };
739
+ }
740
+ function propertiesForItem(file, itemId) {
741
+ const associations = file.itemProperties.associations.get(itemId) ?? [];
742
+ const out = [];
743
+ for (const association of associations) {
744
+ if (association.index === 0) continue;
745
+ const property = file.itemProperties.properties[association.index - 1];
746
+ if (!property) {
747
+ throw new HeicParseError(
748
+ `Item ${itemId} references property ${association.index}, but ipco holds ${file.itemProperties.properties.length}`,
749
+ { itemId, box: "ipma" }
750
+ );
751
+ }
752
+ if (association.essential && property.type === "unknown") {
753
+ throw new HeicParseError(
754
+ `Item ${itemId} requires unsupported essential property '${property.boxType}'`,
755
+ { itemId, box: property.boxType }
756
+ );
757
+ }
758
+ out.push(property);
759
+ }
760
+ return out;
761
+ }
762
+ function findProperty(properties, type) {
763
+ return properties.find((p) => p.type === type);
764
+ }
765
+ function readItemData(file, itemId) {
766
+ const location = file.locations.get(itemId);
767
+ if (!location) {
768
+ throw new HeicParseError(`No iloc entry for item ${itemId}`, { itemId, box: "iloc" });
769
+ }
770
+ const info = file.items.get(itemId);
771
+ const context2 = { itemId, itemType: info?.itemType, box: "iloc" };
772
+ let container;
773
+ switch (location.constructionMethod) {
774
+ case 0:
775
+ container = file.source;
776
+ break;
777
+ case 1:
778
+ if (!file.itemData) {
779
+ throw new HeicParseError(
780
+ `Item ${itemId} points into 'idat', but the file has no idat box`,
781
+ context2
782
+ );
783
+ }
784
+ container = file.itemData;
785
+ break;
786
+ case 2:
787
+ throw new HeicParseError(
788
+ `Item ${itemId} uses construction_method 2 (item offset), which is not supported`,
789
+ context2
790
+ );
791
+ default:
792
+ throw new HeicParseError(
793
+ `Item ${itemId} uses unknown construction_method ${location.constructionMethod}`,
794
+ context2
795
+ );
796
+ }
797
+ let total = 0;
798
+ for (const extent of location.extents) {
799
+ const start = location.baseOffset + extent.offset;
800
+ const length = extent.length === 0 ? container.byteLength - start : extent.length;
801
+ if (start < 0 || length < 0 || start + length > container.byteLength) {
802
+ throw new HeicParseError(
803
+ `Item ${itemId} extent [${start}, ${start + length}) is outside its ${container.byteLength}-byte container`,
804
+ context2
805
+ );
806
+ }
807
+ total += length;
808
+ }
809
+ if (location.extents.length === 1) {
810
+ const extent = location.extents[0];
811
+ const start = location.baseOffset + extent.offset;
812
+ return container.subarray(start, start + total);
813
+ }
814
+ const out = new Uint8Array(total);
815
+ let pos = 0;
816
+ for (const extent of location.extents) {
817
+ const start = location.baseOffset + extent.offset;
818
+ const length = extent.length === 0 ? container.byteLength - start : extent.length;
819
+ out.set(container.subarray(start, start + length), pos);
820
+ pos += length;
821
+ }
822
+ return out;
823
+ }
824
+
825
+ // src/parser/grid.ts
826
+ var MAX_TILES = 4096;
827
+ function parseGridPayload(payload) {
828
+ const r = new Reader(payload);
829
+ const version = r.u8();
830
+ if (version !== 0) {
831
+ throw new HeicParseError(`Unsupported grid version ${version}`, { itemType: "grid" });
832
+ }
833
+ const flags = r.u8();
834
+ const wideFields = (flags & 1) === 1;
835
+ const rows = r.u8() + 1;
836
+ const columns = r.u8() + 1;
837
+ const outputWidth = wideFields ? r.u32() : r.u16();
838
+ const outputHeight = wideFields ? r.u32() : r.u16();
839
+ return { rows, columns, outputWidth, outputHeight };
840
+ }
841
+ function readGrid(file, itemId, warnings = []) {
842
+ const payload = readItemData(file, itemId);
843
+ const { rows, columns, outputWidth, outputHeight } = parseGridPayload(payload);
844
+ const tileItemIds = file.references.get("dimg")?.get(itemId) ?? [];
845
+ if (tileItemIds.length === 0) {
846
+ throw new HeicParseError(`Grid item ${itemId} has no 'dimg' tile references`, {
847
+ itemId,
848
+ itemType: "grid"
849
+ });
850
+ }
851
+ const expected = rows * columns;
852
+ if (expected !== tileItemIds.length) {
853
+ throw new HeicParseError(
854
+ `Grid item ${itemId} declares ${rows}x${columns} = ${expected} tiles but 'dimg' lists ${tileItemIds.length}`,
855
+ { itemId, itemType: "grid" }
856
+ );
857
+ }
858
+ if (expected > MAX_TILES) {
859
+ throw new HeicParseError(`Grid item ${itemId} declares ${expected} tiles (max ${MAX_TILES})`, {
860
+ itemId,
861
+ itemType: "grid"
862
+ });
863
+ }
864
+ let width = outputWidth;
865
+ let height = outputHeight;
866
+ const ispe = findProperty(propertiesForItem(file, itemId), "ispe");
867
+ if (ispe && (ispe.width !== outputWidth || ispe.height !== outputHeight)) {
868
+ warnings.push({
869
+ code: "grid-dimension-mismatch",
870
+ message: `Grid payload declares ${outputWidth}x${outputHeight} but ispe declares ${ispe.width}x${ispe.height}; using ispe`
871
+ });
872
+ width = ispe.width;
873
+ height = ispe.height;
874
+ }
875
+ return { rows, columns, outputWidth: width, outputHeight: height, tileItemIds };
876
+ }
877
+
878
+ // src/plan.ts
879
+ var MAX_TOTAL_PIXELS = 256e6;
880
+ function planDecode(input) {
881
+ const file = parseHeif(input);
882
+ const warnings = [];
883
+ const primaryItemId = file.primaryItemId;
884
+ const info = file.items.get(primaryItemId);
885
+ if (!info) {
886
+ throw new HeicParseError(`Primary item ${primaryItemId} is not described by iinf`, {
887
+ brand: file.majorBrand,
888
+ itemId: primaryItemId
889
+ });
890
+ }
891
+ const isGrid = info.itemType === "grid";
892
+ if (!isGrid && info.itemType !== "hvc1" && info.itemType !== "hev1") {
893
+ throw new HeicUnsupportedError(
894
+ `Primary item type '${info.itemType}' is not a supported image item`,
895
+ [],
896
+ { brand: file.majorBrand, itemType: info.itemType, itemId: primaryItemId }
897
+ );
898
+ }
899
+ const primaryProps = propertiesForItem(file, primaryItemId);
900
+ const tiles = [];
901
+ let codedWidth;
902
+ let codedHeight;
903
+ if (isGrid) {
904
+ const grid = readGrid(file, primaryItemId, warnings);
905
+ codedWidth = grid.outputWidth;
906
+ codedHeight = grid.outputHeight;
907
+ const firstTileProps = propertiesForItem(file, grid.tileItemIds[0]);
908
+ const firstIspe = findProperty(firstTileProps, "ispe");
909
+ if (!firstIspe) {
910
+ throw new HeicParseError(`Grid tile ${grid.tileItemIds[0]} has no ispe`, {
911
+ itemId: grid.tileItemIds[0]
912
+ });
913
+ }
914
+ for (const [index, itemId] of grid.tileItemIds.entries()) {
915
+ const ispe = findProperty(propertiesForItem(file, itemId), "ispe") ?? firstIspe;
916
+ tiles.push({
917
+ itemId,
918
+ x: index % grid.columns * firstIspe.width,
919
+ y: Math.floor(index / grid.columns) * firstIspe.height,
920
+ width: ispe.width,
921
+ height: ispe.height
922
+ });
923
+ }
924
+ } else {
925
+ const ispe = findProperty(primaryProps, "ispe");
926
+ if (!ispe) {
927
+ throw new HeicParseError(`Primary item ${primaryItemId} has no ispe`, {
928
+ itemId: primaryItemId
929
+ });
930
+ }
931
+ codedWidth = ispe.width;
932
+ codedHeight = ispe.height;
933
+ tiles.push({ itemId: primaryItemId, x: 0, y: 0, width: ispe.width, height: ispe.height });
934
+ }
935
+ if (codedWidth <= 0 || codedHeight <= 0) {
936
+ throw new HeicParseError(`Implausible image dimensions ${codedWidth}x${codedHeight}`, {
937
+ itemId: primaryItemId
938
+ });
939
+ }
940
+ if (codedWidth * codedHeight > MAX_TOTAL_PIXELS) {
941
+ throw new HeicUnsupportedError(
942
+ `Image is ${codedWidth}x${codedHeight}, above the ${MAX_TOTAL_PIXELS}-pixel limit`,
943
+ [],
944
+ { itemId: primaryItemId }
945
+ );
946
+ }
947
+ const tileGroups = groupTilesByConfig(file, tiles, warnings);
948
+ const transforms = readTransforms(primaryProps, codedWidth, codedHeight);
949
+ const { displayWidth, displayHeight } = applyTransformsToSize(
950
+ codedWidth,
951
+ codedHeight,
952
+ transforms
953
+ );
954
+ const pixi = findProperty(primaryProps, "pixi");
955
+ const bitDepth = pixi?.bitsPerChannel[0] ?? hvccBitDepth(tileGroups[0].hvcc);
956
+ collectFeatureWarnings(file, primaryItemId, warnings);
957
+ return {
958
+ file,
959
+ primaryItemId,
960
+ isGrid,
961
+ codedWidth,
962
+ codedHeight,
963
+ displayWidth,
964
+ displayHeight,
965
+ tiles,
966
+ tileGroups,
967
+ transforms,
968
+ bitDepth,
969
+ sourceColor: readSourceColor(primaryProps, propertiesForItem(file, tiles[0].itemId)),
970
+ warnings
971
+ };
972
+ }
973
+ function groupTilesByConfig(file, tiles, warnings) {
974
+ const groups = /* @__PURE__ */ new Map();
975
+ for (const [index, tile] of tiles.entries()) {
976
+ const associations = file.itemProperties.associations.get(tile.itemId) ?? [];
977
+ const association = associations.find(
978
+ (a) => file.itemProperties.properties[a.index - 1]?.type === "hvcC"
979
+ );
980
+ if (!association) {
981
+ throw new HeicParseError(`Item ${tile.itemId} has no hvcC property`, { itemId: tile.itemId });
982
+ }
983
+ let group = groups.get(association.index);
984
+ if (!group) {
985
+ const property = file.itemProperties.properties[association.index - 1];
986
+ if (property?.type !== "hvcC") {
987
+ throw new HeicParseError(`Property ${association.index} is not an hvcC`, {
988
+ itemId: tile.itemId
989
+ });
990
+ }
991
+ group = {
992
+ configIndex: association.index,
993
+ hvcc: property.hvcc,
994
+ codec: hvccToCodecString(property.hvcc),
995
+ tileIndices: []
996
+ };
997
+ groups.set(association.index, group);
998
+ }
999
+ group.tileIndices.push(index);
1000
+ }
1001
+ const result = [...groups.values()];
1002
+ if (result.length === 0) {
1003
+ throw new HeicParseError("No decoder configuration found for any tile", {});
1004
+ }
1005
+ if (result.length > 1) {
1006
+ warnings.push({
1007
+ code: "mixed-tile-configs",
1008
+ message: `Tiles use ${result.length} different decoder configurations; decoding in ${result.length} groups`
1009
+ });
1010
+ }
1011
+ return result;
1012
+ }
1013
+ function readTransforms(properties, width, height) {
1014
+ const ops = [];
1015
+ let currentWidth = width;
1016
+ let currentHeight = height;
1017
+ for (const property of properties) {
1018
+ switch (property.type) {
1019
+ case "clap": {
1020
+ const crop2 = resolveCleanAperture(property, currentWidth, currentHeight);
1021
+ if (crop2) {
1022
+ ops.push(crop2);
1023
+ currentWidth = crop2.width;
1024
+ currentHeight = crop2.height;
1025
+ }
1026
+ break;
1027
+ }
1028
+ case "irot":
1029
+ if (property.angle !== 0) {
1030
+ ops.push({ kind: "rotate", angle: property.angle });
1031
+ if (property.angle === 90 || property.angle === 270) {
1032
+ [currentWidth, currentHeight] = [currentHeight, currentWidth];
1033
+ }
1034
+ }
1035
+ break;
1036
+ case "imir":
1037
+ ops.push({ kind: "mirror", axis: property.axis });
1038
+ break;
1039
+ }
1040
+ }
1041
+ return ops;
1042
+ }
1043
+ function resolveCleanAperture(clap, width, height) {
1044
+ if (clap.widthD === 0 || clap.heightD === 0 || clap.horizOffD === 0 || clap.vertOffD === 0) {
1045
+ return void 0;
1046
+ }
1047
+ const cropWidth = Math.round(clap.widthN / clap.widthD);
1048
+ const cropHeight = Math.round(clap.heightN / clap.heightD);
1049
+ const centreOffsetX = clap.horizOffN / clap.horizOffD;
1050
+ const centreOffsetY = clap.vertOffN / clap.vertOffD;
1051
+ const offsetX = Math.round((width - cropWidth) / 2 + centreOffsetX);
1052
+ const offsetY = Math.round((height - cropHeight) / 2 + centreOffsetY);
1053
+ if (cropWidth <= 0 || cropHeight <= 0) return void 0;
1054
+ if (cropWidth === width && cropHeight === height && offsetX === 0 && offsetY === 0) {
1055
+ return void 0;
1056
+ }
1057
+ if (offsetX < 0 || offsetY < 0 || offsetX + cropWidth > width || offsetY + cropHeight > height) {
1058
+ return void 0;
1059
+ }
1060
+ return { kind: "crop", width: cropWidth, height: cropHeight, offsetX, offsetY };
1061
+ }
1062
+ function applyTransformsToSize(width, height, transforms) {
1063
+ let w = width;
1064
+ let h = height;
1065
+ for (const op of transforms) {
1066
+ if (op.kind === "crop") {
1067
+ w = op.width;
1068
+ h = op.height;
1069
+ } else if (op.kind === "rotate" && (op.angle === 90 || op.angle === 270)) {
1070
+ [w, h] = [h, w];
1071
+ }
1072
+ }
1073
+ return { displayWidth: w, displayHeight: h };
1074
+ }
1075
+ function readSourceColor(primaryProps, tileProps) {
1076
+ const colr = findProperty(primaryProps, "colr") ?? findProperty(tileProps, "colr");
1077
+ if (!colr) return null;
1078
+ return colr.colorType === "nclx" ? {
1079
+ type: "nclx",
1080
+ primaries: colr.primaries,
1081
+ transfer: colr.transfer,
1082
+ matrix: colr.matrix,
1083
+ fullRange: colr.fullRange
1084
+ } : { type: "icc", profile: colr.profile };
1085
+ }
1086
+ function collectFeatureWarnings(file, primaryItemId, warnings) {
1087
+ const seen = /* @__PURE__ */ new Set();
1088
+ const add = (warning) => {
1089
+ if (seen.has(warning.code)) return;
1090
+ seen.add(warning.code);
1091
+ warnings.push(warning);
1092
+ };
1093
+ const auxTargets = file.references.get("auxl");
1094
+ if (auxTargets) {
1095
+ for (const [auxItemId, targets] of auxTargets) {
1096
+ if (!targets.includes(primaryItemId)) continue;
1097
+ const auxType = findProperty(propertiesForItem(file, auxItemId), "auxC")?.auxType ?? "";
1098
+ if (/alpha/i.test(auxType)) {
1099
+ add({ code: "alpha-ignored", message: `Alpha aux image ${auxItemId} ignored` });
1100
+ } else if (/depth|disparity/i.test(auxType)) {
1101
+ add({ code: "depth-ignored", message: `Depth aux image ${auxItemId} ignored` });
1102
+ } else if (/hdrgainmap|gainmap/i.test(auxType)) {
1103
+ add({
1104
+ code: "gain-map-ignored",
1105
+ message: `HDR gain map ${auxItemId} ignored; the image decodes as SDR`
1106
+ });
1107
+ }
1108
+ }
1109
+ }
1110
+ for (const item of file.items.values()) {
1111
+ if (item.itemType === "tmap") {
1112
+ add({
1113
+ code: "gain-map-ignored",
1114
+ message: `Tone-map item ${item.itemId} ignored; the image decodes as SDR`
1115
+ });
1116
+ break;
1117
+ }
1118
+ }
1119
+ }
1120
+ function tileData(plan, tile) {
1121
+ return readItemData(plan.file, tile.itemId);
1122
+ }
1123
+
1124
+ // src/decoders/webcodecs.ts
1125
+ function isWebCodecsAvailable() {
1126
+ return typeof VideoDecoder !== "undefined" && typeof EncodedVideoChunk !== "undefined";
1127
+ }
1128
+ async function resolveConfig(group, codedWidth, codedHeight) {
1129
+ const lengthSize = group.hvcc.lengthSizeMinusOne + 1;
1130
+ const failures = [];
1131
+ const hvc1 = {
1132
+ codec: group.codec,
1133
+ // A fresh copy: VideoDecoderConfig.description is retained by the decoder,
1134
+ // and hvcc.raw is a view onto the caller's buffer.
1135
+ description: new Uint8Array(group.hvcc.raw),
1136
+ codedWidth,
1137
+ codedHeight,
1138
+ optimizeForLatency: true
1139
+ };
1140
+ try {
1141
+ const support = await VideoDecoder.isConfigSupported(hvc1);
1142
+ if (support.supported) return { mode: "hvc1", config: support.config ?? hvc1, lengthSize };
1143
+ failures.push({ strategy: "hvc1", reason: "isConfigSupported returned false" });
1144
+ } catch (error) {
1145
+ failures.push({ strategy: "hvc1", reason: String(error) });
1146
+ }
1147
+ const hev1 = {
1148
+ codec: hvccToCodecString(group.hvcc, "hev1"),
1149
+ codedWidth,
1150
+ codedHeight,
1151
+ optimizeForLatency: true
1152
+ };
1153
+ try {
1154
+ const support = await VideoDecoder.isConfigSupported(hev1);
1155
+ if (support.supported) {
1156
+ return {
1157
+ mode: "hev1",
1158
+ config: support.config ?? hev1,
1159
+ prologue: hvccToAnnexBPrologue(group.hvcc),
1160
+ lengthSize
1161
+ };
1162
+ }
1163
+ failures.push({ strategy: "hev1", reason: "isConfigSupported returned false" });
1164
+ } catch (error) {
1165
+ failures.push({ strategy: "hev1", reason: String(error) });
1166
+ }
1167
+ throw new HeicUnsupportedError(
1168
+ "No HEVC decoder configuration was accepted",
1169
+ failures,
1170
+ { strategy: "webcodecs", codec: group.codec }
1171
+ );
1172
+ }
1173
+ function chunkBytes(config, payload) {
1174
+ if (config.mode === "hvc1") return payload;
1175
+ const body = lengthPrefixedToAnnexB(payload, config.lengthSize);
1176
+ const prologue = config.prologue;
1177
+ const out = new Uint8Array(prologue.byteLength + body.byteLength);
1178
+ out.set(prologue, 0);
1179
+ out.set(body, prologue.byteLength);
1180
+ return out;
1181
+ }
1182
+ async function decodeWithWebCodecs(plan, colorSpace, signal) {
1183
+ if (!isWebCodecsAvailable()) {
1184
+ throw new HeicUnsupportedError(
1185
+ "WebCodecs VideoDecoder is not available in this environment",
1186
+ [{ strategy: "webcodecs", reason: "VideoDecoder is undefined" }],
1187
+ { strategy: "webcodecs" }
1188
+ );
1189
+ }
1190
+ throwIfAborted(signal);
1191
+ const canvas = createCanvas(plan.codedWidth, plan.codedHeight);
1192
+ const ctx = canvas.getContext("2d", { colorSpace, alpha: false, willReadFrequently: false });
1193
+ if (!ctx) {
1194
+ throw new HeicDecodeError("Could not get a 2d context for compositing", {
1195
+ strategy: "webcodecs"
1196
+ });
1197
+ }
1198
+ for (const group of plan.tileGroups) {
1199
+ throwIfAborted(signal);
1200
+ await decodeGroup(plan, group, ctx, signal);
1201
+ }
1202
+ return canvas;
1203
+ }
1204
+ async function decodeGroup(plan, group, ctx, signal) {
1205
+ const first = plan.tiles[group.tileIndices[0]];
1206
+ const config = await resolveConfig(group, first.width, first.height);
1207
+ throwIfAborted(signal);
1208
+ let nextTile = 0;
1209
+ let drawn = 0;
1210
+ let settle;
1211
+ const failure = new Promise((_, reject) => {
1212
+ settle = reject;
1213
+ });
1214
+ const decoder = new VideoDecoder({
1215
+ output: (frame) => {
1216
+ try {
1217
+ const tile = plan.tiles[group.tileIndices[nextTile++]];
1218
+ if (tile) {
1219
+ ctx.drawImage(frame, tile.x, tile.y, tile.width, tile.height);
1220
+ drawn++;
1221
+ }
1222
+ } finally {
1223
+ frame.close();
1224
+ }
1225
+ },
1226
+ error: (error) => {
1227
+ settle?.(
1228
+ new HeicDecodeError(`VideoDecoder failed: ${error.message}`, {
1229
+ strategy: "webcodecs",
1230
+ codec: config.config.codec
1231
+ })
1232
+ );
1233
+ }
1234
+ });
1235
+ const onAbort = () => settle?.(new HeicAbortError());
1236
+ signal?.addEventListener("abort", onAbort, { once: true });
1237
+ try {
1238
+ try {
1239
+ decoder.configure(config.config);
1240
+ for (const tileIndex of group.tileIndices) {
1241
+ const tile = plan.tiles[tileIndex];
1242
+ const bytes = chunkBytes(config, tileData(plan, tile));
1243
+ decoder.decode(
1244
+ new EncodedVideoChunk({ type: "key", timestamp: tileIndex, duration: 0, data: bytes })
1245
+ );
1246
+ }
1247
+ } catch (error) {
1248
+ if (error instanceof HeicAbortError || error instanceof HeicDecodeError) throw error;
1249
+ throw new HeicDecodeError(
1250
+ `VideoDecoder rejected the stream: ${error instanceof Error ? error.message : String(error)}`,
1251
+ { strategy: "webcodecs", codec: config.config.codec },
1252
+ { cause: error }
1253
+ );
1254
+ }
1255
+ await Promise.race([decoder.flush(), failure]);
1256
+ if (drawn !== group.tileIndices.length) {
1257
+ throw new HeicDecodeError(
1258
+ `Decoder emitted ${drawn} frames for ${group.tileIndices.length} tiles`,
1259
+ { strategy: "webcodecs", codec: config.config.codec }
1260
+ );
1261
+ }
1262
+ } finally {
1263
+ signal?.removeEventListener("abort", onAbort);
1264
+ try {
1265
+ decoder.close();
1266
+ } catch {
1267
+ }
1268
+ }
1269
+ }
1270
+ var PROBE_CODEC_STRINGS = [
1271
+ "hvc1.3.e.L93.B0",
1272
+ // Main Still Picture, 8-bit — what iPhones write
1273
+ "hvc1.1.6.L93.B0",
1274
+ // Main, 8-bit
1275
+ "hvc1.2.4.L120.B0"
1276
+ // Main10, 10-bit
1277
+ ];
1278
+ async function probeHevcCodecStrings() {
1279
+ if (!isWebCodecsAvailable()) return [];
1280
+ const supported = [];
1281
+ for (const codec of PROBE_CODEC_STRINGS) {
1282
+ try {
1283
+ const support = await VideoDecoder.isConfigSupported({
1284
+ codec,
1285
+ codedWidth: 1920,
1286
+ codedHeight: 1080
1287
+ });
1288
+ if (support.supported) supported.push(codec);
1289
+ } catch {
1290
+ }
1291
+ }
1292
+ return supported;
1293
+ }
1294
+
1295
+ // src/parser/detect.ts
1296
+ var HEIF_BRANDS = /* @__PURE__ */ new Set([
1297
+ "heic",
1298
+ "heix",
1299
+ "hevc",
1300
+ "hevx",
1301
+ "heim",
1302
+ "heis",
1303
+ "hevm",
1304
+ "hevs",
1305
+ "mif1",
1306
+ "msf1"
1307
+ ]);
1308
+ var UNAMBIGUOUS_HEIC_BRANDS = /* @__PURE__ */ new Set([
1309
+ "heic",
1310
+ "heix",
1311
+ "hevc",
1312
+ "hevx",
1313
+ "heim",
1314
+ "heis",
1315
+ "hevm",
1316
+ "hevs"
1317
+ ]);
1318
+ var DETECTION_PREFIX_BYTES = 65536;
1319
+ function detectFromBuffer(input) {
1320
+ const source = input instanceof Uint8Array ? input : new Uint8Array(input);
1321
+ let brands;
1322
+ let brand;
1323
+ try {
1324
+ const boxes = childBoxes(new Reader(source), { lenient: true });
1325
+ const ftyp = findBox(boxes, "ftyp");
1326
+ if (!ftyp) return { isHeic: false };
1327
+ brand = ftyp.body.fourCC();
1328
+ brands = /* @__PURE__ */ new Set([brand]);
1329
+ ftyp.body.u32();
1330
+ while (ftyp.body.remaining >= 4) brands.add(ftyp.body.fourCC());
1331
+ } catch {
1332
+ return { isHeic: false };
1333
+ }
1334
+ if (![...brands].some((b) => HEIF_BRANDS.has(b))) return { isHeic: false, brand };
1335
+ let file;
1336
+ try {
1337
+ file = parseHeif(source, { truncated: true });
1338
+ } catch {
1339
+ file = void 0;
1340
+ }
1341
+ const primaryItemType = file?.items.get(file.primaryItemId)?.itemType;
1342
+ const coding = file ? codingOf(file, primaryItemType) : "unknown";
1343
+ if (coding === "av1") return { isHeic: false, brand, primaryItemType, coding };
1344
+ if (coding === "hevc") return { isHeic: true, brand, primaryItemType, coding };
1345
+ const result = {
1346
+ isHeic: [...brands].some((b) => UNAMBIGUOUS_HEIC_BRANDS.has(b)),
1347
+ brand,
1348
+ coding: "unknown"
1349
+ };
1350
+ if (primaryItemType !== void 0) result.primaryItemType = primaryItemType;
1351
+ return result;
1352
+ }
1353
+ function codingOf(file, itemType, depth = 0) {
1354
+ if (itemType === "hvc1" || itemType === "hev1") return "hevc";
1355
+ if (itemType === "av01") return "av1";
1356
+ if (depth < 4 && (itemType === "grid" || itemType === "iovl" || itemType === "iden")) {
1357
+ const first = file.references.get("dimg")?.get(file.primaryItemId)?.[0];
1358
+ if (first !== void 0) return codingOf(file, file.items.get(first)?.itemType, depth + 1);
1359
+ }
1360
+ return "unknown";
1361
+ }
1362
+
1363
+ // src/render/transform.ts
1364
+ function applyTransforms(source, transforms, colorSpace) {
1365
+ let current = source;
1366
+ for (const op of transforms) {
1367
+ switch (op.kind) {
1368
+ case "crop":
1369
+ current = release(current, crop(current, op, colorSpace), source);
1370
+ break;
1371
+ case "rotate":
1372
+ current = release(current, rotate(current, op.angle, colorSpace), source);
1373
+ break;
1374
+ case "mirror":
1375
+ current = release(current, mirror(current, op.axis, colorSpace), source);
1376
+ break;
1377
+ }
1378
+ }
1379
+ return { canvas: current, applied: summarizeTransforms(transforms) };
1380
+ }
1381
+ function summarizeTransforms(transforms) {
1382
+ const applied = { rotation: 0, mirrored: "none", cropped: false };
1383
+ for (const op of transforms) {
1384
+ switch (op.kind) {
1385
+ case "crop":
1386
+ applied.cropped = true;
1387
+ break;
1388
+ case "rotate":
1389
+ applied.rotation = (applied.rotation + op.angle) % 360;
1390
+ break;
1391
+ case "mirror": {
1392
+ const direction = mirrorDirection(op.axis);
1393
+ if (applied.mirrored === "none") applied.mirrored = direction;
1394
+ else if (applied.mirrored === direction) applied.mirrored = "none";
1395
+ else {
1396
+ applied.mirrored = "none";
1397
+ applied.rotation = (applied.rotation + 180) % 360;
1398
+ }
1399
+ break;
1400
+ }
1401
+ }
1402
+ }
1403
+ return applied;
1404
+ }
1405
+ function mirrorDirection(axis) {
1406
+ return axis === 0 ? "vertical" : "horizontal";
1407
+ }
1408
+ function crop(source, op, colorSpace) {
1409
+ const target = createCanvas(op.width, op.height);
1410
+ const ctx = context(target, colorSpace);
1411
+ ctx.drawImage(
1412
+ source,
1413
+ op.offsetX,
1414
+ op.offsetY,
1415
+ op.width,
1416
+ op.height,
1417
+ 0,
1418
+ 0,
1419
+ op.width,
1420
+ op.height
1421
+ );
1422
+ return target;
1423
+ }
1424
+ function rotate(source, angle, colorSpace) {
1425
+ const swap = angle === 90 || angle === 270;
1426
+ const target = createCanvas(
1427
+ swap ? source.height : source.width,
1428
+ swap ? source.width : source.height
1429
+ );
1430
+ const ctx = context(target, colorSpace);
1431
+ ctx.translate(target.width / 2, target.height / 2);
1432
+ ctx.rotate(-angle * Math.PI / 180);
1433
+ ctx.drawImage(source, -source.width / 2, -source.height / 2);
1434
+ return target;
1435
+ }
1436
+ function mirror(source, axis, colorSpace) {
1437
+ const target = createCanvas(source.width, source.height);
1438
+ const ctx = context(target, colorSpace);
1439
+ if (mirrorDirection(axis) === "horizontal") {
1440
+ ctx.translate(source.width, 0);
1441
+ ctx.scale(-1, 1);
1442
+ } else {
1443
+ ctx.translate(0, source.height);
1444
+ ctx.scale(1, -1);
1445
+ }
1446
+ ctx.drawImage(source, 0, 0);
1447
+ return target;
1448
+ }
1449
+ function context(canvas, colorSpace) {
1450
+ const ctx = canvas.getContext("2d", { colorSpace, alpha: false });
1451
+ if (!ctx) throw new Error("Could not get a 2d context");
1452
+ return ctx;
1453
+ }
1454
+ function release(previous, next, original) {
1455
+ if (previous !== original) {
1456
+ previous.width = 0;
1457
+ previous.height = 0;
1458
+ }
1459
+ return next;
1460
+ }
1461
+
1462
+ // src/probe.ts
1463
+ async function probeSupport() {
1464
+ const [native, hevcCodecStrings] = await Promise.all([
1465
+ probeNativeSupport(),
1466
+ probeHevcCodecStrings()
1467
+ ]);
1468
+ const webcodecs = isWebCodecsAvailable() && hevcCodecStrings.length > 0;
1469
+ const recommended = native ? "native" : webcodecs ? "webcodecs" : "wasm";
1470
+ return { native, webcodecs, hevcCodecStrings, recommended };
1471
+ }
1472
+
1473
+ // src/index.ts
1474
+ async function isHeic(input) {
1475
+ const prefix = await readPrefix(input, DETECTION_PREFIX_BYTES);
1476
+ const detection = detectFromBuffer(prefix);
1477
+ const result = { isHeic: detection.isHeic };
1478
+ if (detection.brand !== void 0) result.brand = detection.brand;
1479
+ if (detection.primaryItemType !== void 0) result.primaryItemType = detection.primaryItemType;
1480
+ if (detection.coding !== void 0) result.coding = detection.coding;
1481
+ return result;
1482
+ }
1483
+ async function decodeHeic(input, options = {}) {
1484
+ const {
1485
+ strategy = "auto",
1486
+ colorSpace = "srgb",
1487
+ maxDimension,
1488
+ signal,
1489
+ wasmLoader
1490
+ } = options;
1491
+ throwIfAborted(signal);
1492
+ const bytes = await readAll(input);
1493
+ throwIfAborted(signal);
1494
+ const plan = planDecode(bytes);
1495
+ throwIfAborted(signal);
1496
+ const attempts = [];
1497
+ const wants = (candidate) => strategy === "auto" || strategy === candidate;
1498
+ if (wants("native")) {
1499
+ const blob = input instanceof Blob ? input : toHeicBlob(bytes);
1500
+ const outcome = await decodeNative(blob, plan, signal);
1501
+ if (outcome.status === "ok") {
1502
+ const finalBitmap = await resizeBitmap(outcome.bitmap, maxDimension, signal);
1503
+ return describe(plan, finalBitmap, "native", summarizeTransforms(plan.transforms));
1504
+ }
1505
+ attempts.push({ strategy: "native", reason: outcome.reason });
1506
+ }
1507
+ if (wants("webcodecs")) {
1508
+ if (!isWebCodecsAvailable()) {
1509
+ attempts.push({ strategy: "webcodecs", reason: "VideoDecoder is not available" });
1510
+ } else {
1511
+ try {
1512
+ const composited = await decodeWithWebCodecs(plan, colorSpace, signal);
1513
+ const { canvas, applied } = applyTransforms(composited, plan.transforms, colorSpace);
1514
+ const bitmap = await canvasToBitmap(canvas, maxDimension, signal);
1515
+ return describe(plan, bitmap, "webcodecs", applied);
1516
+ } catch (error) {
1517
+ if (error instanceof HeicAbortError) throw error;
1518
+ if (strategy === "webcodecs") throw error;
1519
+ attempts.push({ strategy: "webcodecs", reason: describeError(error) });
1520
+ }
1521
+ }
1522
+ }
1523
+ if (wants("wasm")) {
1524
+ const adapter = await resolveAdapter(wasmLoader);
1525
+ if (!adapter) {
1526
+ attempts.push({
1527
+ strategy: "wasm",
1528
+ reason: "no adapter: pass options.wasmLoader or call registerDecoderAdapter()"
1529
+ });
1530
+ } else {
1531
+ try {
1532
+ const result = await adapter.decode({ data: bytes, colorSpace, signal });
1533
+ const applied = summarizeTransforms(plan.transforms);
1534
+ let bitmap;
1535
+ if (result.image instanceof ImageBitmap) {
1536
+ const source = adapter.appliesTransforms ? result.image : await transformBitmap(result.image, plan, colorSpace);
1537
+ bitmap = await resizeBitmap(source, maxDimension, signal);
1538
+ } else {
1539
+ const canvas = adapter.appliesTransforms ? result.image : applyTransforms(result.image, plan.transforms, colorSpace).canvas;
1540
+ bitmap = await canvasToBitmap(canvas, maxDimension, signal);
1541
+ }
1542
+ return describe(plan, bitmap, "wasm", applied);
1543
+ } catch (error) {
1544
+ if (error instanceof HeicAbortError) throw error;
1545
+ if (strategy === "wasm") throw error;
1546
+ attempts.push({ strategy: "wasm", reason: describeError(error) });
1547
+ }
1548
+ }
1549
+ }
1550
+ throw new HeicUnsupportedError("Could not decode this HEIC", attempts, {
1551
+ brand: plan.file.majorBrand,
1552
+ itemType: plan.file.items.get(plan.primaryItemId)?.itemType,
1553
+ itemId: plan.primaryItemId
1554
+ });
1555
+ }
1556
+ var registeredAdapter;
1557
+ function registerDecoderAdapter(adapter) {
1558
+ registeredAdapter = adapter;
1559
+ }
1560
+ function getRegisteredAdapter() {
1561
+ return registeredAdapter;
1562
+ }
1563
+ async function resolveAdapter(loader) {
1564
+ if (registeredAdapter) return registeredAdapter;
1565
+ if (!loader) return void 0;
1566
+ return loader();
1567
+ }
1568
+ async function readAll(input) {
1569
+ if (input instanceof Uint8Array) return input;
1570
+ if (input instanceof ArrayBuffer) return new Uint8Array(input);
1571
+ return new Uint8Array(await input.arrayBuffer());
1572
+ }
1573
+ async function readPrefix(input, byteCount) {
1574
+ if (input instanceof Blob) {
1575
+ return new Uint8Array(await input.slice(0, byteCount).arrayBuffer());
1576
+ }
1577
+ const bytes = await readAll(input);
1578
+ return bytes.subarray(0, byteCount);
1579
+ }
1580
+ function scaleFor(width, height, maxDimension) {
1581
+ if (!maxDimension || maxDimension <= 0) return 1;
1582
+ const longest = Math.max(width, height);
1583
+ return longest <= maxDimension ? 1 : maxDimension / longest;
1584
+ }
1585
+ async function canvasToBitmap(canvas, maxDimension, signal) {
1586
+ throwIfAborted(signal);
1587
+ const scale = scaleFor(canvas.width, canvas.height, maxDimension);
1588
+ if (scale === 1) {
1589
+ return canvas.transferToImageBitmap();
1590
+ }
1591
+ const resizeWidth = Math.max(1, Math.round(canvas.width * scale));
1592
+ const resizeHeight = Math.max(1, Math.round(canvas.height * scale));
1593
+ try {
1594
+ return await createImageBitmap(canvas, {
1595
+ resizeWidth,
1596
+ resizeHeight,
1597
+ resizeQuality: "high"
1598
+ });
1599
+ } finally {
1600
+ canvas.width = 0;
1601
+ canvas.height = 0;
1602
+ }
1603
+ }
1604
+ async function resizeBitmap(bitmap, maxDimension, signal) {
1605
+ throwIfAborted(signal);
1606
+ const scale = scaleFor(bitmap.width, bitmap.height, maxDimension);
1607
+ if (scale === 1) return bitmap;
1608
+ const resized = await createImageBitmap(bitmap, {
1609
+ resizeWidth: Math.max(1, Math.round(bitmap.width * scale)),
1610
+ resizeHeight: Math.max(1, Math.round(bitmap.height * scale)),
1611
+ resizeQuality: "high"
1612
+ });
1613
+ bitmap.close();
1614
+ return resized;
1615
+ }
1616
+ async function transformBitmap(bitmap, plan, colorSpace) {
1617
+ if (plan.transforms.length === 0) return bitmap;
1618
+ const canvas = createCanvas(bitmap.width, bitmap.height);
1619
+ const ctx = canvas.getContext("2d", { colorSpace, alpha: false });
1620
+ if (!ctx) return bitmap;
1621
+ ctx.drawImage(bitmap, 0, 0);
1622
+ bitmap.close();
1623
+ const { canvas: transformed } = applyTransforms(canvas, plan.transforms, colorSpace);
1624
+ return transformed.transferToImageBitmap();
1625
+ }
1626
+ function describe(plan, image, strategy, transformsApplied) {
1627
+ return {
1628
+ image,
1629
+ width: image.width,
1630
+ height: image.height,
1631
+ sourceWidth: plan.displayWidth,
1632
+ sourceHeight: plan.displayHeight,
1633
+ strategy,
1634
+ bitDepth: plan.bitDepth,
1635
+ isGrid: plan.isGrid,
1636
+ tileCount: plan.tiles.length,
1637
+ sourceColor: plan.sourceColor,
1638
+ transformsApplied,
1639
+ warnings: plan.warnings
1640
+ };
1641
+ }
1642
+ function describeError(error) {
1643
+ return error instanceof Error ? error.message : String(error);
1644
+ }
1645
+
1646
+ export { decodeHeic, findProperty, getRegisteredAdapter, hvccToAnnexBPrologue, hvccToCodecString, isHeic, lengthPrefixedToAnnexB, parseGridPayload, parseHeif, parseHvcC, planDecode, probeSupport, propertiesForItem, readGrid, readItemData, registerDecoderAdapter };
1647
+ //# sourceMappingURL=index.js.map
1648
+ //# sourceMappingURL=index.js.map