@cjser/sindresorhus__gifkit 0.1.0-cjser.2

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.
@@ -0,0 +1,536 @@
1
+ import {
2
+ trailerByte,
3
+ extensionIntroducer,
4
+ imageSeparator,
5
+ graphicControlExtensionLabel,
6
+ commentExtensionLabel,
7
+ appExtensionLabel,
8
+ netscapeLoopingAppIdentifier,
9
+ netscapeLoopingAppAuthenticationCodeBytes,
10
+ defaultMaximumBlockCount,
11
+ defaultMaximumPixelCount,
12
+ defaultMaximumIndexedImagePixelCount,
13
+ defaultMaximumEncodeWorkCost,
14
+ defaultMaximumDataPayloadByteLength,
15
+ uint8ArraySubarray,
16
+ } from './constants.js';
17
+ import {
18
+ isUint8Array,
19
+ toUint8ArrayView,
20
+ requireBlockObject,
21
+ requireByte,
22
+ requireUnsigned16,
23
+ requireNonZeroUnsigned16,
24
+ requirePixelCountWithinLimit,
25
+ requirePixelCountValueWithinLimit,
26
+ requireCountWithinLimit,
27
+ requireByteLengthWithinLimit,
28
+ normalizeColorTable,
29
+ normalizeIndexedPixels,
30
+ normalizeRedGreenBlueAlphaPixels,
31
+ normalizeGraphicControlExtension,
32
+ normalizeExtensionPayload,
33
+ normalizeFixedAsciiField,
34
+ normalizeFixedByteField,
35
+ normalizeImageGeometry,
36
+ areBytesEqual,
37
+ calculateColorTableSizeField,
38
+ deriveColorResolution,
39
+ deriveMinimumCodeSize,
40
+ disposalMethodByte,
41
+ } from './validate.js';
42
+ import {GIFByteWriter} from './byte-stream.js';
43
+ import {encodeCompressedIndexStream} from './lzw.js';
44
+ import {
45
+ rejectRenamedLoopCount,
46
+ encodeNetscapeLoopCount,
47
+ encodeExplicitNetscapeLoopCount,
48
+ containsNetscapeLoopingAppExtension,
49
+ createNetscapeLoopingAppExtension,
50
+ } from './loop-count.js';
51
+ import {normalizeRGBAImageData} from './quantize.js';
52
+
53
+ export function encodeGIF(gif) {
54
+ const normalizedGif = normalizeGIFForEncoding(gif);
55
+ const byteWriter = new GIFByteWriter();
56
+
57
+ byteWriter.writeAsciiString('GIF');
58
+ byteWriter.writeAsciiString(normalizedGif.version);
59
+ writeGIFHeader(byteWriter, normalizedGif);
60
+ writeGIFBlocks(byteWriter, normalizedGif);
61
+ byteWriter.writeByte(trailerByte);
62
+ return byteWriter.toUint8Array();
63
+ }
64
+
65
+ function normalizeGIFForEncoding(gif) {
66
+ if (!gif || typeof gif !== 'object') {
67
+ throw new TypeError('Expected a GIF description object');
68
+ }
69
+
70
+ const logicalScreenWidth = requireNonZeroUnsigned16(gif.width, 'width');
71
+ const logicalScreenHeight = requireNonZeroUnsigned16(gif.height, 'height');
72
+ const globalColorTable = normalizeColorTable(gif.globalColorTable, 'globalColorTable');
73
+ const backgroundColorIndex = requireByte(gif.backgroundColorIndex ?? 0, 'backgroundColorIndex');
74
+ rejectRenamedLoopCount(gif);
75
+ if (globalColorTable === undefined && backgroundColorIndex !== 0) {
76
+ throw new Error('Background color index must be zero when there is no global color table');
77
+ }
78
+
79
+ if (
80
+ globalColorTable !== undefined
81
+ && backgroundColorIndex >= globalColorTable.length / 3
82
+ ) {
83
+ throw new Error('Background color index must be inside the global color table');
84
+ }
85
+
86
+ const loopCount = gif.playCount === undefined || gif.playCount === 1
87
+ ? undefined
88
+ : encodeNetscapeLoopCount(gif.playCount, 'playCount');
89
+ const blocks = normalizeBlocksForEncoding(gif, globalColorTable, {
90
+ loopCount,
91
+ });
92
+
93
+ return {
94
+ version: '89a',
95
+ logicalScreenWidth,
96
+ logicalScreenHeight,
97
+ backgroundColorIndex,
98
+ globalColorTable,
99
+ blocks,
100
+ };
101
+ }
102
+
103
+ function normalizeBlocksForEncoding(gif, globalColorTable, {loopCount}) {
104
+ if (gif.imageBlocks !== undefined) {
105
+ throw new Error('imageBlocks is not supported when encoding; use blocks');
106
+ }
107
+
108
+ let sourceBlocks;
109
+
110
+ if (gif.blocks === undefined) {
111
+ sourceBlocks = [];
112
+ } else if (Array.isArray(gif.blocks)) {
113
+ sourceBlocks = gif.blocks;
114
+ } else {
115
+ throw new TypeError('blocks must be an array');
116
+ }
117
+
118
+ const sourceBlockCount = sourceBlocks.length;
119
+ requireCountWithinLimit(sourceBlockCount, defaultMaximumBlockCount, 'block count');
120
+ validateEncodeWorkCost(sourceBlocks, sourceBlockCount);
121
+ const blocks = [];
122
+ let index = 0;
123
+ while (index < sourceBlockCount) {
124
+ blocks.push(normalizeBlock(sourceBlocks[index], globalColorTable));
125
+ index += 1;
126
+ }
127
+
128
+ if (loopCount === undefined || containsNetscapeLoopingAppExtension(blocks)) {
129
+ requireCountWithinLimit(countEncodedStreamBlocks(blocks), defaultMaximumBlockCount, 'block count');
130
+ return blocks;
131
+ }
132
+
133
+ const blocksWithLoopExtension = [
134
+ createNetscapeLoopingAppExtension(loopCount),
135
+ ...blocks,
136
+ ];
137
+ requireCountWithinLimit(countEncodedStreamBlocks(blocksWithLoopExtension), defaultMaximumBlockCount, 'block count');
138
+ return blocksWithLoopExtension;
139
+ }
140
+
141
+ function countEncodedStreamBlocks(blocks) {
142
+ let blockCount = 0;
143
+ for (const block of blocks) {
144
+ blockCount += 1;
145
+
146
+ if (
147
+ block.type === 'image'
148
+ && (
149
+ block.graphicControlExtension !== undefined
150
+ || (
151
+ block.indexedPixels === undefined
152
+ && block.pixels !== undefined
153
+ )
154
+ )
155
+ ) {
156
+ blockCount += 1;
157
+ }
158
+ }
159
+
160
+ return blockCount;
161
+ }
162
+
163
+ function validateEncodeWorkCost(blocks, blockCount = blocks.length) {
164
+ let workCost = 0;
165
+ let index = 0;
166
+ while (index < blockCount) {
167
+ const block = blocks[index];
168
+ if (!block || typeof block !== 'object') {
169
+ index += 1;
170
+ continue;
171
+ }
172
+
173
+ switch (block.type) {
174
+ case 'commentExtension': {
175
+ workCost += getPayloadInputLength(block.data ?? '');
176
+ break;
177
+ }
178
+
179
+ case 'applicationExtension': {
180
+ workCost += getPayloadInputLength(block.data ?? new Uint8Array());
181
+ break;
182
+ }
183
+
184
+ case 'image':
185
+ case 'rgbaImage': {
186
+ const {width, height} = block;
187
+ if (Number.isSafeInteger(width) && Number.isSafeInteger(height) && width > 0 && height > 0) {
188
+ workCost += width * height;
189
+ }
190
+
191
+ break;
192
+ }
193
+
194
+ default: {
195
+ break;
196
+ }
197
+ }
198
+
199
+ requireCountWithinLimit(workCost, defaultMaximumEncodeWorkCost, 'encode work cost');
200
+ index += 1;
201
+ }
202
+
203
+ return workCost;
204
+ }
205
+
206
+ function getPayloadInputLength(value) {
207
+ if (isUint8Array(value)) {
208
+ return toUint8ArrayView(value).length;
209
+ }
210
+
211
+ if (typeof value === 'string' || Array.isArray(value)) {
212
+ return value.length;
213
+ }
214
+
215
+ return 0;
216
+ }
217
+
218
+ function writeGIFHeader(byteWriter, gif) {
219
+ const {
220
+ logicalScreenWidth,
221
+ logicalScreenHeight,
222
+ backgroundColorIndex,
223
+ globalColorTable,
224
+ } = gif;
225
+ byteWriter.writeUnsignedLittleEndian16(logicalScreenWidth);
226
+ byteWriter.writeUnsignedLittleEndian16(logicalScreenHeight);
227
+
228
+ const globalColorTableSizeField = globalColorTable === undefined ? 0 : calculateColorTableSizeField(globalColorTable.length / 3);
229
+ const packedLogicalScreenField
230
+ = (globalColorTable === undefined ? 0 : 0b1000_0000)
231
+ | ((deriveColorResolution(globalColorTable) - 1) << 4)
232
+ | globalColorTableSizeField;
233
+
234
+ byteWriter.writeByte(packedLogicalScreenField);
235
+ byteWriter.writeByte(backgroundColorIndex);
236
+ byteWriter.writeByte(0);
237
+
238
+ if (globalColorTable !== undefined) {
239
+ writeColorTable(byteWriter, globalColorTable);
240
+ }
241
+ }
242
+
243
+ function writeGIFBlocks(byteWriter, gif) {
244
+ const {
245
+ logicalScreenWidth,
246
+ logicalScreenHeight,
247
+ globalColorTable,
248
+ } = gif;
249
+
250
+ for (const block of gif.blocks) {
251
+ switch (block.type) {
252
+ case 'commentExtension': {
253
+ writeCommentExtension(byteWriter, block);
254
+ break;
255
+ }
256
+
257
+ case 'applicationExtension': {
258
+ writeAppExtension(byteWriter, block);
259
+ break;
260
+ }
261
+
262
+ case 'image': {
263
+ writeImageBlock(byteWriter, block, {
264
+ logicalScreenWidth,
265
+ logicalScreenHeight,
266
+ globalColorTable,
267
+ });
268
+ break;
269
+ }
270
+
271
+ default: {
272
+ throw new Error(`Unsupported block type ${JSON.stringify(block.type)}`);
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ function writeImageBlock(byteWriter, block, {logicalScreenWidth, logicalScreenHeight, globalColorTable}) {
279
+ const geometry = normalizeImageGeometry(block, {
280
+ logicalScreenWidth,
281
+ logicalScreenHeight,
282
+ });
283
+ const imageData = normalizeImageDataForEncoding(block, geometry, globalColorTable);
284
+
285
+ if (imageData.graphicControlExtension !== undefined) {
286
+ writeGraphicControlExtension(byteWriter, imageData.graphicControlExtension);
287
+ }
288
+
289
+ writeEncodedImageBlock(byteWriter, block, {
290
+ ...geometry,
291
+ ...imageData,
292
+ });
293
+ }
294
+
295
+ function normalizeImageDataForEncoding(block, {width, height}, globalColorTable) {
296
+ let colorTable = normalizeColorTable(block.colorTable, 'image.colorTable');
297
+ let indexedPixels = normalizeIndexedPixels(block.indexedPixels, width * height, 'image.pixels');
298
+ let graphicControlExtension = normalizeGraphicControlExtension(block.graphicControlExtension);
299
+
300
+ if (indexedPixels === undefined && block.pixels !== undefined) {
301
+ requirePixelCountValueWithinLimit(width * height, defaultMaximumIndexedImagePixelCount, 'RGBA image data');
302
+ const pixels = normalizeRedGreenBlueAlphaPixels(block.pixels, width * height * 4, 'image.pixels');
303
+ const indexedImageData = normalizeRGBAImageData(pixels, block, graphicControlExtension);
304
+ indexedPixels = indexedImageData.indexedPixels;
305
+ colorTable = indexedImageData.colorTable;
306
+ graphicControlExtension = indexedImageData.graphicControlExtension;
307
+ }
308
+
309
+ if (indexedPixels === undefined) {
310
+ throw new Error('An image block requires pixels');
311
+ }
312
+
313
+ const activeColorTable = colorTable ?? globalColorTable;
314
+ if (activeColorTable === undefined) {
315
+ throw new Error('An image block requires a colorTable or a globalColorTable');
316
+ }
317
+
318
+ validateIndexedPixelsAgainstColorTable(indexedPixels, activeColorTable, graphicControlExtension?.transparentColorIndex);
319
+
320
+ let minimumCodeSize = deriveMinimumCodeSize(activeColorTable.length / 3);
321
+ if (minimumCodeSize < 2) {
322
+ minimumCodeSize = 2;
323
+ }
324
+
325
+ return {
326
+ colorTable,
327
+ indexedPixels,
328
+ minimumCodeSize,
329
+ graphicControlExtension,
330
+ };
331
+ }
332
+
333
+ function writeEncodedImageBlock(byteWriter, block, {left, top, width, height, colorTable, indexedPixels, minimumCodeSize}) {
334
+ const pixelsToEncode = block.isInterlaced ? interlaceIndexedPixels(indexedPixels, width, height) : indexedPixels;
335
+ const compressedData = encodeCompressedIndexStream(pixelsToEncode, minimumCodeSize);
336
+
337
+ byteWriter.writeByte(imageSeparator);
338
+ byteWriter.writeUnsignedLittleEndian16(left);
339
+ byteWriter.writeUnsignedLittleEndian16(top);
340
+ byteWriter.writeUnsignedLittleEndian16(width);
341
+ byteWriter.writeUnsignedLittleEndian16(height);
342
+
343
+ const packedImageField
344
+ = (colorTable === undefined ? 0 : 0b1000_0000)
345
+ | (block.isInterlaced ? 0b0100_0000 : 0)
346
+ | (colorTable === undefined ? 0 : calculateColorTableSizeField(colorTable.length / 3));
347
+
348
+ byteWriter.writeByte(packedImageField);
349
+ if (colorTable !== undefined) {
350
+ writeColorTable(byteWriter, colorTable);
351
+ }
352
+
353
+ byteWriter.writeByte(minimumCodeSize);
354
+ writeDataSubBlocks(byteWriter, compressedData);
355
+ }
356
+
357
+ function writeGraphicControlExtension(byteWriter, graphicControlExtension) {
358
+ if (graphicControlExtension === undefined) {
359
+ return;
360
+ }
361
+
362
+ const packedField
363
+ = (disposalMethodByte(graphicControlExtension.disposalMethod) << 2)
364
+ | (graphicControlExtension.transparentColorIndex === undefined ? 0 : 0b1);
365
+
366
+ byteWriter.writeByte(extensionIntroducer);
367
+ byteWriter.writeByte(graphicControlExtensionLabel);
368
+ byteWriter.writeByte(4);
369
+ byteWriter.writeByte(packedField);
370
+ byteWriter.writeUnsignedLittleEndian16(graphicControlExtension.delayInHundredthsOfASecond);
371
+ byteWriter.writeByte(graphicControlExtension.transparentColorIndex ?? 0);
372
+ byteWriter.writeByte(0x00);
373
+ }
374
+
375
+ function writeCommentExtension(byteWriter, block) {
376
+ const data = normalizeExtensionPayload(block.data, 'commentExtension.data');
377
+ byteWriter.writeByte(extensionIntroducer);
378
+ byteWriter.writeByte(commentExtensionLabel);
379
+ writeDataSubBlocks(byteWriter, data);
380
+ }
381
+
382
+ function writeAppExtension(byteWriter, block) {
383
+ const identifier = normalizeFixedAsciiField(block.identifier, 8, 'applicationExtension.identifier');
384
+ const authenticationCode = normalizeFixedByteField(block.authenticationCode, 3, 'applicationExtension.authenticationCode');
385
+ const data = normalizeExtensionPayload(block.data, 'applicationExtension.data');
386
+
387
+ byteWriter.writeByte(extensionIntroducer);
388
+ byteWriter.writeByte(appExtensionLabel);
389
+ byteWriter.writeByte(11);
390
+ byteWriter.writeAsciiString(identifier);
391
+ byteWriter.writeBytes(authenticationCode);
392
+ writeDataSubBlocks(byteWriter, data);
393
+ }
394
+
395
+ function writeDataSubBlocks(byteWriter, data) {
396
+ requireByteLengthWithinLimit(data.length, defaultMaximumDataPayloadByteLength, 'data payload');
397
+
398
+ for (let offset = 0; offset < data.length; offset += 255) {
399
+ const chunkLength = Math.min(255, data.length - offset);
400
+ byteWriter.writeByte(chunkLength);
401
+ byteWriter.writeBytes(uint8ArraySubarray.call(data, offset, offset + chunkLength));
402
+ }
403
+
404
+ byteWriter.writeByte(0x00);
405
+ }
406
+
407
+ function writeColorTable(byteWriter, colorTable) {
408
+ const normalizedColorTable = normalizeColorTable(colorTable, 'colorTable');
409
+ if (normalizedColorTable === undefined) {
410
+ throw new Error('Expected a color table');
411
+ }
412
+
413
+ byteWriter.writeBytes(normalizedColorTable);
414
+ }
415
+
416
+ function interlaceIndexedPixels(indexedPixels, width, height) {
417
+ const interlacedPixels = new Uint8Array(indexedPixels.length);
418
+ let destinationOffset = 0;
419
+ for (const [startRow, rowStep] of [[0, 8], [4, 8], [2, 4], [1, 2]]) {
420
+ for (let row = startRow; row < height; row += rowStep) {
421
+ const sourceOffset = row * width;
422
+ interlacedPixels.set(uint8ArraySubarray.call(indexedPixels, sourceOffset, sourceOffset + width), destinationOffset);
423
+ destinationOffset += width;
424
+ }
425
+ }
426
+
427
+ return interlacedPixels;
428
+ }
429
+
430
+ function validateIndexedPixelsAgainstColorTable(indexedPixels, colorTable, transparentColorIndex) {
431
+ const colorTableEntryCount = colorTable.length / 3;
432
+ let pixelOffset = 0;
433
+ while (pixelOffset < indexedPixels.length) {
434
+ const pixelIndex = indexedPixels[pixelOffset];
435
+ if (pixelIndex >= colorTableEntryCount) {
436
+ throw new Error(`Pixel ${pixelOffset} uses palette index ${pixelIndex}, but the active color table only has ${colorTableEntryCount} entries`);
437
+ }
438
+
439
+ pixelOffset += 1;
440
+ }
441
+
442
+ if (transparentColorIndex !== undefined && transparentColorIndex >= colorTableEntryCount) {
443
+ throw new Error(`Transparent color index ${transparentColorIndex} exceeds the active color table`);
444
+ }
445
+ }
446
+
447
+ function normalizeBlock(block, globalColorTable) {
448
+ requireBlockObject(block);
449
+
450
+ switch (block.type) {
451
+ case 'commentExtension': {
452
+ return normalizeCommentExtensionBlock(block);
453
+ }
454
+
455
+ case 'applicationExtension': {
456
+ return normalizeAppExtensionBlock(block);
457
+ }
458
+
459
+ case 'image':
460
+ case 'rgbaImage': {
461
+ return normalizeImageBlock(block, globalColorTable);
462
+ }
463
+
464
+ default: {
465
+ throw new Error(`Unsupported block type ${JSON.stringify(block.type)}`);
466
+ }
467
+ }
468
+ }
469
+
470
+ function normalizeCommentExtensionBlock(block) {
471
+ return {
472
+ type: 'commentExtension',
473
+ data: normalizeExtensionPayload(block.data ?? '', 'commentExtension.data'),
474
+ };
475
+ }
476
+
477
+ function normalizeAppExtensionBlock(block) {
478
+ const authenticationCode = normalizeFixedByteField(block.authenticationCode, 3, 'applicationExtension.authenticationCode');
479
+ const identifier = normalizeFixedAsciiField(block.identifier, 8, 'applicationExtension.identifier');
480
+ rejectRenamedLoopCount(block);
481
+
482
+ const loopCount = block.playCount === undefined
483
+ ? undefined
484
+ : encodeExplicitNetscapeLoopCount(block.playCount, 'applicationExtension.playCount');
485
+
486
+ const data = block.data ?? (
487
+ identifier === netscapeLoopingAppIdentifier
488
+ && areBytesEqual(authenticationCode, netscapeLoopingAppAuthenticationCodeBytes)
489
+ && loopCount !== undefined
490
+ ? Uint8Array.of(0x01, loopCount & 0xFF, loopCount >> 8)
491
+ : new Uint8Array()
492
+ );
493
+
494
+ return {
495
+ type: 'applicationExtension',
496
+ identifier,
497
+ authenticationCode,
498
+ data: normalizeExtensionPayload(data, 'applicationExtension.data'),
499
+ };
500
+ }
501
+
502
+ function normalizeImageBlock(block, globalColorTable) {
503
+ const isRGBAImage = block.type === 'rgbaImage';
504
+ const width = requireNonZeroUnsigned16(block.width, 'image.width');
505
+ const height = requireNonZeroUnsigned16(block.height, 'image.height');
506
+
507
+ requirePixelCountWithinLimit(width, height, defaultMaximumPixelCount, 'image block');
508
+
509
+ const colorTable = isRGBAImage ? undefined : normalizeColorTable(block.colorTable, 'image.colorTable');
510
+
511
+ const normalizedImageBlock = {
512
+ type: 'image',
513
+ graphicControlExtension: block.graphicControlExtension,
514
+ left: requireUnsigned16(block.left ?? 0, 'image.left'),
515
+ top: requireUnsigned16(block.top ?? 0, 'image.top'),
516
+ width,
517
+ height,
518
+ isInterlaced: Boolean(block.isInterlaced),
519
+ colorTable,
520
+ indexedPixels: isRGBAImage ? undefined : block.pixels,
521
+ };
522
+
523
+ if (!isRGBAImage && normalizedImageBlock.colorTable === undefined && globalColorTable === undefined) {
524
+ throw new Error('An image block requires a color table or a global color table');
525
+ }
526
+
527
+ if (isRGBAImage) {
528
+ normalizedImageBlock.pixels = block.pixels;
529
+
530
+ if (block.transparentColor !== undefined) {
531
+ normalizedImageBlock.transparentColor = block.transparentColor;
532
+ }
533
+ }
534
+
535
+ return normalizedImageBlock;
536
+ }
@@ -0,0 +1,72 @@
1
+ import {netscapeLoopingAppIdentifier, netscapeLoopingAppAuthenticationCode, netscapeLoopingAppAuthenticationCodeBytes} from './constants.js';
2
+ import {requireIntegerInRange, areBytesEqual, normalizeFixedByteField} from './validate.js';
3
+
4
+ export function renderTotalPlayCount(playCount) {
5
+ return playCount === 'forever'
6
+ ? Infinity
7
+ : playCount ?? 1;
8
+ }
9
+
10
+ export function normalizePlayCount(value, name) {
11
+ // Use a string sentinel because `Infinity` serializes to `null` in JSON.
12
+ if (value === 'forever') {
13
+ return value;
14
+ }
15
+
16
+ return requireIntegerInRange(value, 1, 0x1_00_00, name);
17
+ }
18
+
19
+ export function encodeNetscapeLoopCount(playCount, name) {
20
+ if (playCount === 'forever') {
21
+ return 0;
22
+ }
23
+
24
+ return requireIntegerInRange(playCount, 1, 0x1_00_00, name) - 1;
25
+ }
26
+
27
+ export function encodeExplicitNetscapeLoopCount(playCount, name) {
28
+ if (playCount === 'forever') {
29
+ return 0;
30
+ }
31
+
32
+ return requireIntegerInRange(playCount, 2, 0x1_00_00, name) - 1;
33
+ }
34
+
35
+ export function decodeNetscapeLoopCount(loopCount) {
36
+ return loopCount === 0
37
+ ? 'forever'
38
+ : loopCount + 1;
39
+ }
40
+
41
+ export function rejectRenamedLoopCount(object) {
42
+ if (object.loopCount !== undefined) {
43
+ throw new TypeError('loopCount has been renamed to playCount');
44
+ }
45
+ }
46
+
47
+ export function containsNetscapeLoopingAppExtension(blocks) {
48
+ for (const block of blocks) {
49
+ if (isNetscapeLoopingAppExtension(block)) {
50
+ return true;
51
+ }
52
+ }
53
+
54
+ return false;
55
+ }
56
+
57
+ export function createNetscapeLoopingAppExtension(loopCount) {
58
+ return {
59
+ type: 'applicationExtension',
60
+ identifier: netscapeLoopingAppIdentifier,
61
+ authenticationCode: netscapeLoopingAppAuthenticationCode,
62
+ data: Uint8Array.of(0x01, loopCount & 0xFF, (loopCount >> 8) & 0xFF),
63
+ };
64
+ }
65
+
66
+ export function isNetscapeLoopingAppExtension(block) {
67
+ return block.type === 'applicationExtension'
68
+ && block.identifier === netscapeLoopingAppIdentifier
69
+ && block.data.length >= 3
70
+ && block.data[0] === 0x01
71
+ && areBytesEqual(normalizeFixedByteField(block.authenticationCode, 3, 'applicationExtension.authenticationCode'), netscapeLoopingAppAuthenticationCodeBytes);
72
+ }