@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.
package/source/lzw.js ADDED
@@ -0,0 +1,322 @@
1
+ export function decodeCompressedIndexStream(compressedData, minimumCodeSize, expectedPixelCount, {strict, colorTableEntryCount}) {
2
+ const clearCode = 1 << minimumCodeSize;
3
+ const endOfInformationCode = clearCode + 1;
4
+
5
+ const context = {
6
+ compressedData,
7
+ clearCode,
8
+ endOfInformationCode,
9
+ minimumCodeSize,
10
+ prefixCodes: new Int16Array(4096),
11
+ suffixBytes: new Uint8Array(4096),
12
+ outputStack: new Uint8Array(4097),
13
+ decodedPixels: new Uint8Array(expectedPixelCount),
14
+ codeSize: minimumCodeSize + 1,
15
+ nextAvailableCode: clearCode + 2,
16
+ bitBuffer: 0,
17
+ bitCount: 0,
18
+ dataOffset: 0,
19
+ previousCode: -1,
20
+ outputOffset: 0,
21
+ firstByteOfCurrentString: 0,
22
+ currentStackLength: 0,
23
+ currentFirstByte: 0,
24
+ colorTableEntryCount,
25
+ hasReadAnyCode: false,
26
+ };
27
+
28
+ resetLzwDictionary(context);
29
+
30
+ while (true) {
31
+ const currentCode = readNextLzwCode(context);
32
+ validateFirstLzwCode(context, currentCode, {strict});
33
+
34
+ if (currentCode === context.clearCode) {
35
+ resetLzwDictionary(context);
36
+ continue;
37
+ }
38
+
39
+ if (currentCode === context.endOfInformationCode) {
40
+ validateNoUnusedCompressedData(context, {strict});
41
+ break;
42
+ }
43
+
44
+ expandLzwCode(context, currentCode);
45
+ copyLzwStackToPixels(context, expectedPixelCount);
46
+ addLzwDictionaryEntry(context);
47
+ context.previousCode = currentCode;
48
+ }
49
+
50
+ if (context.outputOffset !== expectedPixelCount) {
51
+ throw new Error(`Expected ${expectedPixelCount} pixel indices, decoded ${context.outputOffset}`);
52
+ }
53
+
54
+ return context.decodedPixels;
55
+ }
56
+
57
+ function resetLzwDictionary(context) {
58
+ for (let index = 0; index < context.clearCode; index += 1) {
59
+ context.prefixCodes[index] = -1;
60
+ context.suffixBytes[index] = index;
61
+ }
62
+
63
+ context.codeSize = context.minimumCodeSize + 1;
64
+ context.nextAvailableCode = context.clearCode + 2;
65
+ context.previousCode = -1;
66
+ }
67
+
68
+ function readNextLzwCode(context) {
69
+ while (context.bitCount < context.codeSize) {
70
+ if (context.dataOffset >= context.compressedData.length) {
71
+ throw new Error('Unexpected end of compressed image data before End of Information code');
72
+ }
73
+
74
+ context.bitBuffer |= context.compressedData[context.dataOffset] << context.bitCount;
75
+ context.bitCount += 8;
76
+ context.dataOffset += 1;
77
+ }
78
+
79
+ const currentCode = context.bitBuffer & ((1 << context.codeSize) - 1);
80
+ context.bitBuffer >>= context.codeSize;
81
+ context.bitCount -= context.codeSize;
82
+ return currentCode;
83
+ }
84
+
85
+ function validateFirstLzwCode(context, currentCode, {strict}) {
86
+ if (context.hasReadAnyCode) {
87
+ return;
88
+ }
89
+
90
+ context.hasReadAnyCode = true;
91
+
92
+ if (strict && currentCode !== context.clearCode) {
93
+ throw new Error('LZW image data must start with a Clear code');
94
+ }
95
+ }
96
+
97
+ function validateNoUnusedCompressedData(context, {strict}) {
98
+ if (strict && context.dataOffset < context.compressedData.length) {
99
+ throw new Error('Found unused compressed image data after the End of Information code');
100
+ }
101
+ }
102
+
103
+ function expandLzwCode(context, currentCode) {
104
+ if (currentCode > context.nextAvailableCode) {
105
+ throw new Error(`Encountered invalid compressed code ${currentCode}`);
106
+ }
107
+
108
+ let stackLength = 0;
109
+ let codeToExpand = currentCode;
110
+ if (currentCode === context.nextAvailableCode) {
111
+ if (context.previousCode === -1) {
112
+ throw new Error('Encountered a dictionary-reference code before any previous code existed');
113
+ }
114
+
115
+ // GIF LZW's KwKwK case references the entry being created from the previous code plus its own first byte.
116
+ context.outputStack[stackLength] = context.firstByteOfCurrentString;
117
+ stackLength += 1;
118
+ codeToExpand = context.previousCode;
119
+ }
120
+
121
+ while (codeToExpand > context.clearCode + 1) {
122
+ context.outputStack[stackLength] = context.suffixBytes[codeToExpand];
123
+ stackLength += 1;
124
+ codeToExpand = context.prefixCodes[codeToExpand];
125
+
126
+ if (stackLength >= context.outputStack.length) {
127
+ throw new Error('Compressed code expansion overflowed the output stack');
128
+ }
129
+ }
130
+
131
+ const firstByte = context.suffixBytes[codeToExpand];
132
+ context.outputStack[stackLength] = firstByte;
133
+ stackLength += 1;
134
+ context.firstByteOfCurrentString = firstByte;
135
+ context.currentStackLength = stackLength;
136
+ context.currentFirstByte = firstByte;
137
+ }
138
+
139
+ function copyLzwStackToPixels(context, expectedPixelCount) {
140
+ for (let stackIndex = context.currentStackLength - 1; stackIndex >= 0; stackIndex -= 1) {
141
+ if (context.outputOffset >= expectedPixelCount) {
142
+ throw new Error('Decompressed more pixels than the image dimensions allow');
143
+ }
144
+
145
+ const pixelIndex = context.outputStack[stackIndex];
146
+
147
+ // Validate decoded palette indexes while copying LZW output so strict mode does not need a second full image pass.
148
+ if (context.colorTableEntryCount !== undefined && pixelIndex >= context.colorTableEntryCount) {
149
+ throw new Error(`Pixel ${context.outputOffset} uses palette index ${pixelIndex}, but the active color table only has ${context.colorTableEntryCount} entries`);
150
+ }
151
+
152
+ context.decodedPixels[context.outputOffset] = pixelIndex;
153
+ context.outputOffset += 1;
154
+ }
155
+ }
156
+
157
+ function addLzwDictionaryEntry(context) {
158
+ if (context.previousCode === -1 || context.nextAvailableCode >= 4096) {
159
+ return;
160
+ }
161
+
162
+ context.prefixCodes[context.nextAvailableCode] = context.previousCode;
163
+ context.suffixBytes[context.nextAvailableCode] = context.currentFirstByte;
164
+ context.nextAvailableCode += 1;
165
+ if (context.nextAvailableCode === (1 << context.codeSize) && context.codeSize < 12) {
166
+ context.codeSize += 1;
167
+ }
168
+ }
169
+
170
+ export function encodeCompressedIndexStream(indexedPixels, minimumCodeSize) {
171
+ const clearCode = 1 << minimumCodeSize;
172
+ const endOfInformationCode = clearCode + 1;
173
+ const codeSequence = [clearCode];
174
+
175
+ if (indexedPixels.length === 0) {
176
+ codeSequence.push(endOfInformationCode);
177
+ return packCompressedCodeSequence(codeSequence, minimumCodeSize);
178
+ }
179
+
180
+ // Small GIFs should not pay for the dense 2 MiB lookup table; larger images usually make up that cost by avoiding Map hashing in the LZW hot path.
181
+ if (indexedPixels.length < 4096) {
182
+ return encodeCompressedIndexStreamWithMap(indexedPixels, minimumCodeSize);
183
+ }
184
+
185
+ return encodeCompressedIndexStreamWithDenseLookup(indexedPixels, minimumCodeSize);
186
+ }
187
+
188
+ function encodeCompressedIndexStreamWithMap(indexedPixels, minimumCodeSize) {
189
+ const clearCode = 1 << minimumCodeSize;
190
+ const endOfInformationCode = clearCode + 1;
191
+ let nextAvailableCode = clearCode + 2;
192
+ const codeLookup = new Map();
193
+ const codeSequence = [clearCode];
194
+
195
+ function resetDictionary() {
196
+ codeLookup.clear();
197
+ nextAvailableCode = clearCode + 2;
198
+ }
199
+
200
+ let currentCode = indexedPixels[0];
201
+ for (let index = 1; index < indexedPixels.length; index += 1) {
202
+ const nextPixelIndex = indexedPixels[index];
203
+ const dictionaryKey = (currentCode << 8) | nextPixelIndex;
204
+
205
+ const combinedCode = codeLookup.get(dictionaryKey);
206
+ if (combinedCode !== undefined) {
207
+ currentCode = combinedCode;
208
+ continue;
209
+ }
210
+
211
+ codeSequence.push(currentCode);
212
+
213
+ if (nextAvailableCode < 4096) {
214
+ codeLookup.set(dictionaryKey, nextAvailableCode);
215
+ nextAvailableCode += 1;
216
+ } else {
217
+ codeSequence.push(clearCode);
218
+ resetDictionary();
219
+ }
220
+
221
+ currentCode = nextPixelIndex;
222
+ }
223
+
224
+ codeSequence.push(currentCode, endOfInformationCode);
225
+ return packCompressedCodeSequence(codeSequence, minimumCodeSize);
226
+ }
227
+
228
+ function encodeCompressedIndexStreamWithDenseLookup(indexedPixels, minimumCodeSize) {
229
+ const clearCode = 1 << minimumCodeSize;
230
+ const endOfInformationCode = clearCode + 1;
231
+ let nextAvailableCode = clearCode + 2;
232
+ const codeLookup = new Uint16Array(4096 * 256);
233
+ const touchedDictionaryKeys = new Uint32Array(4096);
234
+ let touchedDictionaryKeyCount = 0;
235
+ const codeSequence = [clearCode];
236
+
237
+ function resetDictionary() {
238
+ for (let index = 0; index < touchedDictionaryKeyCount; index += 1) {
239
+ codeLookup[touchedDictionaryKeys[index]] = 0;
240
+ }
241
+
242
+ touchedDictionaryKeyCount = 0;
243
+ nextAvailableCode = clearCode + 2;
244
+ }
245
+
246
+ let currentCode = indexedPixels[0];
247
+ for (let index = 1; index < indexedPixels.length; index += 1) {
248
+ const nextPixelIndex = indexedPixels[index];
249
+ const dictionaryKey = (currentCode << 8) | nextPixelIndex;
250
+ const combinedCode = codeLookup[dictionaryKey];
251
+ if (combinedCode !== 0) {
252
+ currentCode = combinedCode;
253
+ continue;
254
+ }
255
+
256
+ codeSequence.push(currentCode);
257
+
258
+ if (nextAvailableCode < 4096) {
259
+ codeLookup[dictionaryKey] = nextAvailableCode;
260
+ touchedDictionaryKeys[touchedDictionaryKeyCount] = dictionaryKey;
261
+ touchedDictionaryKeyCount += 1;
262
+ nextAvailableCode += 1;
263
+ } else {
264
+ codeSequence.push(clearCode);
265
+ resetDictionary();
266
+ }
267
+
268
+ currentCode = nextPixelIndex;
269
+ }
270
+
271
+ codeSequence.push(currentCode, endOfInformationCode);
272
+ return packCompressedCodeSequence(codeSequence, minimumCodeSize);
273
+ }
274
+
275
+ function packCompressedCodeSequence(codeSequence, minimumCodeSize) {
276
+ const clearCode = 1 << minimumCodeSize;
277
+ const endOfInformationCode = clearCode + 1;
278
+ let codeSize = minimumCodeSize + 1;
279
+ let nextAvailableCode = clearCode + 2;
280
+ let previousCode;
281
+ let bitBuffer = 0;
282
+ let bitCount = 0;
283
+ const packedBytes = [];
284
+
285
+ for (const code of codeSequence) {
286
+ bitBuffer |= code << bitCount;
287
+ bitCount += codeSize;
288
+
289
+ while (bitCount >= 8) {
290
+ packedBytes.push(bitBuffer & 0xFF);
291
+ bitBuffer >>= 8;
292
+ bitCount -= 8;
293
+ }
294
+
295
+ if (code === clearCode) {
296
+ codeSize = minimumCodeSize + 1;
297
+ nextAvailableCode = clearCode + 2;
298
+ previousCode = undefined;
299
+ continue;
300
+ }
301
+
302
+ if (code === endOfInformationCode) {
303
+ break;
304
+ }
305
+
306
+ if (previousCode !== undefined && nextAvailableCode < 4096) {
307
+ nextAvailableCode += 1;
308
+
309
+ if (nextAvailableCode === (1 << codeSize) && codeSize < 12) {
310
+ codeSize += 1;
311
+ }
312
+ }
313
+
314
+ previousCode = code;
315
+ }
316
+
317
+ if (bitCount > 0) {
318
+ packedBytes.push(bitBuffer & 0xFF);
319
+ }
320
+
321
+ return Uint8Array.from(packedBytes);
322
+ }
@@ -0,0 +1,185 @@
1
+ import {defaultMaximumIndexedImagePixelCount} from './constants.js';
2
+ import {
3
+ isUint8Array,
4
+ isUint8ClampedArray,
5
+ toUint8ArrayView,
6
+ requirePixelCountValueWithinLimit,
7
+ normalizeSingleColorTriplet,
8
+ padColorTableToPowerOfTwo,
9
+ } from './validate.js';
10
+
11
+ export function indexedImage(pixels, options = {}) {
12
+ const {
13
+ transparentColor = [0, 0, 0],
14
+ } = options;
15
+
16
+ if (!isUint8Array(pixels) && !isUint8ClampedArray(pixels)) {
17
+ throw new TypeError('Expected pixels to be a Uint8Array or Uint8ClampedArray');
18
+ }
19
+
20
+ const redGreenBlueAlphaBytes = toUint8ArrayView(pixels);
21
+ if (redGreenBlueAlphaBytes.length % 4 !== 0) {
22
+ throw new Error('pixels length must be divisible by 4');
23
+ }
24
+
25
+ requirePixelCountValueWithinLimit(redGreenBlueAlphaBytes.length / 4, defaultMaximumIndexedImagePixelCount, 'pixels');
26
+ const [transparentRed, transparentGreen, transparentBlue] = normalizeSingleColorTriplet(transparentColor, 'transparentColor');
27
+ const paletteLookup = new Map();
28
+ const palette = [];
29
+ const indexedPixels = new Uint8Array(redGreenBlueAlphaBytes.length / 4);
30
+ let transparentColorIndex;
31
+
32
+ for (let pixelOffset = 0, pixelIndex = 0; pixelOffset < redGreenBlueAlphaBytes.length; pixelOffset += 4, pixelIndex += 1) {
33
+ const red = redGreenBlueAlphaBytes[pixelOffset];
34
+ const green = redGreenBlueAlphaBytes[pixelOffset + 1];
35
+ const blue = redGreenBlueAlphaBytes[pixelOffset + 2];
36
+ const alpha = redGreenBlueAlphaBytes[pixelOffset + 3];
37
+
38
+ if (alpha !== 0 && alpha !== 255) {
39
+ throw new Error(`Pixel ${pixelIndex} has alpha ${alpha}; GIF only supports fully transparent or fully opaque pixels`);
40
+ }
41
+
42
+ // All fully transparent pixels share one palette entry, kept separate from any opaque pixel even when its RGB bytes match the transparent color.
43
+ if (alpha === 0) {
44
+ if (transparentColorIndex === undefined) {
45
+ transparentColorIndex = palette.length / 3;
46
+ palette.push(transparentRed, transparentGreen, transparentBlue);
47
+ }
48
+
49
+ indexedPixels[pixelIndex] = transparentColorIndex;
50
+ continue;
51
+ }
52
+
53
+ const lookupKey = `${red},${green},${blue}`;
54
+ let paletteIndex = paletteLookup.get(lookupKey);
55
+ if (paletteIndex === undefined) {
56
+ paletteIndex = palette.length / 3;
57
+
58
+ if (paletteIndex >= 256) {
59
+ throw new Error('The image uses more than 256 palette entries');
60
+ }
61
+
62
+ palette.push(red, green, blue);
63
+ paletteLookup.set(lookupKey, paletteIndex);
64
+ }
65
+
66
+ indexedPixels[pixelIndex] = paletteIndex;
67
+ }
68
+
69
+ const colorTable = padColorTableToPowerOfTwo(Uint8Array.from(palette));
70
+
71
+ return {
72
+ pixels: indexedPixels,
73
+ colorTable,
74
+ transparentColorIndex,
75
+ };
76
+ }
77
+
78
+ export function normalizeRGBAImageData(pixels, block, graphicControlExtension) {
79
+ const indexedImageData = indexedImage(pixels, {
80
+ transparentColor: block.transparentColor ?? [0, 0, 0],
81
+ });
82
+
83
+ const normalizedGraphicControlExtension = indexedImageData.transparentColorIndex !== undefined || graphicControlExtension !== undefined
84
+ ? {
85
+ disposalMethod: graphicControlExtension?.disposalMethod ?? 'unspecified',
86
+ delayInHundredthsOfASecond: graphicControlExtension?.delayInHundredthsOfASecond ?? 0,
87
+ transparentColorIndex: indexedImageData.transparentColorIndex,
88
+ }
89
+ : undefined;
90
+
91
+ return {
92
+ indexedPixels: indexedImageData.pixels,
93
+ colorTable: indexedImageData.colorTable,
94
+ graphicControlExtension: normalizedGraphicControlExtension,
95
+ };
96
+ }
97
+
98
+ export function buildQuantizedIndexedImage(pixels, {quality}) {
99
+ const redGreenBlueAlphaBytes = toUint8ArrayView(pixels);
100
+ requirePixelCountValueWithinLimit(redGreenBlueAlphaBytes.length / 4, defaultMaximumIndexedImagePixelCount, 'pixels');
101
+ const hasTransparency = hasTransparentPixels(redGreenBlueAlphaBytes);
102
+ const colorBits = quantizationBitsForQuality(quality, hasTransparency ? 255 : 256);
103
+ const paletteLookup = new Map();
104
+ const palette = [];
105
+ const indexedPixels = new Uint8Array(redGreenBlueAlphaBytes.length / 4);
106
+ let transparentColorIndex;
107
+
108
+ for (let pixelOffset = 0, pixelIndex = 0; pixelOffset < redGreenBlueAlphaBytes.length; pixelOffset += 4, pixelIndex += 1) {
109
+ const red = redGreenBlueAlphaBytes[pixelOffset];
110
+ const green = redGreenBlueAlphaBytes[pixelOffset + 1];
111
+ const blue = redGreenBlueAlphaBytes[pixelOffset + 2];
112
+ const alpha = redGreenBlueAlphaBytes[pixelOffset + 3];
113
+
114
+ if (alpha === 0) {
115
+ if (transparentColorIndex === undefined) {
116
+ transparentColorIndex = palette.length / 3;
117
+ palette.push(0, 0, 0);
118
+ }
119
+
120
+ indexedPixels[pixelIndex] = transparentColorIndex;
121
+ continue;
122
+ }
123
+
124
+ const quantizedRed = quantizeColorChannel(red, colorBits.red);
125
+ const quantizedGreen = quantizeColorChannel(green, colorBits.green);
126
+ const quantizedBlue = quantizeColorChannel(blue, colorBits.blue);
127
+ const lookupKey = `${quantizedRed},${quantizedGreen},${quantizedBlue}`;
128
+
129
+ let paletteIndex = paletteLookup.get(lookupKey);
130
+ if (paletteIndex === undefined) {
131
+ paletteIndex = palette.length / 3;
132
+ palette.push(quantizedRed, quantizedGreen, quantizedBlue);
133
+ paletteLookup.set(lookupKey, paletteIndex);
134
+ }
135
+
136
+ indexedPixels[pixelIndex] = paletteIndex;
137
+ }
138
+
139
+ return {
140
+ pixels: indexedPixels,
141
+ colorTable: padColorTableToPowerOfTwo(Uint8Array.from(palette)),
142
+ transparentColorIndex,
143
+ };
144
+ }
145
+
146
+ function hasTransparentPixels(redGreenBlueAlphaBytes) {
147
+ for (let offset = 3; offset < redGreenBlueAlphaBytes.length; offset += 4) {
148
+ if (redGreenBlueAlphaBytes[offset] === 0) {
149
+ return true;
150
+ }
151
+ }
152
+
153
+ return false;
154
+ }
155
+
156
+ function quantizationBitsForQuality(quality, maximumColorCount) {
157
+ const targetColorCount = Math.max(2, Math.min(maximumColorCount, Math.round(2 + (quality * (maximumColorCount - 2)))));
158
+ let bitCount = Math.floor(Math.log2(targetColorCount));
159
+ const bits = [0, 0, 0];
160
+ let channelIndex = 0;
161
+ while (bitCount > 0) {
162
+ bits[channelIndex] += 1;
163
+ channelIndex = (channelIndex + 1) % bits.length;
164
+ bitCount -= 1;
165
+ }
166
+
167
+ return {
168
+ red: bits[0],
169
+ green: bits[1],
170
+ blue: bits[2],
171
+ };
172
+ }
173
+
174
+ function quantizeColorChannel(value, bitCount) {
175
+ if (bitCount >= 8) {
176
+ return value;
177
+ }
178
+
179
+ if (bitCount === 0) {
180
+ return 0;
181
+ }
182
+
183
+ const maximumValue = (1 << bitCount) - 1;
184
+ return Math.round(Math.round(value * maximumValue / 255) * 255 / maximumValue);
185
+ }