Code for a in-browser gif editor project
1/**
2 *
3 * This file holds the components needed to compress and decompress gif image data.
4 * Main components are the Compressor and Decompressors classes who offer the methods to compress or decompress data according to the LZW algorithm that gifs use.
5 *
6 */
7
8
9/**
10 * Special case of TrieNodes that do not have children. Making for a reversed Trie like structure where one only knows the parents.
11 * Used by the {@link Decompressor}
12 */
13class ChildlessTrieNode {
14 public nodeValue: number;
15 public parent: ChildlessTrieNode | undefined;
16
17 constructor(code: number) {
18 this.nodeValue = code;
19 }
20
21 /**
22 * Returns the sequence of codevalues from thge path of the root to this node
23 * @param includeRoot indicates if the root's node value should be included in the sequence. Default is false
24 */
25 resolveTrieToSequence(includeRoot = false): Array<number> {
26 if (this.parent == undefined) {
27 if (includeRoot) {
28 return [this.nodeValue];
29 }
30 return [];
31 }
32
33 const sequence = [this.nodeValue];
34 let node = this.parent;
35 while (node.parent) {
36 sequence.push(node.nodeValue);
37 node = node.parent;
38 }
39
40 if (includeRoot) {
41 sequence.push(node.nodeValue);
42 };
43
44 sequence.reverse();
45 return sequence;
46 }
47
48}
49
50
51/**
52 * Class for a simple Trie data structure. Each node can have a parent and as many children as needed.
53 * Used by the {@link Compressor}
54 */
55class TrieNode {
56 public codeValue: number;
57 public parent?: TrieNode;
58 public children: TrieNode[];
59
60 constructor(code: number) {
61 this.codeValue = code;
62 this.children = new Array();
63 }
64
65 addChild(child: TrieNode, pos: number) {
66 this.children[pos] = child;
67 }
68
69 /**
70 * Method to recursively eliminate all parent links. Maybe not needed in JS, but here for memory reasons back from ther C++ port.
71 */
72 orphanator() {
73 this.parent = undefined;
74 this.children.forEach(element => { element?.orphanator(); });
75 }
76
77}
78
79
80/**
81 * Decompressor for GIF images.
82 * Offers methods to either decompress table based image data directly, or general gif lzw compressed data
83 */
84class Decompressor {
85 private dicitionaryBase: Array<ChildlessTrieNode>; // basic array, holding only TrieNodes for our base cases
86 private reverseCodeMap: Map<number, ChildlessTrieNode>; //resolves codevalues to nodes in the subtrees
87 private unpacker: UnpackingBench; // gives access to the formerly packed codes
88 private MAXCODESIZE = 12; // for gifs this should be 12
89 private nextCodenumber = 0; //next codenumber to be put into the codemap
90 private codesize = 0; // how many bits do we currently use per code?
91 private resetCode = 0; // special code for "ups we need to reset the dictionary because we run out of space". Is set to amount of colors +1
92 private endOfDataCode = 0; // special code for end of data. Is set to amount of colors +2
93 private minimumlzwCodeLenght = 0; // smallest amount of bits needed to read a code, obtained from the tbid
94
95 constructor() {
96 this.dicitionaryBase = new Array();
97 this.unpacker = new UnpackingBench();
98 this.reverseCodeMap = new Map();
99 }
100
101 /**
102 * Decompress method that takes care of reading the min code lenght and pulling all that delicious data out of its wrapper
103 */
104 decompressTableBasedImageData(tbid: TableBasedImageData) {
105 return this.decompress(tbid.getImageData(), tbid.LZWMinimumCodeSize());
106 }
107
108 /**
109 * sets the amount of bits needed to represent the amount of base symbols (i.e. at least the amount of colors) before decompressing a datastream
110 */
111 private setMinimumlzwCodeLenght(minimumlzwCodeLenght: number) {
112 this.minimumlzwCodeLenght = minimumlzwCodeLenght;
113 }
114
115 /**
116 * Main method to decompress data that was compressed with the gif lzw variant.
117 * For Table Based Image Data use the {@link decompressTableBasedImageData} method
118 * @param compressedData compressed data that should be decompressed
119 * @param minCodeLenght The amount of bits needed to represent the number of base symbols used. (i.e. the amount of colors in a gif image)
120 * @returns Array of decompressed symbols
121 */
122 decompress(compressedData: Array<number>, minCodeLenght: number): Array<number> {
123 this.setMinimumlzwCodeLenght(minCodeLenght);
124 this.prepareBaseValues();
125 this.resetcodemap();
126 this.unpacker.loadPackeData(compressedData);
127
128 const indexStream = []; // will later contain the output
129
130 let prevCode = this.unpacker.getNextCode();
131
132 while (prevCode == this.resetCode && !this.unpacker.nextCodeEmpty) {
133 prevCode = this.unpacker.getNextCode();
134 }
135
136 if (prevCode == this.endOfDataCode) {
137 console.log("empty data")
138 return [];
139 }
140
141 indexStream.push(prevCode); // first "normal" code is always just a direct symbol
142
143 while (!this.unpacker.nextCodeEmpty) {
144 let nextCode = this.unpacker.getNextCode();
145 if (nextCode == this.endOfDataCode) {
146 break;
147 }
148
149 if (nextCode == this.resetCode) {
150 this.resetcodemap();
151 prevCode = this.unpacker.getNextCode();
152 while (prevCode == this.resetCode && !this.unpacker.nextCodeEmpty) {
153 prevCode = this.unpacker.getNextCode();
154 }
155
156 if (nextCode == this.endOfDataCode) {
157 console.log("EOD code encountered directly after a reset " + this.unpacker.packedData.length); // good for debugging and later checks for not well formed data
158 break;
159 }
160 // no lookup needed because we have reset the codemap.
161 indexStream.push(prevCode);
162 continue;
163
164 } else {
165 //prevcode is already in our dictionary but for debubbing purposes we still check
166 if (!this.reverseCodeMap.get(prevCode)) {
167 throw new Error(" An Error occured. A code was encountered that should have been known but it was not. " + prevCode + " " + nextCode + " " + this.nextCodenumber)
168 }
169 const newCodeSeq = Array.from(this.reverseCodeMap.get(prevCode)!.resolveTrieToSequence(true));
170
171 //two cases for if we already know the code or not
172 if (this.reverseCodeMap.has(nextCode)) {
173 const t = this.reverseCodeMap.get(nextCode)!.resolveTrieToSequence(true)
174 indexStream.push(...t);
175 newCodeSeq.push(t[0]);
176 } else {
177 newCodeSeq.push(newCodeSeq[0]);
178 indexStream.push(...newCodeSeq);
179 }
180
181 //handling of codesize increase
182 if (this.nextCodenumber >= ((1 << this.codesize) - 1)) {
183 if (this.codesize == this.MAXCODESIZE) {
184 //not elegant but it allows us to add the last possible codenumber (2^12-1) without accidents
185 if (this.nextCodenumber != ((1 << this.codesize) - 1)) {
186 prevCode = nextCode;
187 // we are not allowing any bigger codenumbers so adding anything new to the codemap makes nos ense.
188 continue;
189 }
190 } else {
191 this.codesize++;
192 this.unpacker.setCurrentCodeSize(this.codesize);
193 }
194 }
195
196 //add new codesequence to the Trie and reverse map
197 const position = newCodeSeq[newCodeSeq.length - 1];
198 const newNode = new ChildlessTrieNode(position);
199 newNode.parent = this.reverseCodeMap.get(prevCode)!;
200 this.reverseCodeMap.set(this.nextCodenumber, newNode);
201
202 this.nextCodenumber++;
203 prevCode = nextCode;
204 }
205 }
206
207 //cleanup
208 this.tearDown()
209
210 return indexStream;
211 }
212
213 /**
214 * sets some values at the start of decompression which will never change during decompression
215 */
216 private prepareBaseValues() {
217 this.resetCode = 1 << this.minimumlzwCodeLenght;
218 this.endOfDataCode = this.resetCode + 1;
219 this.nextCodenumber = this.endOfDataCode + 1;
220 for (let i = 0; i < this.resetCode; i++) {
221 const node = new ChildlessTrieNode(i);
222 this.dicitionaryBase[i] = node;
223 }
224 }
225
226 /**
227 * Helper function which resets the dicitonary to only the base cases and resets the codesize.
228 */
229 private resetcodemap() {
230 // clear gets rid of all non-base nodes because references are only kept via map
231 this.reverseCodeMap.clear()
232 this.nextCodenumber = this.endOfDataCode + 1;
233 this.codesize = this.minimumlzwCodeLenght + 1;
234
235 for (let i = 0; i < this.resetCode; i++) {
236 // a bit dumb but else we can't really point to our base symbols in the structure
237 this.reverseCodeMap.set(i, this.dicitionaryBase[i]);
238 }
239
240 this.unpacker.setCurrentCodeSize(this.codesize);
241 }
242
243 /**
244 * cleans up the state after decompression
245 */
246 private tearDown() {
247 this.dicitionaryBase = new Array();
248 this.unpacker = new UnpackingBench();
249 this.reverseCodeMap = new Map();
250 }
251
252}
253
254
255/**
256 * Compressor for GIF images.
257 * Offers methods to compress data into a new table based image data directly, or to do basic compression
258 */
259class Compressor {
260 private dicitonaryroot: TrieNode; // basis of our Trie
261 private currCodeMapPos: TrieNode; //instead of a temporary buffer we simply store where in the Trie we are
262 private packer: PackingBench; // takes care of bit packing the compressed data
263 private MAXCODESIZE = 12; // for gifs this should be 12
264 private nextCodenumber = 0; //next codenumber to be put into the codemap. On reset it is 2**initial codesize + 1
265 private codesize = 0; // how many bits do we currently use per code.
266 private resetCode = 0; // special code for "ups we need to reset the dictionary because we run out of space". Is set to 2**initial codesize
267 private endOfDataCode = 0; // special code for end of data. Is set to 2**initial codesize + 2
268 private minLZWCodeSize = 0; //minimal amount of bits needed to represent the number of colors. Codes for packing compressed data will use one more bit. At least 2, at most MaxCodesize
269
270 constructor() {
271 this.dicitonaryroot = new TrieNode(0);
272 this.currCodeMapPos = this.dicitonaryroot;
273 this.packer = new PackingBench();
274 }
275
276 /**
277 * Resets the dicitonary and codesize
278 */
279 private resetcodemap() {
280 //this.dicitonaryroot.orphanator(); //trades a bit of speed for sooner cleanup
281 this.dicitonaryroot = new TrieNode(0);
282
283 this.nextCodenumber = this.endOfDataCode + 1;
284
285 for (let i = 0; i < this.resetCode; i++) {
286 this.dicitonaryroot.addChild(new TrieNode(i), i);
287 }
288
289 this.currCodeMapPos = this.dicitonaryroot;
290
291 this.codesize = this.minLZWCodeSize + 1; // the least amount of bits needed to store the next number in the codemap
292 this.packer.setCurrentCodeSize(this.codesize);
293 }
294
295 /**
296 * Main part of the compression. Adds symbols into a Trie to walk and build up the compüression dictionary.
297 * Adds the corresponding compression numbers to the packer whenever needed.
298 * @param symbol next symbol to be added to the compression
299 * @returns true if compression took place, else false (equivalent to the question if the newly added symbol lead to a new dicitonary entry or not)
300 */
301 private addSymbolToCompressor(symbol: number): boolean {
302
303 // no actual compression needs to take place because we are not at a leaf of the Trie
304 if (this.currCodeMapPos.children[symbol] != undefined) {
305 this.currCodeMapPos = this.currCodeMapPos.children[symbol];
306 return false;
307 }
308
309 this.packer.addCode(this.currCodeMapPos.codeValue);
310
311 // increase codesize if needed. And reset if codesize gets above 12
312 if (this.nextCodenumber >= (1 << this.codesize)) {
313 if (this.codesize == this.MAXCODESIZE) {
314 // We could add a locked flag to allow the compressor to decide if the codemap should simply stop growing and be kept for some time longer
315 this.packer.addCode(this.resetCode);
316 this.resetcodemap();
317 // no code added to our map cause we start fresh next round
318 } else {
319 this.codesize++;
320 this.currCodeMapPos.addChild(new TrieNode(this.nextCodenumber), symbol)
321 this.nextCodenumber++;
322 }
323 this.packer.setCurrentCodeSize(this.codesize);
324 } else {
325 this.currCodeMapPos.addChild(new TrieNode(this.nextCodenumber), symbol)
326 this.nextCodenumber++;
327 }
328
329 this.currCodeMapPos = this.dicitonaryroot.children[symbol];
330
331 return true;
332
333 }
334
335 /**
336 * Ends the compression by adding whatever is left uncompressed to the packer,a dding a end of data code, and then returning the compressed data
337 * @returns The compressed data
338 */
339 private endCompression(): Array<number> {
340 if (this.currCodeMapPos != this.dicitonaryroot) {
341 this.packer.addCode(this.currCodeMapPos.codeValue);
342 }
343
344 this.packer.addCode(this.endOfDataCode);
345 const packedData = this.packer.endPacking();
346 return packedData;
347 }
348
349 /**
350 * Compresses a given sequence of indecies according to the gif augmented version of the LZW algortihm.
351 * @param indexes An array of indecies for a gif image. Entries need to be less than 2^minCodeSize (for example [0-255] for a minCodeSize of 8)
352 * @param minCodeSize At least the amount of bits needed to represent the highest given index. For gifs this value has to be between 2 and 12, and should correspond to the size of the associated colortable
353 * @returns The compressed gif image data
354 */
355 compress(indexes: number[], minCodeSize: number): Array<number> {
356 this.setUp(minCodeSize)
357 this.packer.addCode(this.resetCode);
358 for (let index of indexes) {
359 this.addSymbolToCompressor(index)
360 }
361 const compressedData = this.endCompression();
362 this.tearDown()
363 return compressedData
364 }
365
366 /**
367 * Same as {@link compress} but returns a ready to use TableBasedImageData object
368 * @returns TableBasedImageData which can be used as gifblock
369 */
370 compressToTableBasedImageData(indexes: number[], minCodeSize: number) {
371 const compressed = this.compress(indexes, minCodeSize)
372 return new TableBasedImageData({ lzwMinSize: minCodeSize, imageData: compressed })
373 }
374
375 /**
376 * Sets some values that will not change during compression
377 */
378 private setUp(minCodeSize: number) {
379 this.minLZWCodeSize = minCodeSize
380 this.resetCode = 1 << this.minLZWCodeSize;
381 this.endOfDataCode = this.resetCode + 1;
382 this.resetcodemap()
383 }
384
385 /**
386 * cleans the satte after compression
387 */
388 private tearDown() {
389 this.dicitonaryroot = new TrieNode(0);
390 this.currCodeMapPos = this.dicitonaryroot;
391 this.packer = new PackingBench();
392 }
393
394}
395
396
397/**
398 * Allows packing codes into one long sequence of bytes
399 * Code sizes for packing should be set by the user (like the compressor) to enter the right data.
400 */
401class PackingBench {
402 private packedData: number[] = [];
403 private currentBit: number = 0; // bit-position on the workbench
404 private workbench: number = 0; // we use 16 bit to prepare data for packing.
405 private currentCodeSize: number = 8; //how many bits the added codes should take up
406
407 static masks: number[] = [
408 0x0, 0x1, 0x3, 0x7, 0xF, 0x1F, 0x3F, 0x7F,
409 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF
410 ];
411
412 setCurrentCodeSize(codeSize: number) {
413 this.currentCodeSize = codeSize;
414 }
415
416 /**
417 * Puts the next code onto the bench. It will be packed with the current codesize. Code must not be larger than the current codesize allows.
418 * @param value code to be added.
419 */
420 addCode(value: number): void {
421 // if we go above the lenght of our workbench we put as much on it as we can before storing the packed data and adding the rest to a fresh workbench
422 if ((this.currentBit + this.currentCodeSize) > 16) {
423 const temp = (value << this.currentBit);
424 this.workbench = this.workbench | temp;
425
426 const t1 = (this.workbench >> 8) & PackingBench.masks[8];
427 const t2 = PackingBench.masks[8] & this.workbench;
428
429 this.packedData.push(t2);
430 this.packedData.push(t1);
431
432 this.workbench = (value >> (16 - this.currentBit));
433 this.currentBit = (this.currentBit + this.currentCodeSize) - 16;
434
435 } else {
436 const temp = (value << this.currentBit);
437 this.workbench = this.workbench | temp;
438 this.currentBit = this.currentBit + this.currentCodeSize;
439 }
440 }
441
442 /**
443 * Ends the packing process. If there is still unpacked data it will be packed and the end padded with 0s.
444 * Also resets the state so new packing can be started
445 * @returns the packed data
446 */
447 endPacking(): Array<number> {
448 // flush whatever is left on the workbench into packedData
449 if (this.currentBit != 0) {
450 if (this.currentBit <= 8) {
451 const t1 = ((this.workbench & PackingBench.masks[8]));
452 this.packedData.push(t1);
453 } else {
454 const t1 = (this.workbench >> 8) & PackingBench.masks[8];
455 const t2 = PackingBench.masks[8] & this.workbench;
456 this.packedData.push(t2);
457 this.packedData.push(t1);
458 }
459 }
460 // shallow copy because we reset the original
461 const tmpArr = Object.assign([], this.packedData);
462 this.reset();
463
464 return tmpArr
465 }
466
467 /**
468 * resets the state to some default values
469 */
470 private reset(): void {
471 this.packedData = [];
472 this.currentBit = 0;
473 this.workbench = 0;
474 this.currentCodeSize = 8;
475 }
476
477}
478
479
480/**
481 * Allows access to the prior packed codes in a compressed gif image.
482 * Code sizes should be set by the user (like the decompressor) to get the right data.
483 */
484class UnpackingBench {
485 public packedData: Array<number> = []; //unlike in the cpp variant where we used a stack. Here we need to make sure the first byte is at the end like in a queue, because JS pop works like that -.-
486 private currentBit: number = 0; // current position on the workbench
487 private workbench: number = 0; // 16 bit buffer to load and read from
488 private currentCodeSize: number = 8; //size of codes to be read
489 public nextCodeEmpty = true;
490
491 static masks: number[] = [
492 0x0, 0x1, 0x3, 0x7, 0xF, 0x1F, 0x3F, 0x7F,
493 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF
494 ];
495
496 /**
497 * Sets the size of codes that should be returned.
498 * @param codeSize
499 */
500 setCurrentCodeSize(codeSize: number) {
501 this.currentCodeSize = codeSize;
502 }
503
504 /**
505 * resets the inner state of the UnpackingBench. Should be called before it is reused.
506 */
507 reset(): void {
508 this.packedData = [];
509 this.currentBit = 0;
510 this.workbench = 0;
511 this.currentCodeSize = 8;
512 this.nextCodeEmpty = true;
513 }
514
515 /**
516 * Load data into the unpacker. Note:The first byte to read has to be the last in the array because we use pop
517 * @param data packed data to be loaded
518 */
519 loadPackeData(data: Array<number>) {
520 if (data.length != 0) {
521 this.packedData = Array.from(data);
522 this.packedData.reverse();
523 this.nextCodeEmpty = false;
524
525 // already preload data onto workbench
526 this.workbench = this.packedData.pop()!;
527 if (this.packedData.length != 0) {
528 this.workbench = this.workbench | (this.packedData.pop()! << 8)
529 }
530 }
531 }
532
533 /**
534 * Unpacks and returns the next code, using the currently set code size
535 * @returns next code number from loaded packed data, or 0 if nextCodeEmpty is true
536 */
537 getNextCode(): number {
538
539 if (this.nextCodeEmpty) {
540 console.log("No more data");
541 return 0;
542 }
543 // basically data will "fall" into the workbench from the left to the right whenever we fill up
544
545 let output = 0;
546
547 if (this.currentBit + this.currentCodeSize > 16) { // we need more data than is in the buffer so we grab some data from the stack to fill up
548
549 let overhang = (this.currentBit + this.currentCodeSize) - 16;
550 output = (this.workbench >> this.currentBit); //put the data that we do have on the bench into position, we fill the rest later
551 this.workbench = this.packedData.pop()!; // refill the first 8 bit
552 if (this.packedData.length !== 0) { // add another 8 bit if we can
553 const tmp = this.packedData.pop()!; // can't be empty because we checked
554 this.workbench = this.workbench | (tmp << 8); // refill the last 8 bit
555 }
556
557 if (this.packedData.length == 0) { // if we cannot load any more data at the current code size, we are official empty
558 this.nextCodeEmpty = true;
559 }
560
561 // now add the rest to the output
562 output = output | ((this.workbench & UnpackingBench.masks[overhang]) << (this.currentCodeSize - overhang));
563 this.currentBit = overhang;
564
565 } else {
566 output = (this.workbench >> this.currentBit) & UnpackingBench.masks[this.currentCodeSize];
567 this.currentBit = this.currentBit + this.currentCodeSize;
568
569 if (this.packedData.length == 0 && this.currentBit + this.currentCodeSize > 16) { //special case where the next code is a overhang but we did not laod enough data before
570 this.nextCodeEmpty = true;
571 }
572
573 }
574
575 return output;
576 }
577
578}