Code for a in-browser gif editor project
at master 1285 lines 38 kB view raw
1/** 2 * File to hold all kinds of classes, types, enums, and so on. 3 * Mainly used to house the classes related directly to gifblocks and the gifView. 4 */ 5 6 7// Property types for the different blocks. 8 9type ImageDescriptorProperties = { 10 leftPosition?: number, 11 topPosition?: number, 12 width?: number, 13 height?: number, 14 lctFlag?: boolean, 15 interlacedFlag?: boolean, 16 lctSortedFlag?: boolean, 17 lctSize?: number, 18} 19 20type GraphicControlExtensionProperties = { 21 disposalMethod?: DisposalMethod, 22 userInputFlag?: boolean, 23 transparencyFlag?: boolean, 24 delay?: number, 25 transparencyIndex?: number 26} 27 28type LogicalScreenDescriptorProperties = { 29 screenWidth?: number, 30 screenHeight?: number, 31 gctFlag?: boolean, 32 colorResolution?: number, 33 gctSortedFlag?: boolean, 34 gctSize?: number, 35 backgroundColorIndex?: number, 36 pixelAspectRatio?: number, 37} 38 39type HeaderProperties = { 40 version?: "87a" | "89a" 41} 42 43type ApplicationExtensionProperties = { 44 identifier: string, 45 authenitifcation: [number, number, number], 46 applicationData: number[], 47 48} 49 50type PlainTextExtensionProperties = { 51 leftpos?: number, 52 topos?: number, 53 gridheight?: number, 54 gridwidth?: number, 55 cellheight?: number, 56 cellwidth?: number, 57 foregroundindex?: number, 58 backgroundindex?: number, 59 plaintextdata?: string 60} 61 62type ColorTableProperties = { 63 type?: BlockType.LocalColorTable | BlockType.GlobalColorTable | BlockType.ColorTable //last type is for special purpose like a standard table that is not actually in the gif 64 colors?: number[] | Uint8Array<ArrayBuffer> 65} 66 67/** 68 * Properties for the TableBasedImageData class. imageData is a raw lzw compressed image, not in subblocks. 69 */ 70type TableBasedImageDataProperties = { 71 lzwMinSize?: number, 72 imageData?: number[], 73} 74 75// Block related classes 76 77/** 78 * Basic structure to keep a blocks data, its type, and an id so we can adress them within a gif 79 */ 80abstract class GifBlock { 81 public blockType: BlockType; // what block we have 82 public blockID: number; //Number which should be unqiue to every block so we can find them later 83 public data: Uint8Array<ArrayBuffer> // Actual data of the block. SHould be private or rotected at some point in the future 84 85 constructor(blocktype: BlockType, blockID?: number, data?: Uint8Array<ArrayBuffer>) { 86 this.blockType = blocktype 87 this.blockID = typeof blockID !== "undefined" ? blockID : -1 88 this.data = data! 89 } 90 91 /** 92 * Creates a new block based on the data of the original one. 93 */ 94 abstract clone(): GifBlock 95 96} 97 98class ImageDescriptor extends (GifBlock) { 99 100 /** 101 * 102 * @param props pass an empty object if data is given directly. Else use this to set the initial values 103 * @param blockID A number which should be unique in whatever {@link GifView} this block is used. Defaults to -1 104 * @param data The raw bytes of the block, passed when a file is parsed. 105 */ 106 constructor(props: ImageDescriptorProperties, blockID?: number, data?: Uint8Array<ArrayBuffer>) { 107 if (typeof data !== "undefined") { 108 super(BlockType.ImageDescriptor, blockID, data) 109 } 110 else { 111 const newdata = new Uint8Array(10) //set lenght so we can initilaize with just zeroes and change later 112 super(BlockType.ImageDescriptor, blockID, newdata) 113 this.data[0] = 0x2C //image separator 114 this.leftPosition(props.leftPosition) 115 this.topPosition(props.topPosition) 116 this.height(props.height) 117 this.width(props.width) 118 this.lctFlag(props.lctFlag) 119 this.interlacedFlag(props.interlacedFlag) 120 this.lctSortedFlag(props.lctSortedFlag) 121 this.lctSize(props.lctSize) 122 } 123 } 124 125 public clone(): ImageDescriptor { 126 return new ImageDescriptor({}, -1, Uint8Array.from(this.data)); 127 } 128 129 public toProps(): ImageDescriptorProperties { 130 return { 131 leftPosition: this.leftPosition(), 132 topPosition: this.topPosition(), 133 height: this.height(), 134 width: this.width(), 135 lctFlag: this.lctFlag(), 136 interlacedFlag: this.interlacedFlag(), 137 lctSortedFlag: this.lctSortedFlag(), 138 lctSize: this.lctSize(), 139 } 140 } 141 142 public leftPosition(leftPos?: number) { 143 if (typeof leftPos !== "undefined") { 144 this.data![1] = leftPos & 0b11111111 145 this.data![2] = (leftPos >> 8) & 0b11111111 146 } 147 return this.data![1] | (this.data![2] << 8) 148 } 149 150 public topPosition(topPos?: number) { 151 if (typeof topPos !== "undefined") { 152 this.data![3] = topPos & 0b11111111 153 this.data![4] = (topPos >> 8) & 0b11111111 154 } 155 return this.data![3] | (this.data![4] << 8) 156 } 157 158 public width(width?: number) { 159 if (typeof width !== "undefined") { 160 this.data![5] = width & 0b11111111 161 this.data![6] = (width >> 8) & 0b11111111 162 } 163 return this.data![5] | (this.data![6] << 8) 164 } 165 166 public height(height?: number) { 167 if (typeof height !== "undefined") { 168 this.data![7] = height & 0b11111111 169 this.data![8] = (height >> 8) & 0b11111111 170 } 171 return this.data![7] | (this.data![8] << 8) 172 } 173 174 public lctFlag(flag?: boolean) { 175 if (typeof flag !== "undefined") { 176 this.data![9] = flag ? this.data![9] | 0b10000000 : this.data![9] & ~0b10000000 177 } 178 return (this.data![9] & 0b10000000) > 0 ? true : false 179 } 180 181 public interlacedFlag(flag?: boolean) { 182 if (typeof flag !== "undefined") { 183 this.data![9] = flag ? this.data![9] | 0b01000000 : this.data![9] & ~0b01000000 184 } 185 return (this.data![9] & 0b01000000) > 0 ? true : false 186 } 187 188 public lctSortedFlag(flag?: boolean) { 189 if (typeof flag !== "undefined") { 190 this.data![9] = flag ? this.data![9] | 0b00100000 : this.data![9] & ~0b00100000 191 } 192 return (this.data![9] & 0b00100000) > 0 ? true : false 193 } 194 195 public lctSize(lctSize?: number) { 196 if (typeof lctSize !== "undefined") { 197 this.data![9] = (this.data![9] & 0b11111000) | (lctSize & 0b111) 198 } 199 return this.data![9] & 0b111 200 } 201 202} 203 204class GraphicControlExtension extends (GifBlock) { 205 clone(): GraphicControlExtension { 206 return new GraphicControlExtension({}, -1, Uint8Array.from(this.data)) 207 } 208 209 constructor(props: GraphicControlExtensionProperties, blockID?: number, data?: Uint8Array<ArrayBuffer>) { 210 if (typeof data !== "undefined") { 211 super(BlockType.GraphicControlExtension, blockID, data) 212 } else { 213 const newData = new Uint8Array(BlockLenghts.GRAPHICCONTROLEXTENSION) 214 super(BlockType.GraphicControlExtension, blockID, newData) 215 this.data[0] = 0x21 //extrension introducer 216 this.data[1] = 0xF9 //extension label 217 this.data[2] = 4 // lenght 218 this.disposalMethod(props.disposalMethod) 219 this.userInputFlag(props.userInputFlag) 220 this.transparencyFlag(props.transparencyFlag) 221 this.delay(props.delay) 222 this.transparencyIndex(props.transparencyIndex) 223 this.data[7] = 0 224 } 225 226 } 227 228 public disposalMethod(method?: DisposalMethod) { 229 if (typeof method !== "undefined") { 230 this.data![3] = (this.data![3] & 0b11100011) | ((method & 0b111) << 2) 231 } 232 return ((this.data![3] >> 2) & 0b111) as DisposalMethod 233 } 234 235 public userInputFlag(flag?: boolean) { 236 if (typeof flag !== "undefined") { 237 this.data![3] = flag ? this.data![3] | 0b00000010 : this.data![3] & 0b11111101 238 } 239 return (this.data![3] & 0b00000010) > 0 ? true : false 240 } 241 242 public transparencyFlag(flag?: boolean) { 243 if (typeof flag !== "undefined") { 244 this.data![3] = flag ? this.data![3] | 0b00000001 : this.data![3] & 0b11111110 245 } 246 return (this.data![3] & 0b00000001) > 0 ? true : false 247 } 248 249 public delay(delay?: number) { 250 if (typeof delay !== "undefined") { 251 this.data![4] = delay & 0b11111111 252 this.data![5] = (delay >> 8) & 0b11111111 253 } 254 return this.data![4] | (this.data![5] << 8) 255 } 256 257 public transparencyIndex(index?: number) { 258 if (typeof index !== "undefined") { 259 this.data![6] = index 260 } 261 return this.data![6] 262 } 263 264} 265 266class LogicalScreenDescriptor extends GifBlock { 267 clone(): LogicalScreenDescriptor { 268 return new LogicalScreenDescriptor({}, this.blockID, Uint8Array.from(this.data)) 269 } 270 271 constructor(props: LogicalScreenDescriptorProperties, blockID?: number, data?: Uint8Array<ArrayBuffer>) { 272 if (typeof data !== "undefined") { 273 super(BlockType.LogicalScreenDescriptor, blockID, data) 274 } else { 275 const newData = new Uint8Array(BlockLenghts.LOGICALSCREENDESCRIPTOR) 276 super(BlockType.LogicalScreenDescriptor, blockID, newData) 277 this.screenWidth(props.screenWidth) 278 this.screenHeight(props.screenHeight) 279 this.gctFlag(props.gctFlag) 280 this.colorResolution(props.colorResolution) 281 this.gctSortedFlag(props.gctSortedFlag) 282 this.gctSize(props.gctSize) 283 this.backgroundColorIndex(props.backgroundColorIndex) 284 this.pixelAspectRatio(props.pixelAspectRatio) 285 } 286 } 287 288 public toProps(): LogicalScreenDescriptorProperties { 289 return { 290 screenWidth: this.screenWidth(), 291 screenHeight: this.screenHeight(), 292 gctFlag: this.gctFlag(), 293 colorResolution: this.colorResolution(), 294 gctSortedFlag: this.gctSortedFlag(), 295 gctSize: this.gctSize(), 296 backgroundColorIndex: this.backgroundColorIndex(), 297 pixelAspectRatio: this.pixelAspectRatio() 298 } 299 } 300 301 public screenWidth(width?: number) { 302 if (typeof width !== "undefined") { 303 this.data![0] = width & 0b11111111 304 this.data![1] = (width >> 8) & 0b11111111 305 } 306 return this.data![0] | (this.data![1] << 8) 307 } 308 309 public screenHeight(height?: number) { 310 if (typeof height !== "undefined") { 311 this.data![2] = height & 0b11111111 312 this.data![3] = (height >> 8) & 0b11111111 313 } 314 return this.data![2] | (this.data![3] << 8) 315 } 316 317 public gctFlag(flag?: boolean) { 318 if (typeof flag !== "undefined") { 319 this.data![4] = flag ? this.data![4] | 0b10000000 : this.data![4] & ~0b10000000 320 } 321 return (this.data![4] & 0b10000000) > 0 ? true : false 322 } 323 324 public colorResolution(resolution?: number) { 325 if (typeof resolution !== "undefined") { 326 this.data![4] = (this.data![4] & ~0b01110000) | ((resolution & 0b111) << 4) 327 } 328 return (this.data![4] & 0b01110000) >> 4 329 } 330 331 public gctSortedFlag(flag?: boolean) { 332 if (typeof flag !== "undefined") { 333 this.data![4] = flag ? this.data![4] | 0b00001000 : this.data![4] & ~0b00001000 334 } 335 return (this.data![4] & 0b00001000) > 0 ? true : false 336 } 337 338 339 public gctSize(size?: number) { 340 if (typeof size !== "undefined") { 341 this.data![4] = (this.data![4] & ~0b00000111) | (size & 0b00000111) 342 } 343 return (this.data![4] & 0b00000111) 344 } 345 346 347 public backgroundColorIndex(index?: number) { 348 if (typeof index !== "undefined") { 349 this.data![5] = index 350 } 351 return this.data![5] 352 } 353 354 // Aspect Ratio = (Pixel Aspect Ratio + 15) / 64 355 public pixelAspectRatio(ratio?: number) { 356 if (typeof ratio !== "undefined") { 357 this.data![6] = ratio 358 } 359 return this.data![6] 360 } 361 362} 363 364class ColorTable extends GifBlock { 365 clone(): ColorTable { 366 return new ColorTable(this.toProps(), this.blockID) 367 } 368 369 /** 370 * Because a color tables colors are all of its data there is no extra parameter for the data 371 * @param props type and colors need to be set. 372 * @param blockID 373 */ 374 constructor(props: ColorTableProperties, blockID?: number) { 375 if (props.colors!.length % 3 !== 0) { throw new Error("Not viable props.colors.lenght. Colortables need to have three entries per color.") } 376 377 const newData = Uint8Array.from(props.colors!) 378 super(props.type!, blockID, newData) 379 } 380 381 public toProps(): ColorTableProperties { 382 return { 383 type: this.blockType === BlockType.LocalColorTable ? BlockType.LocalColorTable : BlockType.GlobalColorTable, 384 colors: Uint8Array.from(this.data) 385 } 386 } 387 388 // returns how many colors are in the colortable 389 public getColorAmount() { 390 return this.data.length / 3 391 } 392 393 public getColor(index: number) { 394 return [this.data[index * 3], this.data[index * 3 + 1], this.data[index * 3 + 2]] 395 } 396 397 setColor(index: number, rgb: [number, number, number]) { 398 if (index > this.data.length / 3) { 399 return; 400 } 401 this.data![index * 3] = rgb[0] 402 this.data![index * 3 + 1] = rgb[1] 403 this.data![index * 3 + 2] = rgb[2] 404 } 405 406 setColors(colors: number[]) { 407 this.data! = Uint8Array.from(colors) 408 } 409 410 /** 411 * Returns the amount of bits needed to represent the amount of colors 412 */ 413 getBitAmount() { 414 return Math.ceil(Math.log2(this.getColorAmount())) 415 } 416 417} 418 419class Header extends GifBlock { 420 clone(): Header { 421 throw new Error("This Block should never be cloned.") 422 } 423 424 constructor(props: HeaderProperties, blockID?: number, data?: Uint8Array<ArrayBuffer>) { 425 if (typeof data !== "undefined") { 426 super(BlockType.Header, blockID, data) 427 } else { 428 const newData = new Uint8Array(BlockLenghts.HEADER) 429 super(BlockType.Header, blockID, newData) 430 this.data[0] = "G".charCodeAt(0) 431 this.data[1] = "I".charCodeAt(0) 432 this.data[2] = "F".charCodeAt(0) 433 this.version(props.version) 434 } 435 } 436 437 public version(version?: ("87a" | "89a")) { 438 if (typeof version !== "undefined") { 439 this.data[3] = version.charCodeAt(0) 440 this.data[4] = version.charCodeAt(1) 441 this.data[5] = version.charCodeAt(2) 442 } 443 444 return String.fromCharCode(...this.data.slice(3)) 445 } 446 447 448} 449 450class Trailer extends GifBlock { 451 clone(): Trailer { 452 throw new Error("This Block should never be cloned") 453 } 454 455 constructor(blockID?: number) { 456 const newData = Uint8Array.from([0x3B]) 457 super(BlockType.Trailer, blockID, newData) 458 } 459 460} 461 462class ApplicationExtension extends GifBlock { 463 464 clone(): ApplicationExtension { 465 return new ApplicationExtension(-1, Uint8Array.from(this.data)) 466 } 467 468 constructor(blockID?: number, data?: Uint8Array<ArrayBuffer>) { 469 if (typeof data !== "undefined") { 470 super(BlockType.ApplicationExtension, blockID, data) 471 } else { 472 const newData = new Uint8Array(15) 473 newData[0] = 0x21 474 newData[1] = 0xFF 475 newData[2] = 11 // block size 476 newData[14] = 0 477 super(BlockType.ApplicationExtension, blockID, newData) 478 } 479 480 } 481 482 identifier(identifier?: string) { 483 if (typeof identifier !== "undefined" && identifier.length >= 8) { 484 for (let index = 0; index < 8; index++) { 485 this.data[3 + index] = identifier.charCodeAt(index); 486 } 487 } 488 489 return String.fromCharCode(...this.data.slice(3, 11)) 490 } 491 492 authentification(auth?: [number, number, number]) { 493 if (typeof auth !== "undefined") { 494 this.data[11] = auth[0] 495 this.data[12] = auth[1] 496 this.data[13] = auth[2] 497 } 498 499 return this.data.slice(11, 14) 500 } 501 502 getApplicationdata() { 503 return (getDataFromSubDataBlock(this.data.slice(14))) 504 } 505 506 setApplicationdata(data: number[]) { 507 this.data = Uint8Array.from(Array.from(this.data.slice(0, 14)).concat(putDatatIntoSubdataBlocks(data))) 508 } 509 510} 511 512/** 513 * Represents a specific Applicationblock, defined by netscape to allow the looping of gifs. 514 * 515 */ 516class NetscapeLoopApplication extends ApplicationExtension { 517 //https://web.archive.org/web/19990418091037/http://www6.uniovi.es/gifanim/gifabout.htm 518 519 private static header = [0x21, 0xFF, 11, 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48] // "NETSCAPE" "2.0" 520 /** 521 * always initializes with an infinite loop 522 */ 523 constructor(blockID?: number) { 524 525 const newData = [0x21, 0xFF, 11, 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48, 3, 1, 0, 0, 0] 526 super(blockID, Uint8Array.from(newData)) 527 } 528 529 setLoops(loops: number) { 530 this.data[16] = loops & 7 531 this.data[17] = (loops >> 8) & 7 532 } 533 534 getLoops() { 535 return this.data[16] | (this.data[17] << 8) 536 } 537 538} 539 540class TableBasedImageData extends GifBlock { 541 clone(): TableBasedImageData { 542 const newtbid = new TableBasedImageData({}, -1, Uint8Array.from(this.data)) 543 //newtbid.setDryImage(this.getDryImage()) 544 return newtbid 545 } 546 547 private dryImage: number[] | undefined; 548 549 constructor(props: TableBasedImageDataProperties, blockID?: number, data?: Uint8Array<ArrayBuffer>) { 550 if (typeof data !== "undefined") { 551 super(BlockType.TableBasedImageData, blockID, data) 552 } else { 553 let newData = [props.lzwMinSize!] 554 newData = newData.concat(putDatatIntoSubdataBlocks(props.imageData!)) 555 super(BlockType.TableBasedImageData, blockID, Uint8Array.from(newData)) 556 } 557 this.dryImage = undefined 558 } 559 560 LZWMinimumCodeSize(minCodeSize?: number) { 561 if (typeof minCodeSize !== "undefined") { 562 this.data[0] = minCodeSize 563 } 564 return this.data[0] 565 } 566 567 /** 568 * @returns The compressed image data 569 */ 570 getImageData() { 571 return getDataFromSubDataBlock(this.data.slice(1)) 572 } 573 574 /** 575 * Takes a freshly compressed gif image and puts it inot the tbids subdata blocks 576 * @param imageData compressed image data 577 */ 578 setImageData(imageData: number[]) { 579 const newData = [this.data[0]] 580 this.data = Uint8Array.from(newData.concat(putDatatIntoSubdataBlocks(imageData))) 581 } 582 583 /** 584 * Reencodes data so it uses another number of symbols as base vocabulary. 585 * On reencoding any base codes that are too big for the new vocabulary will be replaced with 'code%targetLZWMinCodeSize' 586 * @param targetLZWMinCodeSize The amount of bits available for the base vocabulary. Has to be between 2 and 12 (sizes from the gif standard) 587 */ 588 reencode(targetLZWMinCodeSize: number) { 589 targetLZWMinCodeSize = Math.min(Math.max(targetLZWMinCodeSize, 2), 12) 590 591 const compressor = new Compressor() 592 let indexstream = decompressor.decompressTableBasedImageData(this) 593 const maxVal = (1 << (targetLZWMinCodeSize)) 594 indexstream = indexstream.map( 595 (value) => value >= maxVal ? value % maxVal : value 596 ) 597 this.setImageData(compressor.compress(indexstream, targetLZWMinCodeSize)) 598 this.LZWMinimumCodeSize(targetLZWMinCodeSize) 599 } 600 601 /** 602 * Returns the decompressed index sequence 603 * Could be used for caching 604 */ 605 getDryImage() { 606 return decompressor.decompressTableBasedImageData(this); 607 //This is a bad idea in terms of memory... 608 /** 609 if (typeof this.dryImage === "undefined") { 610 this.dryImage = decompressor.decompressTableBasedImageData(this) 611 } 612 return this.dryImage*/ 613 } 614 615} 616 617class CommentExtension extends GifBlock { 618 clone(): CommentExtension { 619 return new CommentExtension(-1, Uint8Array.from(this.data)) 620 } 621 622 constructor(blockID?: number, data?: Uint8Array<ArrayBuffer>) { 623 if (typeof data !== "undefined") { 624 super(BlockType.CommentExtension, blockID, data) 625 } else { 626 const newData = Uint8Array.from([0x21, 0xFE, 0x00]) 627 super(BlockType.CommentExtension, blockID, newData) 628 } 629 } 630 631 public setComment(commentmessage: string) { 632 const comment = [ 633 0x21, // extension introducer 634 0xFE, // Comment Label 635 ]; 636 const commentdata = [] 637 for (let char of commentmessage) { 638 commentdata.push(char.charCodeAt(0)) 639 } 640 this.data = Uint8Array.from(comment.concat(putDatatIntoSubdataBlocks(commentdata))) 641 } 642 643 public getComment() { 644 return String.fromCharCode(...getDataFromSubDataBlock(this.data.slice(2))) 645 } 646} 647 648class PlainTextExtension extends GifBlock { 649 clone(): GifBlock { 650 return new PlainTextExtension({}, -1, Uint8Array.from(this.data)) 651 } 652 653 constructor(props: PlainTextExtensionProperties, blockID?: number, data?: Uint8Array<ArrayBuffer>) { 654 if (typeof data !== "undefined") { 655 super(BlockType.PlainTextExtension, blockID, data) 656 } else { 657 const newData = new Uint8Array(16) 658 newData[0] = 0x21 659 newData[1] = 0x01 660 newData[2] = 12 // block size 661 for (let i = 3; i < 15; i++) { 662 newData[i] = 0; 663 } 664 newData[15] = 0 665 //TODO use the props here to set the values 666 super(BlockType.PlainTextExtension, blockID, newData) 667 this.gridTopPosition(props.topos) 668 this.gridLeftPosition(props.leftpos) 669 this.gridWidth(props.gridwidth) 670 this.gridHeight(props.gridheight) 671 this.cellWidth(props.cellwidth) 672 this.cellHeight(props.cellheight) 673 this.textForegorundColorIndex(props.foregroundindex) 674 this.textBackgroundColorIndex(props.backgroundindex) 675 this.textBackgroundColorIndex(props.backgroundindex) 676 if (props.plaintextdata) { 677 this.setPlainTextData(props.plaintextdata) 678 } 679 } 680 } 681 682 gridLeftPosition(leftpos?: number) { 683 if (typeof leftpos !== "undefined") { 684 this.data[3] = leftpos & 0b11111111 685 this.data[4] = (leftpos >> 8) & 0b11111111 686 } 687 688 return this.data[3] | (this.data[4] << 8) 689 } 690 691 gridTopPosition(toppos?: number) { 692 if (typeof toppos !== "undefined") { 693 this.data[5] = toppos & 0b11111111 694 this.data[6] = (toppos >> 8) & 0b11111111 695 } 696 697 return this.data[5] | (this.data[6] << 8) 698 } 699 700 gridWidth(width?: number) { 701 if (typeof width !== "undefined") { 702 this.data[7] = width & 0b11111111 703 this.data[8] = (width >> 8) & 0b11111111 704 } 705 706 return this.data[7] | (this.data[8] << 8) 707 } 708 709 gridHeight(height?: number) { 710 if (typeof height !== "undefined") { 711 this.data[9] = height & 0b11111111 712 this.data[10] = (height >> 8) & 0b11111111 713 } 714 715 return this.data[9] | (this.data[10] << 8) 716 } 717 718 cellWidth(width?: number) { 719 if (typeof width !== "undefined") { 720 this.data[11] = width 721 } 722 723 return this.data[11] 724 } 725 726 cellHeight(height?: number) { 727 if (typeof height !== "undefined") { 728 this.data[12] = height 729 } 730 731 return this.data[12] 732 } 733 734 textForegorundColorIndex(index?: number) { 735 if (typeof index !== "undefined") { 736 this.data[13] = index 737 } 738 739 return this.data[13] 740 } 741 742 textBackgroundColorIndex(index?: number) { 743 if (typeof index !== "undefined") { 744 this.data[14] = index 745 } 746 747 return this.data[14] 748 } 749 750 getPlainTextData() { 751 return String.fromCharCode(...getDataFromSubDataBlock(this.data.slice(15,))) 752 } 753 754 setPlainTextData(plaintext: string) { 755 const plaintextcodes = [] 756 for (let char of plaintext) { 757 plaintextcodes.push(char.charCodeAt(0)) 758 } 759 const newData = Array.from(this.data.slice(0, 15)) 760 this.data = new Uint8Array(newData.concat(putDatatIntoSubdataBlocks(plaintextcodes))) 761 } 762 763} 764 765 766// Types to make our life easier 767 768/** 769 * Type to bundle all possible data for a single gif image. Contains at least the image descriptor and table based image data 770 */ 771type GifBlockImage = { 772 graphicextension?: GraphicControlExtension; 773 imagedescriptor: ImageDescriptor; 774 colortable: ColorTable; 775 tableBasedImageData: TableBasedImageData; 776} 777 778/** 779 * type to bundle all data related to a plaintext extension. 780 */ 781type GifBlockPlainText = { 782 graphicextension?: GraphicControlExtension; 783 globalColortable: ColorTable; 784 plainText: PlainTextExtension; 785} 786 787/** 788 * type to bundle all data related to the meta datat of a gif. 789 */ 790type GifBlockMeta = { 791 header: Header; 792 logicalScreen: LogicalScreenDescriptor; 793 globalColorTable?: ColorTable; 794} 795 796/** 797 * Lenght of all fixed length blocks 798 */ 799enum BlockLenghts { 800 HEADER = 6, 801 LOGICALSCREENDESCRIPTOR = 7, 802 IMAGEDESCRIPTOR = 10, 803 GRAPHICCONTROLEXTENSION = 8, 804 TRAILER = 1, 805} 806 807/** 808 * All different types of blocks. 809 */ 810enum BlockType { 811 None, 812 Header, 813 LogicalScreenDescriptor, 814 GlobalColorTable, 815 TableBasedImageData, 816 ImageDescriptor, 817 LocalColorTable, 818 Trailer, 819 820 GraphicControlExtension, 821 CommentExtension, 822 PlainTextExtension, 823 ApplicationExtension, 824 ColorTable, 825} 826 827/** 828 * Different integer values of disposal methods that graphic control extensions use 829 */ 830enum DisposalMethod { 831 NoDisposalDefined, 832 DoNotDispose, 833 RestoreToBackgroundColor, 834 RestoreToPrevious, 835}; 836 837/** 838 * jsut a collection of things we might wanna keep around. Might not be a good idea to do it like this tho. 839 */ 840type siteState = { 841 currBlock: number; // where we are in terms of "full" blocks 842 maxBlockCount: number; 843 blocksperpage: number; 844 currFileName?: string 845 defaultColortable: ColorTable 846 tempSaved?: GifBlockImage | GifBlock | GifBlockPlainText //TODO allow copy and paste of blocks 847} 848 849 850/** 851 * Class representing a parsed gif in memory. Holds the gif blocks and allows access and manpilaution of them and their ordering. 852 */ 853class GifView { 854 855 //we cut the gif's blob into finer blocks and store these 856 public blocks: Array<GifBlock>; 857 private lastID = 0; //id for new blocks 858 public logicalScreen: LogicalScreenDescriptor | undefined 859 public globalColorTable: ColorTable | undefined 860 861 /** 862 * Creates a new GifView with a Header, LSD, GCT and Trailer. 863 * @returns a fresh GifView object 864 */ 865 static createBlankGif() { 866 const mgifview = new GifView() 867 const lsd = new LogicalScreenDescriptor({ 868 screenWidth: 500, screenHeight: 250, gctFlag: true, gctSize: 7, 869 gctSortedFlag: false, colorResolution: 7, backgroundColorIndex: 1, pixelAspectRatio: 0 870 }); 871 const gct = new ColorTable({ type: BlockType.GlobalColorTable, colors: BlockAdder.makeRGBColor() }); 872 const blanks = [ 873 new Header({ version: "89a" }), 874 lsd, 875 gct, 876 new Trailer() 877 ] 878 mgifview.addBlocks(blanks, 0) 879 mgifview.logicalScreen = lsd; 880 mgifview.globalColorTable = gct; 881 return mgifview 882 } 883 884 /** 885 * Creates a new GifView. If any gif data is supplied it will be parsed 886 * @param buffer gif data (like a file's ArrayBuffer) that should be parsed into blocks 887 */ 888 public constructor(buffer?: ArrayBuffer | Uint8Array) { 889 this.blocks = new Array(); 890 if (buffer && buffer.byteLength > 0) { 891 this.parseGifToBlockPointers(new Uint8Array(buffer)); 892 } 893 } 894 895 /** 896 * Parses the given Uint8Array and puts the data in neat little boxes. Should probably be async but meh. 897 * @param gifBlob Uint8Array comming from wherever, has to hold the bytes of a valid gif 898 */ 899 private parseGifToBlockPointers(gifBlob: Uint8Array) { 900 901 /** 902 * Takes the position of the first sub data block and returns the position of to the terminating subdatablock or -1 if it would go out of bounds 903 * @param position 904 */ 905 const parseSubData = function (position: number): number { 906 let blockLenght = gifBlob[position]; 907 while (blockLenght !== 0 && position + blockLenght < gifBlob.length) { 908 position = position + blockLenght + 1; // go to next block start. 909 blockLenght = gifBlob[position]; 910 } 911 if (position + blockLenght >= gifBlob.length) { 912 console.log("parsing error") //TODO throw an error insetad of returning -1 913 return -1; 914 } 915 return position; 916 } 917 918 919 let position = 0; //where in the data are we at the moment 920 let dataleft = true; //flag to indicte that parsing is over. //TODO this shoudl be replaced and all non-normal ways of ending parsing should throw an error. 921 922 // header 923 this.blocks.push(new Header({}, this.lastID, gifBlob.slice(0, position + BlockLenghts.HEADER))); 924 this.lastID++ 925 position = position + BlockLenghts.HEADER; 926 927 // LSD + optional GCT 928 this.logicalScreen = new LogicalScreenDescriptor({}, this.lastID, gifBlob.slice(position, position + BlockLenghts.LOGICALSCREENDESCRIPTOR)) 929 this.blocks.push(this.logicalScreen); 930 this.lastID++ 931 932 if (this.logicalScreen.gctFlag()) { // GCT present 933 let gctlen = 3 * (1 << ((gifBlob[position + 4] & 0b111) + 1)) 934 position = position + BlockLenghts.LOGICALSCREENDESCRIPTOR 935 this.globalColorTable = new ColorTable({ type: BlockType.GlobalColorTable, colors: gifBlob.slice(position, position + gctlen) }, this.lastID) 936 this.blocks.push(this.globalColorTable); 937 position = position + gctlen; 938 this.lastID++ 939 } else { 940 position = position + BlockLenghts.LOGICALSCREENDESCRIPTOR 941 } 942 943 while (dataleft) { 944 switch (gifBlob[position]) { 945 //for now only gce and application extension 946 case 0x21: // we have an extension block: 947 if (gifBlob[position + 1] && gifBlob[position + 1] == 0xF9) { //Graphic control extension 948 this.blocks.push(new GraphicControlExtension( 949 {}, 950 this.lastID, 951 gifBlob.slice(position, position + BlockLenghts.GRAPHICCONTROLEXTENSION) 952 )); 953 this.lastID++ 954 position = position + BlockLenghts.GRAPHICCONTROLEXTENSION; 955 } else if (gifBlob[position + 1] && gifBlob[position + 1] == 0xFF) { //Application extension 956 let applicationDataStart = position + 14; 957 let end = parseSubData(applicationDataStart); 958 if (end < 0) { 959 console.log("wtf, problem at byte " + applicationDataStart); 960 dataleft = false; 961 break; 962 } else { 963 this.blocks.push(new ApplicationExtension(this.lastID, gifBlob.slice(position, end + 1))); 964 this.lastID++ 965 } 966 position = end + 1; //skips behind the block terminator 967 } else if (gifBlob[position + 1] && gifBlob[position + 1] == 0xFE) { //Comment Extension 968 let commentDataStart = position + 2; 969 let end = parseSubData(commentDataStart); 970 if (end < 0) { 971 console.log("wtf, problem at byte " + commentDataStart); 972 dataleft = false; 973 break; 974 } else { 975 this.blocks.push(new CommentExtension(this.lastID, gifBlob.slice(position, end + 1))); 976 this.lastID++; 977 } 978 position = end + 1; 979 } else if (gifBlob[position + 1] && gifBlob[position + 1] == 0x01) { //PlainText Extension 980 let plaintextDataStart = position + 15; 981 let end = parseSubData(plaintextDataStart); 982 if (end < 0) { 983 console.log("wtf, problem at byte " + plaintextDataStart); 984 dataleft = false; 985 break; 986 } else { 987 this.blocks.push(new PlainTextExtension({}, this.lastID, gifBlob.slice(position, end + 1))); 988 this.lastID++ 989 } 990 position = end + 1; 991 } else { 992 console.log("Extension currently not supported. Aborting parsing"); 993 dataleft = false; 994 } 995 break; 996 997 case 0x2C: // we have an image descriptor so we parse the whole image 998 //image descriptor 999 const imgdscr = new ImageDescriptor( 1000 {}, 1001 this.lastID, 1002 gifBlob.slice(position, position + BlockLenghts.IMAGEDESCRIPTOR)) 1003 this.blocks.push(imgdscr); 1004 this.lastID++ 1005 if (imgdscr.lctFlag()) { //handle local colortable if one exists 1006 let ctlen = 3 * (1 << (imgdscr.lctSize() + 1)); 1007 position = position + BlockLenghts.IMAGEDESCRIPTOR; 1008 this.blocks.push(new ColorTable( 1009 { 1010 type: BlockType.LocalColorTable, 1011 colors: gifBlob.slice(position, position + ctlen) 1012 }, 1013 this.lastID 1014 )) 1015 this.lastID++ 1016 position = position + ctlen; 1017 } else { 1018 position = position + BlockLenghts.IMAGEDESCRIPTOR; 1019 } 1020 //handle tbid 1021 const start = position; 1022 position = position + 1; // skip the min lzw size byte and point to the first subdata block 1023 const end = parseSubData(position); 1024 if (end < 0) { 1025 console.log("wtf, problem at byte " + position + " with value " + gifBlob[position]); 1026 dataleft = false; 1027 break; 1028 } else { 1029 this.blocks.push(new TableBasedImageData({}, this.lastID, gifBlob.slice(start, end + 1))) 1030 this.lastID++ 1031 } 1032 position = end + 1; //end points to the last byte of the tbid so we go one further. 1033 break; 1034 1035 case 0x3B: // end of data 1036 this.blocks.push(new Trailer(this.lastID)) 1037 this.lastID++ 1038 dataleft = false; 1039 break; 1040 1041 default: 1042 console.log("wtf, problem at byte " + position + " with value " + gifBlob[position]); 1043 dataleft = false; 1044 break; 1045 } 1046 } 1047 1048 this.lastID++ // just to be sure we don 't give the same id twice 1049 console.log("Finished populating blocks.") 1050 } 1051 1052 /** 1053 * Returns the index of a GifBlock in the block array based on the ID. If no such block is found, -1 is returned. 1054 */ 1055 getBlockPosition(block: GifBlock) { 1056 return this.blocks.findIndex((value) => value.blockID == block.blockID); 1057 } 1058 1059 /** 1060 * Returns the first block from the block array with the given id. Undefined is returned if no block was found 1061 */ 1062 getBlockFromID(id: number) { 1063 return this.blocks.find((value) => value.blockID == id); 1064 } 1065 1066 /** 1067 * Returns all blocks from the block array of a given kind 1068 */ 1069 public getBlocksOfType(type: BlockType) { 1070 return this.blocks.filter((x) => x.blockType == type); 1071 } 1072 1073 /** 1074 * Exports the internal data to a Blob with type 'image/gif' 1075 */ 1076 public async exportToBlob() { 1077 const parts = [] 1078 for (let index = 0; index < this.blocks.length; index++) { 1079 if (typeof this.blocks[index].data !== "undefined") { 1080 parts.push(this.blocks[index].data) 1081 } else { 1082 throw new Error(`Block ${this.blocks[index].blockID} with undefined data in gif.`) 1083 } 1084 } 1085 1086 return new Blob(parts, { type: "image/gif" }) 1087 } 1088 1089 /** 1090 * Same as {@link getBlockPosition} but uses the id directly instead of a block 1091 */ 1092 getBlockPositionFromID(blockID: number) { 1093 return this.blocks.findIndex((value) => value.blockID == blockID) 1094 } 1095 1096 /** 1097 * Add new blocks into the gifview. The blocks blockID will be overwritten by this function. 1098 * @param newBlocks new blocks to be addedd 1099 * @param position where in the array the new blocks are to be addedd. The block at the given position will be offset to the left so the new blocks start at position+1 1100 */ 1101 public addBlocks(newBlocks: GifBlock[], position: number) { 1102 for (let block of newBlocks) { 1103 block.blockID = this.lastID; 1104 this.lastID++; 1105 } 1106 this.blocks.splice(position + 1, 0, ...newBlocks) 1107 } 1108 1109 /** 1110 * Removes the first blocks with the given ids from the block array 1111 */ 1112 public removeBlocks(idsToRemove: number[]) { 1113 const positionsToRemove = [] 1114 for (let bID of idsToRemove) { 1115 const index = this.blocks.findIndex((value) => value.blockID === bID) 1116 if (index >= 0) { 1117 positionsToRemove.push(index) 1118 } 1119 } 1120 1121 positionsToRemove.sort((a, b) => b - a) // descending order so we can splice without errors because of array lenght changes 1122 for (let pos of positionsToRemove) { 1123 this.blocks.splice(pos, 1) 1124 } 1125 1126 } 1127 1128 /** 1129 * Reencodes all images such that their lzwminocde fits the given colortable size. 1130 * Can be used to make the final gif a bit smaller 1131 */ 1132 public recodeImages() { 1133 const meta = this.bundleMeta()! 1134 for (let block of this.blocks) { 1135 if (block.blockType === BlockType.TableBasedImageData) { 1136 const bundle = this.bundleImage(block)! 1137 let newSize = (block as TableBasedImageData).LZWMinimumCodeSize(); 1138 if (bundle.imagedescriptor.lctFlag()) { 1139 newSize = bundle.imagedescriptor.lctSize() + 1 1140 } else if (meta.logicalScreen.gctFlag()) { 1141 newSize = meta.logicalScreen.gctSize() + 1 1142 } 1143 if (newSize !== (block as TableBasedImageData).LZWMinimumCodeSize()) { 1144 (block as TableBasedImageData).reencode(newSize) 1145 } 1146 } 1147 } 1148 } 1149 1150 /** 1151 * Gathers datat related to an image and returns it. 1152 * The image is the first one based on TableBasedImageData to right or equals to the contextBlock 1153 */ 1154 public bundleImage(contextBlock: GifBlock): GifBlockImage | undefined { 1155 let currBlockIndex = this.getBlockPosition(contextBlock); 1156 if (currBlockIndex == -1) { 1157 return undefined; 1158 } 1159 1160 let tbid: TableBasedImageData; 1161 let controlextension: GraphicControlExtension | undefined; 1162 let colortable: ColorTable; 1163 let imgdescriptor: ImageDescriptor; 1164 1165 let tbidIndex = -1; 1166 // we check indecies to the right, so that we always catch the tbid associated with any image related block we have 1167 while (currBlockIndex < this.blocks.length) { 1168 if (this.blocks[currBlockIndex].blockType === BlockType.TableBasedImageData) { 1169 tbidIndex = currBlockIndex; 1170 break; 1171 } 1172 currBlockIndex++ 1173 } 1174 1175 if (tbidIndex == -1) { 1176 return undefined; 1177 } 1178 tbid = this.blocks[tbidIndex] as TableBasedImageData 1179 1180 //now we walk backwards 1181 let imgdescriptorIndex = -1; 1182 while (currBlockIndex > 0) { 1183 if (this.blocks[currBlockIndex].blockType == BlockType.ImageDescriptor) { 1184 imgdescriptorIndex = currBlockIndex; 1185 break; 1186 } 1187 currBlockIndex-- 1188 } 1189 1190 if (imgdescriptorIndex == -1) { 1191 throw new Error("No Image Descriptor for available") 1192 return undefined; 1193 } 1194 1195 imgdescriptor = this.blocks[imgdescriptorIndex] as ImageDescriptor 1196 1197 if (imgdescriptor.lctFlag()) { 1198 colortable = this.blocks[imgdescriptorIndex + 1] as ColorTable 1199 } else if (this.globalColorTable) { 1200 colortable = this.globalColorTable as ColorTable 1201 } else { 1202 colortable = state.defaultColortable // for any gif that has neither gct nor lct 1203 } 1204 1205 let controlIndex = -1; 1206 while (currBlockIndex > 0) { 1207 if (this.blocks[currBlockIndex].blockType == BlockType.GraphicControlExtension) { 1208 controlIndex = currBlockIndex; 1209 break; 1210 } else if (this.blocks[currBlockIndex].blockType == BlockType.TableBasedImageData || this.blocks[currBlockIndex].blockType == BlockType.PlainTextExtension) { 1211 break; 1212 } 1213 currBlockIndex--; 1214 } 1215 1216 if (controlIndex > 0) { 1217 controlextension = this.blocks[controlIndex] as GraphicControlExtension 1218 } 1219 1220 //not smart but should work to make sure we can use gct's and get the first image in the gif with the gct even if it would have an lct when bundled 1221 if (contextBlock.blockType == BlockType.GlobalColorTable) { 1222 colortable = contextBlock as ColorTable; 1223 } 1224 1225 const gifimage: GifBlockImage = { 1226 graphicextension: controlextension, 1227 imagedescriptor: imgdescriptor, 1228 tableBasedImageData: tbid, 1229 colortable: colortable 1230 }; 1231 1232 return gifimage; 1233 } 1234 1235 /** 1236 * Gathers datat related to a plain text extension and returns it. 1237 */ 1238 public bundlePlainText(blockPointer: PlainTextExtension): undefined | GifBlockPlainText { 1239 1240 let currBlockIndex = this.getBlockPositionFromID(blockPointer.blockID) 1241 if (currBlockIndex < 0 || this.blocks[2].blockType !== BlockType.GlobalColorTable) { 1242 return undefined 1243 } 1244 1245 currBlockIndex-- 1246 let gceIndex = -1 1247 while (currBlockIndex > 0) { 1248 if (this.blocks[currBlockIndex].blockType === BlockType.GraphicControlExtension) { 1249 gceIndex = currBlockIndex 1250 break; 1251 } else if (this.blocks[currBlockIndex].blockType === BlockType.TableBasedImageData || this.blocks[currBlockIndex].blockType === BlockType.PlainTextExtension) { 1252 break; 1253 } 1254 currBlockIndex-- 1255 } 1256 1257 let gce; 1258 if (gceIndex > 0) { 1259 gce = this.blocks[gceIndex] as GraphicControlExtension 1260 } 1261 1262 let bundle: GifBlockPlainText = { 1263 globalColortable: this.blocks[2] as ColorTable, 1264 plainText: blockPointer, 1265 graphicextension: gce, 1266 }; 1267 1268 return bundle 1269 } 1270 1271 /** 1272 * Function to retrieve the Header, Logical Screen, and optional Global Colortable of the gif. 1273 */ 1274 public bundleMeta(): GifBlockMeta { 1275 const header = this.blocks[0] as Header 1276 const lsd = this.blocks[1] as LogicalScreenDescriptor 1277 let gct = undefined 1278 if (lsd.gctFlag()) { 1279 gct = this.blocks[2] as ColorTable 1280 } 1281 1282 return { header: header, logicalScreen: lsd, globalColorTable: gct } 1283 } 1284 1285}