/** * File to hold all kinds of classes, types, enums, and so on. * Mainly used to house the classes related directly to gifblocks and the gifView. */ // Property types for the different blocks. type ImageDescriptorProperties = { leftPosition?: number, topPosition?: number, width?: number, height?: number, lctFlag?: boolean, interlacedFlag?: boolean, lctSortedFlag?: boolean, lctSize?: number, } type GraphicControlExtensionProperties = { disposalMethod?: DisposalMethod, userInputFlag?: boolean, transparencyFlag?: boolean, delay?: number, transparencyIndex?: number } type LogicalScreenDescriptorProperties = { screenWidth?: number, screenHeight?: number, gctFlag?: boolean, colorResolution?: number, gctSortedFlag?: boolean, gctSize?: number, backgroundColorIndex?: number, pixelAspectRatio?: number, } type HeaderProperties = { version?: "87a" | "89a" } type ApplicationExtensionProperties = { identifier: string, authenitifcation: [number, number, number], applicationData: number[], } type PlainTextExtensionProperties = { leftpos?: number, topos?: number, gridheight?: number, gridwidth?: number, cellheight?: number, cellwidth?: number, foregroundindex?: number, backgroundindex?: number, plaintextdata?: string } type ColorTableProperties = { type?: BlockType.LocalColorTable | BlockType.GlobalColorTable | BlockType.ColorTable //last type is for special purpose like a standard table that is not actually in the gif colors?: number[] | Uint8Array } /** * Properties for the TableBasedImageData class. imageData is a raw lzw compressed image, not in subblocks. */ type TableBasedImageDataProperties = { lzwMinSize?: number, imageData?: number[], } // Block related classes /** * Basic structure to keep a blocks data, its type, and an id so we can adress them within a gif */ abstract class GifBlock { public blockType: BlockType; // what block we have public blockID: number; //Number which should be unqiue to every block so we can find them later public data: Uint8Array // Actual data of the block. SHould be private or rotected at some point in the future constructor(blocktype: BlockType, blockID?: number, data?: Uint8Array) { this.blockType = blocktype this.blockID = typeof blockID !== "undefined" ? blockID : -1 this.data = data! } /** * Creates a new block based on the data of the original one. */ abstract clone(): GifBlock } class ImageDescriptor extends (GifBlock) { /** * * @param props pass an empty object if data is given directly. Else use this to set the initial values * @param blockID A number which should be unique in whatever {@link GifView} this block is used. Defaults to -1 * @param data The raw bytes of the block, passed when a file is parsed. */ constructor(props: ImageDescriptorProperties, blockID?: number, data?: Uint8Array) { if (typeof data !== "undefined") { super(BlockType.ImageDescriptor, blockID, data) } else { const newdata = new Uint8Array(10) //set lenght so we can initilaize with just zeroes and change later super(BlockType.ImageDescriptor, blockID, newdata) this.data[0] = 0x2C //image separator this.leftPosition(props.leftPosition) this.topPosition(props.topPosition) this.height(props.height) this.width(props.width) this.lctFlag(props.lctFlag) this.interlacedFlag(props.interlacedFlag) this.lctSortedFlag(props.lctSortedFlag) this.lctSize(props.lctSize) } } public clone(): ImageDescriptor { return new ImageDescriptor({}, -1, Uint8Array.from(this.data)); } public toProps(): ImageDescriptorProperties { return { leftPosition: this.leftPosition(), topPosition: this.topPosition(), height: this.height(), width: this.width(), lctFlag: this.lctFlag(), interlacedFlag: this.interlacedFlag(), lctSortedFlag: this.lctSortedFlag(), lctSize: this.lctSize(), } } public leftPosition(leftPos?: number) { if (typeof leftPos !== "undefined") { this.data![1] = leftPos & 0b11111111 this.data![2] = (leftPos >> 8) & 0b11111111 } return this.data![1] | (this.data![2] << 8) } public topPosition(topPos?: number) { if (typeof topPos !== "undefined") { this.data![3] = topPos & 0b11111111 this.data![4] = (topPos >> 8) & 0b11111111 } return this.data![3] | (this.data![4] << 8) } public width(width?: number) { if (typeof width !== "undefined") { this.data![5] = width & 0b11111111 this.data![6] = (width >> 8) & 0b11111111 } return this.data![5] | (this.data![6] << 8) } public height(height?: number) { if (typeof height !== "undefined") { this.data![7] = height & 0b11111111 this.data![8] = (height >> 8) & 0b11111111 } return this.data![7] | (this.data![8] << 8) } public lctFlag(flag?: boolean) { if (typeof flag !== "undefined") { this.data![9] = flag ? this.data![9] | 0b10000000 : this.data![9] & ~0b10000000 } return (this.data![9] & 0b10000000) > 0 ? true : false } public interlacedFlag(flag?: boolean) { if (typeof flag !== "undefined") { this.data![9] = flag ? this.data![9] | 0b01000000 : this.data![9] & ~0b01000000 } return (this.data![9] & 0b01000000) > 0 ? true : false } public lctSortedFlag(flag?: boolean) { if (typeof flag !== "undefined") { this.data![9] = flag ? this.data![9] | 0b00100000 : this.data![9] & ~0b00100000 } return (this.data![9] & 0b00100000) > 0 ? true : false } public lctSize(lctSize?: number) { if (typeof lctSize !== "undefined") { this.data![9] = (this.data![9] & 0b11111000) | (lctSize & 0b111) } return this.data![9] & 0b111 } } class GraphicControlExtension extends (GifBlock) { clone(): GraphicControlExtension { return new GraphicControlExtension({}, -1, Uint8Array.from(this.data)) } constructor(props: GraphicControlExtensionProperties, blockID?: number, data?: Uint8Array) { if (typeof data !== "undefined") { super(BlockType.GraphicControlExtension, blockID, data) } else { const newData = new Uint8Array(BlockLenghts.GRAPHICCONTROLEXTENSION) super(BlockType.GraphicControlExtension, blockID, newData) this.data[0] = 0x21 //extrension introducer this.data[1] = 0xF9 //extension label this.data[2] = 4 // lenght this.disposalMethod(props.disposalMethod) this.userInputFlag(props.userInputFlag) this.transparencyFlag(props.transparencyFlag) this.delay(props.delay) this.transparencyIndex(props.transparencyIndex) this.data[7] = 0 } } public disposalMethod(method?: DisposalMethod) { if (typeof method !== "undefined") { this.data![3] = (this.data![3] & 0b11100011) | ((method & 0b111) << 2) } return ((this.data![3] >> 2) & 0b111) as DisposalMethod } public userInputFlag(flag?: boolean) { if (typeof flag !== "undefined") { this.data![3] = flag ? this.data![3] | 0b00000010 : this.data![3] & 0b11111101 } return (this.data![3] & 0b00000010) > 0 ? true : false } public transparencyFlag(flag?: boolean) { if (typeof flag !== "undefined") { this.data![3] = flag ? this.data![3] | 0b00000001 : this.data![3] & 0b11111110 } return (this.data![3] & 0b00000001) > 0 ? true : false } public delay(delay?: number) { if (typeof delay !== "undefined") { this.data![4] = delay & 0b11111111 this.data![5] = (delay >> 8) & 0b11111111 } return this.data![4] | (this.data![5] << 8) } public transparencyIndex(index?: number) { if (typeof index !== "undefined") { this.data![6] = index } return this.data![6] } } class LogicalScreenDescriptor extends GifBlock { clone(): LogicalScreenDescriptor { return new LogicalScreenDescriptor({}, this.blockID, Uint8Array.from(this.data)) } constructor(props: LogicalScreenDescriptorProperties, blockID?: number, data?: Uint8Array) { if (typeof data !== "undefined") { super(BlockType.LogicalScreenDescriptor, blockID, data) } else { const newData = new Uint8Array(BlockLenghts.LOGICALSCREENDESCRIPTOR) super(BlockType.LogicalScreenDescriptor, blockID, newData) this.screenWidth(props.screenWidth) this.screenHeight(props.screenHeight) this.gctFlag(props.gctFlag) this.colorResolution(props.colorResolution) this.gctSortedFlag(props.gctSortedFlag) this.gctSize(props.gctSize) this.backgroundColorIndex(props.backgroundColorIndex) this.pixelAspectRatio(props.pixelAspectRatio) } } public toProps(): LogicalScreenDescriptorProperties { return { screenWidth: this.screenWidth(), screenHeight: this.screenHeight(), gctFlag: this.gctFlag(), colorResolution: this.colorResolution(), gctSortedFlag: this.gctSortedFlag(), gctSize: this.gctSize(), backgroundColorIndex: this.backgroundColorIndex(), pixelAspectRatio: this.pixelAspectRatio() } } public screenWidth(width?: number) { if (typeof width !== "undefined") { this.data![0] = width & 0b11111111 this.data![1] = (width >> 8) & 0b11111111 } return this.data![0] | (this.data![1] << 8) } public screenHeight(height?: number) { if (typeof height !== "undefined") { this.data![2] = height & 0b11111111 this.data![3] = (height >> 8) & 0b11111111 } return this.data![2] | (this.data![3] << 8) } public gctFlag(flag?: boolean) { if (typeof flag !== "undefined") { this.data![4] = flag ? this.data![4] | 0b10000000 : this.data![4] & ~0b10000000 } return (this.data![4] & 0b10000000) > 0 ? true : false } public colorResolution(resolution?: number) { if (typeof resolution !== "undefined") { this.data![4] = (this.data![4] & ~0b01110000) | ((resolution & 0b111) << 4) } return (this.data![4] & 0b01110000) >> 4 } public gctSortedFlag(flag?: boolean) { if (typeof flag !== "undefined") { this.data![4] = flag ? this.data![4] | 0b00001000 : this.data![4] & ~0b00001000 } return (this.data![4] & 0b00001000) > 0 ? true : false } public gctSize(size?: number) { if (typeof size !== "undefined") { this.data![4] = (this.data![4] & ~0b00000111) | (size & 0b00000111) } return (this.data![4] & 0b00000111) } public backgroundColorIndex(index?: number) { if (typeof index !== "undefined") { this.data![5] = index } return this.data![5] } // Aspect Ratio = (Pixel Aspect Ratio + 15) / 64 public pixelAspectRatio(ratio?: number) { if (typeof ratio !== "undefined") { this.data![6] = ratio } return this.data![6] } } class ColorTable extends GifBlock { clone(): ColorTable { return new ColorTable(this.toProps(), this.blockID) } /** * Because a color tables colors are all of its data there is no extra parameter for the data * @param props type and colors need to be set. * @param blockID */ constructor(props: ColorTableProperties, blockID?: number) { if (props.colors!.length % 3 !== 0) { throw new Error("Not viable props.colors.lenght. Colortables need to have three entries per color.") } const newData = Uint8Array.from(props.colors!) super(props.type!, blockID, newData) } public toProps(): ColorTableProperties { return { type: this.blockType === BlockType.LocalColorTable ? BlockType.LocalColorTable : BlockType.GlobalColorTable, colors: Uint8Array.from(this.data) } } // returns how many colors are in the colortable public getColorAmount() { return this.data.length / 3 } public getColor(index: number) { return [this.data[index * 3], this.data[index * 3 + 1], this.data[index * 3 + 2]] } setColor(index: number, rgb: [number, number, number]) { if (index > this.data.length / 3) { return; } this.data![index * 3] = rgb[0] this.data![index * 3 + 1] = rgb[1] this.data![index * 3 + 2] = rgb[2] } setColors(colors: number[]) { this.data! = Uint8Array.from(colors) } /** * Returns the amount of bits needed to represent the amount of colors */ getBitAmount() { return Math.ceil(Math.log2(this.getColorAmount())) } } class Header extends GifBlock { clone(): Header { throw new Error("This Block should never be cloned.") } constructor(props: HeaderProperties, blockID?: number, data?: Uint8Array) { if (typeof data !== "undefined") { super(BlockType.Header, blockID, data) } else { const newData = new Uint8Array(BlockLenghts.HEADER) super(BlockType.Header, blockID, newData) this.data[0] = "G".charCodeAt(0) this.data[1] = "I".charCodeAt(0) this.data[2] = "F".charCodeAt(0) this.version(props.version) } } public version(version?: ("87a" | "89a")) { if (typeof version !== "undefined") { this.data[3] = version.charCodeAt(0) this.data[4] = version.charCodeAt(1) this.data[5] = version.charCodeAt(2) } return String.fromCharCode(...this.data.slice(3)) } } class Trailer extends GifBlock { clone(): Trailer { throw new Error("This Block should never be cloned") } constructor(blockID?: number) { const newData = Uint8Array.from([0x3B]) super(BlockType.Trailer, blockID, newData) } } class ApplicationExtension extends GifBlock { clone(): ApplicationExtension { return new ApplicationExtension(-1, Uint8Array.from(this.data)) } constructor(blockID?: number, data?: Uint8Array) { if (typeof data !== "undefined") { super(BlockType.ApplicationExtension, blockID, data) } else { const newData = new Uint8Array(15) newData[0] = 0x21 newData[1] = 0xFF newData[2] = 11 // block size newData[14] = 0 super(BlockType.ApplicationExtension, blockID, newData) } } identifier(identifier?: string) { if (typeof identifier !== "undefined" && identifier.length >= 8) { for (let index = 0; index < 8; index++) { this.data[3 + index] = identifier.charCodeAt(index); } } return String.fromCharCode(...this.data.slice(3, 11)) } authentification(auth?: [number, number, number]) { if (typeof auth !== "undefined") { this.data[11] = auth[0] this.data[12] = auth[1] this.data[13] = auth[2] } return this.data.slice(11, 14) } getApplicationdata() { return (getDataFromSubDataBlock(this.data.slice(14))) } setApplicationdata(data: number[]) { this.data = Uint8Array.from(Array.from(this.data.slice(0, 14)).concat(putDatatIntoSubdataBlocks(data))) } } /** * Represents a specific Applicationblock, defined by netscape to allow the looping of gifs. * */ class NetscapeLoopApplication extends ApplicationExtension { //https://web.archive.org/web/19990418091037/http://www6.uniovi.es/gifanim/gifabout.htm private static header = [0x21, 0xFF, 11, 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48] // "NETSCAPE" "2.0" /** * always initializes with an infinite loop */ constructor(blockID?: number) { const newData = [0x21, 0xFF, 11, 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48, 3, 1, 0, 0, 0] super(blockID, Uint8Array.from(newData)) } setLoops(loops: number) { this.data[16] = loops & 7 this.data[17] = (loops >> 8) & 7 } getLoops() { return this.data[16] | (this.data[17] << 8) } } class TableBasedImageData extends GifBlock { clone(): TableBasedImageData { const newtbid = new TableBasedImageData({}, -1, Uint8Array.from(this.data)) //newtbid.setDryImage(this.getDryImage()) return newtbid } private dryImage: number[] | undefined; constructor(props: TableBasedImageDataProperties, blockID?: number, data?: Uint8Array) { if (typeof data !== "undefined") { super(BlockType.TableBasedImageData, blockID, data) } else { let newData = [props.lzwMinSize!] newData = newData.concat(putDatatIntoSubdataBlocks(props.imageData!)) super(BlockType.TableBasedImageData, blockID, Uint8Array.from(newData)) } this.dryImage = undefined } LZWMinimumCodeSize(minCodeSize?: number) { if (typeof minCodeSize !== "undefined") { this.data[0] = minCodeSize } return this.data[0] } /** * @returns The compressed image data */ getImageData() { return getDataFromSubDataBlock(this.data.slice(1)) } /** * Takes a freshly compressed gif image and puts it inot the tbids subdata blocks * @param imageData compressed image data */ setImageData(imageData: number[]) { const newData = [this.data[0]] this.data = Uint8Array.from(newData.concat(putDatatIntoSubdataBlocks(imageData))) } /** * Reencodes data so it uses another number of symbols as base vocabulary. * On reencoding any base codes that are too big for the new vocabulary will be replaced with 'code%targetLZWMinCodeSize' * @param targetLZWMinCodeSize The amount of bits available for the base vocabulary. Has to be between 2 and 12 (sizes from the gif standard) */ reencode(targetLZWMinCodeSize: number) { targetLZWMinCodeSize = Math.min(Math.max(targetLZWMinCodeSize, 2), 12) const compressor = new Compressor() let indexstream = decompressor.decompressTableBasedImageData(this) const maxVal = (1 << (targetLZWMinCodeSize)) indexstream = indexstream.map( (value) => value >= maxVal ? value % maxVal : value ) this.setImageData(compressor.compress(indexstream, targetLZWMinCodeSize)) this.LZWMinimumCodeSize(targetLZWMinCodeSize) } /** * Returns the decompressed index sequence * Could be used for caching */ getDryImage() { return decompressor.decompressTableBasedImageData(this); //This is a bad idea in terms of memory... /** if (typeof this.dryImage === "undefined") { this.dryImage = decompressor.decompressTableBasedImageData(this) } return this.dryImage*/ } } class CommentExtension extends GifBlock { clone(): CommentExtension { return new CommentExtension(-1, Uint8Array.from(this.data)) } constructor(blockID?: number, data?: Uint8Array) { if (typeof data !== "undefined") { super(BlockType.CommentExtension, blockID, data) } else { const newData = Uint8Array.from([0x21, 0xFE, 0x00]) super(BlockType.CommentExtension, blockID, newData) } } public setComment(commentmessage: string) { const comment = [ 0x21, // extension introducer 0xFE, // Comment Label ]; const commentdata = [] for (let char of commentmessage) { commentdata.push(char.charCodeAt(0)) } this.data = Uint8Array.from(comment.concat(putDatatIntoSubdataBlocks(commentdata))) } public getComment() { return String.fromCharCode(...getDataFromSubDataBlock(this.data.slice(2))) } } class PlainTextExtension extends GifBlock { clone(): GifBlock { return new PlainTextExtension({}, -1, Uint8Array.from(this.data)) } constructor(props: PlainTextExtensionProperties, blockID?: number, data?: Uint8Array) { if (typeof data !== "undefined") { super(BlockType.PlainTextExtension, blockID, data) } else { const newData = new Uint8Array(16) newData[0] = 0x21 newData[1] = 0x01 newData[2] = 12 // block size for (let i = 3; i < 15; i++) { newData[i] = 0; } newData[15] = 0 //TODO use the props here to set the values super(BlockType.PlainTextExtension, blockID, newData) this.gridTopPosition(props.topos) this.gridLeftPosition(props.leftpos) this.gridWidth(props.gridwidth) this.gridHeight(props.gridheight) this.cellWidth(props.cellwidth) this.cellHeight(props.cellheight) this.textForegorundColorIndex(props.foregroundindex) this.textBackgroundColorIndex(props.backgroundindex) this.textBackgroundColorIndex(props.backgroundindex) if (props.plaintextdata) { this.setPlainTextData(props.plaintextdata) } } } gridLeftPosition(leftpos?: number) { if (typeof leftpos !== "undefined") { this.data[3] = leftpos & 0b11111111 this.data[4] = (leftpos >> 8) & 0b11111111 } return this.data[3] | (this.data[4] << 8) } gridTopPosition(toppos?: number) { if (typeof toppos !== "undefined") { this.data[5] = toppos & 0b11111111 this.data[6] = (toppos >> 8) & 0b11111111 } return this.data[5] | (this.data[6] << 8) } gridWidth(width?: number) { if (typeof width !== "undefined") { this.data[7] = width & 0b11111111 this.data[8] = (width >> 8) & 0b11111111 } return this.data[7] | (this.data[8] << 8) } gridHeight(height?: number) { if (typeof height !== "undefined") { this.data[9] = height & 0b11111111 this.data[10] = (height >> 8) & 0b11111111 } return this.data[9] | (this.data[10] << 8) } cellWidth(width?: number) { if (typeof width !== "undefined") { this.data[11] = width } return this.data[11] } cellHeight(height?: number) { if (typeof height !== "undefined") { this.data[12] = height } return this.data[12] } textForegorundColorIndex(index?: number) { if (typeof index !== "undefined") { this.data[13] = index } return this.data[13] } textBackgroundColorIndex(index?: number) { if (typeof index !== "undefined") { this.data[14] = index } return this.data[14] } getPlainTextData() { return String.fromCharCode(...getDataFromSubDataBlock(this.data.slice(15,))) } setPlainTextData(plaintext: string) { const plaintextcodes = [] for (let char of plaintext) { plaintextcodes.push(char.charCodeAt(0)) } const newData = Array.from(this.data.slice(0, 15)) this.data = new Uint8Array(newData.concat(putDatatIntoSubdataBlocks(plaintextcodes))) } } // Types to make our life easier /** * Type to bundle all possible data for a single gif image. Contains at least the image descriptor and table based image data */ type GifBlockImage = { graphicextension?: GraphicControlExtension; imagedescriptor: ImageDescriptor; colortable: ColorTable; tableBasedImageData: TableBasedImageData; } /** * type to bundle all data related to a plaintext extension. */ type GifBlockPlainText = { graphicextension?: GraphicControlExtension; globalColortable: ColorTable; plainText: PlainTextExtension; } /** * type to bundle all data related to the meta datat of a gif. */ type GifBlockMeta = { header: Header; logicalScreen: LogicalScreenDescriptor; globalColorTable?: ColorTable; } /** * Lenght of all fixed length blocks */ enum BlockLenghts { HEADER = 6, LOGICALSCREENDESCRIPTOR = 7, IMAGEDESCRIPTOR = 10, GRAPHICCONTROLEXTENSION = 8, TRAILER = 1, } /** * All different types of blocks. */ enum BlockType { None, Header, LogicalScreenDescriptor, GlobalColorTable, TableBasedImageData, ImageDescriptor, LocalColorTable, Trailer, GraphicControlExtension, CommentExtension, PlainTextExtension, ApplicationExtension, ColorTable, } /** * Different integer values of disposal methods that graphic control extensions use */ enum DisposalMethod { NoDisposalDefined, DoNotDispose, RestoreToBackgroundColor, RestoreToPrevious, }; /** * jsut a collection of things we might wanna keep around. Might not be a good idea to do it like this tho. */ type siteState = { currBlock: number; // where we are in terms of "full" blocks maxBlockCount: number; blocksperpage: number; currFileName?: string defaultColortable: ColorTable tempSaved?: GifBlockImage | GifBlock | GifBlockPlainText //TODO allow copy and paste of blocks } /** * Class representing a parsed gif in memory. Holds the gif blocks and allows access and manpilaution of them and their ordering. */ class GifView { //we cut the gif's blob into finer blocks and store these public blocks: Array; private lastID = 0; //id for new blocks public logicalScreen: LogicalScreenDescriptor | undefined public globalColorTable: ColorTable | undefined /** * Creates a new GifView with a Header, LSD, GCT and Trailer. * @returns a fresh GifView object */ static createBlankGif() { const mgifview = new GifView() const lsd = new LogicalScreenDescriptor({ screenWidth: 500, screenHeight: 250, gctFlag: true, gctSize: 7, gctSortedFlag: false, colorResolution: 7, backgroundColorIndex: 1, pixelAspectRatio: 0 }); const gct = new ColorTable({ type: BlockType.GlobalColorTable, colors: BlockAdder.makeRGBColor() }); const blanks = [ new Header({ version: "89a" }), lsd, gct, new Trailer() ] mgifview.addBlocks(blanks, 0) mgifview.logicalScreen = lsd; mgifview.globalColorTable = gct; return mgifview } /** * Creates a new GifView. If any gif data is supplied it will be parsed * @param buffer gif data (like a file's ArrayBuffer) that should be parsed into blocks */ public constructor(buffer?: ArrayBuffer | Uint8Array) { this.blocks = new Array(); if (buffer && buffer.byteLength > 0) { this.parseGifToBlockPointers(new Uint8Array(buffer)); } } /** * Parses the given Uint8Array and puts the data in neat little boxes. Should probably be async but meh. * @param gifBlob Uint8Array comming from wherever, has to hold the bytes of a valid gif */ private parseGifToBlockPointers(gifBlob: Uint8Array) { /** * 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 * @param position */ const parseSubData = function (position: number): number { let blockLenght = gifBlob[position]; while (blockLenght !== 0 && position + blockLenght < gifBlob.length) { position = position + blockLenght + 1; // go to next block start. blockLenght = gifBlob[position]; } if (position + blockLenght >= gifBlob.length) { console.log("parsing error") //TODO throw an error insetad of returning -1 return -1; } return position; } let position = 0; //where in the data are we at the moment 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. // header this.blocks.push(new Header({}, this.lastID, gifBlob.slice(0, position + BlockLenghts.HEADER))); this.lastID++ position = position + BlockLenghts.HEADER; // LSD + optional GCT this.logicalScreen = new LogicalScreenDescriptor({}, this.lastID, gifBlob.slice(position, position + BlockLenghts.LOGICALSCREENDESCRIPTOR)) this.blocks.push(this.logicalScreen); this.lastID++ if (this.logicalScreen.gctFlag()) { // GCT present let gctlen = 3 * (1 << ((gifBlob[position + 4] & 0b111) + 1)) position = position + BlockLenghts.LOGICALSCREENDESCRIPTOR this.globalColorTable = new ColorTable({ type: BlockType.GlobalColorTable, colors: gifBlob.slice(position, position + gctlen) }, this.lastID) this.blocks.push(this.globalColorTable); position = position + gctlen; this.lastID++ } else { position = position + BlockLenghts.LOGICALSCREENDESCRIPTOR } while (dataleft) { switch (gifBlob[position]) { //for now only gce and application extension case 0x21: // we have an extension block: if (gifBlob[position + 1] && gifBlob[position + 1] == 0xF9) { //Graphic control extension this.blocks.push(new GraphicControlExtension( {}, this.lastID, gifBlob.slice(position, position + BlockLenghts.GRAPHICCONTROLEXTENSION) )); this.lastID++ position = position + BlockLenghts.GRAPHICCONTROLEXTENSION; } else if (gifBlob[position + 1] && gifBlob[position + 1] == 0xFF) { //Application extension let applicationDataStart = position + 14; let end = parseSubData(applicationDataStart); if (end < 0) { console.log("wtf, problem at byte " + applicationDataStart); dataleft = false; break; } else { this.blocks.push(new ApplicationExtension(this.lastID, gifBlob.slice(position, end + 1))); this.lastID++ } position = end + 1; //skips behind the block terminator } else if (gifBlob[position + 1] && gifBlob[position + 1] == 0xFE) { //Comment Extension let commentDataStart = position + 2; let end = parseSubData(commentDataStart); if (end < 0) { console.log("wtf, problem at byte " + commentDataStart); dataleft = false; break; } else { this.blocks.push(new CommentExtension(this.lastID, gifBlob.slice(position, end + 1))); this.lastID++; } position = end + 1; } else if (gifBlob[position + 1] && gifBlob[position + 1] == 0x01) { //PlainText Extension let plaintextDataStart = position + 15; let end = parseSubData(plaintextDataStart); if (end < 0) { console.log("wtf, problem at byte " + plaintextDataStart); dataleft = false; break; } else { this.blocks.push(new PlainTextExtension({}, this.lastID, gifBlob.slice(position, end + 1))); this.lastID++ } position = end + 1; } else { console.log("Extension currently not supported. Aborting parsing"); dataleft = false; } break; case 0x2C: // we have an image descriptor so we parse the whole image //image descriptor const imgdscr = new ImageDescriptor( {}, this.lastID, gifBlob.slice(position, position + BlockLenghts.IMAGEDESCRIPTOR)) this.blocks.push(imgdscr); this.lastID++ if (imgdscr.lctFlag()) { //handle local colortable if one exists let ctlen = 3 * (1 << (imgdscr.lctSize() + 1)); position = position + BlockLenghts.IMAGEDESCRIPTOR; this.blocks.push(new ColorTable( { type: BlockType.LocalColorTable, colors: gifBlob.slice(position, position + ctlen) }, this.lastID )) this.lastID++ position = position + ctlen; } else { position = position + BlockLenghts.IMAGEDESCRIPTOR; } //handle tbid const start = position; position = position + 1; // skip the min lzw size byte and point to the first subdata block const end = parseSubData(position); if (end < 0) { console.log("wtf, problem at byte " + position + " with value " + gifBlob[position]); dataleft = false; break; } else { this.blocks.push(new TableBasedImageData({}, this.lastID, gifBlob.slice(start, end + 1))) this.lastID++ } position = end + 1; //end points to the last byte of the tbid so we go one further. break; case 0x3B: // end of data this.blocks.push(new Trailer(this.lastID)) this.lastID++ dataleft = false; break; default: console.log("wtf, problem at byte " + position + " with value " + gifBlob[position]); dataleft = false; break; } } this.lastID++ // just to be sure we don 't give the same id twice console.log("Finished populating blocks.") } /** * Returns the index of a GifBlock in the block array based on the ID. If no such block is found, -1 is returned. */ getBlockPosition(block: GifBlock) { return this.blocks.findIndex((value) => value.blockID == block.blockID); } /** * Returns the first block from the block array with the given id. Undefined is returned if no block was found */ getBlockFromID(id: number) { return this.blocks.find((value) => value.blockID == id); } /** * Returns all blocks from the block array of a given kind */ public getBlocksOfType(type: BlockType) { return this.blocks.filter((x) => x.blockType == type); } /** * Exports the internal data to a Blob with type 'image/gif' */ public async exportToBlob() { const parts = [] for (let index = 0; index < this.blocks.length; index++) { if (typeof this.blocks[index].data !== "undefined") { parts.push(this.blocks[index].data) } else { throw new Error(`Block ${this.blocks[index].blockID} with undefined data in gif.`) } } return new Blob(parts, { type: "image/gif" }) } /** * Same as {@link getBlockPosition} but uses the id directly instead of a block */ getBlockPositionFromID(blockID: number) { return this.blocks.findIndex((value) => value.blockID == blockID) } /** * Add new blocks into the gifview. The blocks blockID will be overwritten by this function. * @param newBlocks new blocks to be addedd * @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 */ public addBlocks(newBlocks: GifBlock[], position: number) { for (let block of newBlocks) { block.blockID = this.lastID; this.lastID++; } this.blocks.splice(position + 1, 0, ...newBlocks) } /** * Removes the first blocks with the given ids from the block array */ public removeBlocks(idsToRemove: number[]) { const positionsToRemove = [] for (let bID of idsToRemove) { const index = this.blocks.findIndex((value) => value.blockID === bID) if (index >= 0) { positionsToRemove.push(index) } } positionsToRemove.sort((a, b) => b - a) // descending order so we can splice without errors because of array lenght changes for (let pos of positionsToRemove) { this.blocks.splice(pos, 1) } } /** * Reencodes all images such that their lzwminocde fits the given colortable size. * Can be used to make the final gif a bit smaller */ public recodeImages() { const meta = this.bundleMeta()! for (let block of this.blocks) { if (block.blockType === BlockType.TableBasedImageData) { const bundle = this.bundleImage(block)! let newSize = (block as TableBasedImageData).LZWMinimumCodeSize(); if (bundle.imagedescriptor.lctFlag()) { newSize = bundle.imagedescriptor.lctSize() + 1 } else if (meta.logicalScreen.gctFlag()) { newSize = meta.logicalScreen.gctSize() + 1 } if (newSize !== (block as TableBasedImageData).LZWMinimumCodeSize()) { (block as TableBasedImageData).reencode(newSize) } } } } /** * Gathers datat related to an image and returns it. * The image is the first one based on TableBasedImageData to right or equals to the contextBlock */ public bundleImage(contextBlock: GifBlock): GifBlockImage | undefined { let currBlockIndex = this.getBlockPosition(contextBlock); if (currBlockIndex == -1) { return undefined; } let tbid: TableBasedImageData; let controlextension: GraphicControlExtension | undefined; let colortable: ColorTable; let imgdescriptor: ImageDescriptor; let tbidIndex = -1; // we check indecies to the right, so that we always catch the tbid associated with any image related block we have while (currBlockIndex < this.blocks.length) { if (this.blocks[currBlockIndex].blockType === BlockType.TableBasedImageData) { tbidIndex = currBlockIndex; break; } currBlockIndex++ } if (tbidIndex == -1) { return undefined; } tbid = this.blocks[tbidIndex] as TableBasedImageData //now we walk backwards let imgdescriptorIndex = -1; while (currBlockIndex > 0) { if (this.blocks[currBlockIndex].blockType == BlockType.ImageDescriptor) { imgdescriptorIndex = currBlockIndex; break; } currBlockIndex-- } if (imgdescriptorIndex == -1) { throw new Error("No Image Descriptor for available") return undefined; } imgdescriptor = this.blocks[imgdescriptorIndex] as ImageDescriptor if (imgdescriptor.lctFlag()) { colortable = this.blocks[imgdescriptorIndex + 1] as ColorTable } else if (this.globalColorTable) { colortable = this.globalColorTable as ColorTable } else { colortable = state.defaultColortable // for any gif that has neither gct nor lct } let controlIndex = -1; while (currBlockIndex > 0) { if (this.blocks[currBlockIndex].blockType == BlockType.GraphicControlExtension) { controlIndex = currBlockIndex; break; } else if (this.blocks[currBlockIndex].blockType == BlockType.TableBasedImageData || this.blocks[currBlockIndex].blockType == BlockType.PlainTextExtension) { break; } currBlockIndex--; } if (controlIndex > 0) { controlextension = this.blocks[controlIndex] as GraphicControlExtension } //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 if (contextBlock.blockType == BlockType.GlobalColorTable) { colortable = contextBlock as ColorTable; } const gifimage: GifBlockImage = { graphicextension: controlextension, imagedescriptor: imgdescriptor, tableBasedImageData: tbid, colortable: colortable }; return gifimage; } /** * Gathers datat related to a plain text extension and returns it. */ public bundlePlainText(blockPointer: PlainTextExtension): undefined | GifBlockPlainText { let currBlockIndex = this.getBlockPositionFromID(blockPointer.blockID) if (currBlockIndex < 0 || this.blocks[2].blockType !== BlockType.GlobalColorTable) { return undefined } currBlockIndex-- let gceIndex = -1 while (currBlockIndex > 0) { if (this.blocks[currBlockIndex].blockType === BlockType.GraphicControlExtension) { gceIndex = currBlockIndex break; } else if (this.blocks[currBlockIndex].blockType === BlockType.TableBasedImageData || this.blocks[currBlockIndex].blockType === BlockType.PlainTextExtension) { break; } currBlockIndex-- } let gce; if (gceIndex > 0) { gce = this.blocks[gceIndex] as GraphicControlExtension } let bundle: GifBlockPlainText = { globalColortable: this.blocks[2] as ColorTable, plainText: blockPointer, graphicextension: gce, }; return bundle } /** * Function to retrieve the Header, Logical Screen, and optional Global Colortable of the gif. */ public bundleMeta(): GifBlockMeta { const header = this.blocks[0] as Header const lsd = this.blocks[1] as LogicalScreenDescriptor let gct = undefined if (lsd.gctFlag()) { gct = this.blocks[2] as ColorTable } return { header: header, logicalScreen: lsd, globalColorTable: gct } } }