Code for a in-browser gif editor project
at master 578 lines 21 kB view raw
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}