Code for a in-browser gif editor project
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}