浏览代码

Fix GIF rendering issue

Remove the GIF parsing code from this repo and instead add a dependency on gifuct-js.
JC Brand 2 年之前
父节点
当前提交
a94c45af2d
共有 9 个文件被更改,包括 112 次插入596 次删除
  1. 2 2
      dev.html
  2. 14 0
      package-lock.json
  3. 1 0
      package.json
  4. 1 2
      src/shared/components/gif.js
  5. 8 0
      src/shared/components/image.js
  6. 81 268
      src/shared/gif/index.js
  7. 0 319
      src/shared/gif/utils.js
  8. 0 5
      src/shared/rich-text.js
  9. 5 0
      tsconfig.json

+ 2 - 2
dev.html

@@ -40,8 +40,8 @@
         muc_show_logs_before_join: true,
         muc_show_logs_before_join: true,
         notify_all_room_messages: ['discuss@conference.conversejs.org'],
         notify_all_room_messages: ['discuss@conference.conversejs.org'],
         view_mode: 'fullscreen',
         view_mode: 'fullscreen',
-        websocket_url: 'wss://conversejs.org/xmpp-websocket',
-        // websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
+        // websocket_url: 'wss://conversejs.org/xmpp-websocket',
+        websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
         whitelisted_plugins: ['converse-debug'],
         whitelisted_plugins: ['converse-debug'],
         // connection_options: { worker: '/dist/shared-connection-worker.js' }
         // connection_options: { worker: '/dist/shared-connection-worker.js' }
     });
     });

+ 14 - 0
package-lock.json

@@ -20,6 +20,7 @@
         "dayjs": "^1.11.8",
         "dayjs": "^1.11.8",
         "dompurify": "^2.3.1",
         "dompurify": "^2.3.1",
         "favico.js-slevomat": "^0.3.11",
         "favico.js-slevomat": "^0.3.11",
+        "gifuct-js": "^2.1.2",
         "jed": "1.1.1",
         "jed": "1.1.1",
         "lit": "^2.4.0",
         "lit": "^2.4.0",
         "localforage-webextensionstorage-driver": "^3.0.0",
         "localforage-webextensionstorage-driver": "^3.0.0",
@@ -5611,6 +5612,14 @@
         "safe-buffer": "^5.1.1"
         "safe-buffer": "^5.1.1"
       }
       }
     },
     },
+    "node_modules/gifuct-js": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz",
+      "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==",
+      "dependencies": {
+        "js-binary-schema-parser": "^2.0.3"
+      }
+    },
     "node_modules/glob": {
     "node_modules/glob": {
       "version": "7.2.3",
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -6631,6 +6640,11 @@
       "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
       "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
       "peer": true
       "peer": true
     },
     },
+    "node_modules/js-binary-schema-parser": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz",
+      "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg=="
+    },
     "node_modules/js-tokens": {
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

+ 1 - 0
package.json

@@ -120,6 +120,7 @@
     "dayjs": "^1.11.8",
     "dayjs": "^1.11.8",
     "dompurify": "^2.3.1",
     "dompurify": "^2.3.1",
     "favico.js-slevomat": "^0.3.11",
     "favico.js-slevomat": "^0.3.11",
+    "gifuct-js": "^2.1.2",
     "jed": "1.1.1",
     "jed": "1.1.1",
     "lit": "^2.4.0",
     "lit": "^2.4.0",
     "localforage-webextensionstorage-driver": "^3.0.0",
     "localforage-webextensionstorage-driver": "^3.0.0",

+ 1 - 2
src/shared/components/gif.js

@@ -28,6 +28,7 @@ export default class ConverseGIFElement extends CustomElement {
 
 
     constructor () {
     constructor () {
         super();
         super();
+        this.src = null;
         this.autoplay = false;
         this.autoplay = false;
         this.noloop = false;
         this.noloop = false;
         this.fallback = 'url';
         this.fallback = 'url';
@@ -90,8 +91,6 @@ export default class ConverseGIFElement extends CustomElement {
 
 
     onControlsClicked (ev) {
     onControlsClicked (ev) {
         ev.preventDefault();
         ev.preventDefault();
-
-
         if (this.supergif.playing) {
         if (this.supergif.playing) {
             this.supergif.pause();
             this.supergif.pause();
         } else if (this.supergif.frames.length > 0) {
         } else if (this.supergif.frames.length > 0) {

+ 8 - 0
src/shared/components/image.js

@@ -16,6 +16,14 @@ export default class Image extends CustomElement {
         }
         }
     }
     }
 
 
+    constructor () {
+        super();
+        this.src = null;
+        this.href = null;
+        this.onImgClick = null;
+        this.onImgLoad = null;
+    }
+
     render () {
     render () {
         if (isGIFURL(this.src) && shouldRenderMediaFromURL(this.src, 'image')) {
         if (isGIFURL(this.src) && shouldRenderMediaFromURL(this.src, 'image')) {
             return tplGif(filterQueryParamsFromURL(this.src), true);
             return tplGif(filterQueryParamsFromURL(this.src), true);

+ 81 - 268
src/shared/gif/index.js

@@ -1,22 +1,10 @@
-/**
- * @copyright Shachaf Ben-Kiki, JC Brand
- * @description
- *  Started as a fork of Shachaf Ben-Kiki's jsgif library
- *  https://github.com/shachaf/jsgif
- * @license MIT License
- */
-import Stream from './stream.js';
 import { getOpenPromise } from '@converse/openpromise';
 import { getOpenPromise } from '@converse/openpromise';
-import { parseGIF } from './utils.js';
-
-const DELAY_FACTOR = 10;
-
+import { parseGIF, decompressFrames } from 'gifuct-js';
 
 
 export default class ConverseGif {
 export default class ConverseGif {
-
     /**
     /**
      * Creates a new ConverseGif instance
      * Creates a new ConverseGif instance
-     * @param { HTMLElement } el
+     * @param { import('lit').LitElement } el
      * @param { Object } [options]
      * @param { Object } [options]
      * @param { Number } [options.width] - The width, in pixels, of the canvas
      * @param { Number } [options.width] - The width, in pixels, of the canvas
      * @param { Number } [options.height] - The height, in pixels, of the canvas
      * @param { Number } [options.height] - The height, in pixels, of the canvas
@@ -30,7 +18,8 @@ export default class ConverseGif {
      * @param { Number } [options.progress_bar_height=5]
      * @param { Number } [options.progress_bar_height=5]
      */
      */
     constructor (el, opts) {
     constructor (el, opts) {
-        this.options = Object.assign({
+        this.options = Object.assign(
+            {
                 width: null,
                 width: null,
                 height: null,
                 height: null,
                 autoplay: true,
                 autoplay: true,
@@ -38,7 +27,7 @@ export default class ConverseGif {
                 show_progress_bar: true,
                 show_progress_bar: true,
                 progress_bg_color: 'rgba(0,0,0,0.4)',
                 progress_bg_color: 'rgba(0,0,0,0.4)',
                 progress_color: 'rgba(255,0,22,.8)',
                 progress_color: 'rgba(255,0,22,.8)',
-                progress_bar_height: 5
+                progress_bar_height: 5,
             },
             },
             opts
             opts
         );
         );
@@ -51,21 +40,15 @@ export default class ConverseGif {
         this.offscreenCanvas = document.createElement('canvas');
         this.offscreenCanvas = document.createElement('canvas');
 
 
         this.ctx_scaled = false;
         this.ctx_scaled = false;
-        this.disposal_method = null;
-        this.disposal_restore_from_idx = null;
-        this.frame = null;
-        this.frame_offsets = []; // elements have .x and .y properties
         this.frames = [];
         this.frames = [];
-        this.last_disposal_method = null;
-        this.last_img = null;
         this.load_error = null;
         this.load_error = null;
         this.playing = this.options.autoplay;
         this.playing = this.options.autoplay;
-        this.transparency = null;
-        this.frame_delay = null;
 
 
         this.frame_idx = 0;
         this.frame_idx = 0;
         this.iteration_count = 0;
         this.iteration_count = 0;
         this.start = null;
         this.start = null;
+        this.hovering = null;
+        this.frameImageData = null;
 
 
         this.initialize();
         this.initialize();
     }
     }
@@ -75,7 +58,7 @@ export default class ConverseGif {
             this.setSizes(this.options.width, this.options.height);
             this.setSizes(this.options.width, this.options.height);
         }
         }
         const data = await this.fetchGIF(this.gif_el.src);
         const data = await this.fetchGIF(this.gif_el.src);
-        requestAnimationFrame(() => this.startParsing(data));
+        requestAnimationFrame(() => this.handleGIFResponse(data));
     }
     }
 
 
     initPlayer () {
     initPlayer () {
@@ -90,7 +73,7 @@ export default class ConverseGif {
         this.putFrame(this.frame_idx);
         this.putFrame(this.frame_idx);
 
 
         if (this.options.autoplay) {
         if (this.options.autoplay) {
-            const delay = (this.frames[this.frame_idx]?.delay ?? 0) * DELAY_FACTOR;
+            const delay = (this.frames[this.frame_idx]?.delay ?? 0);
             setTimeout(() => this.play(), delay);
             setTimeout(() => this.play(), delay);
         }
         }
     }
     }
@@ -137,8 +120,8 @@ export default class ConverseGif {
      * `putFrame(0)` needs to be called *before* this method, otherwise the
      * `putFrame(0)` needs to be called *before* this method, otherwise the
      * animation will incorrectly start from frame #1 (this is done in `initPlayer`).
      * animation will incorrectly start from frame #1 (this is done in `initPlayer`).
      *
      *
-     * @param { DOMHighRestTimestamp } timestamp - The timestamp as returned by `requestAnimationFrame`
-     * @param { DOMHighRestTimestamp } previous_timestamp - The timestamp from the previous iteration of this method.
+     * @param { DOMHighResTimeStamp } timestamp - The timestamp as returned by `requestAnimationFrame`
+     * @param { DOMHighResTimeStamp } previous_timestamp - The timestamp from the previous iteration of this method.
      * We need this in order to calculate whether we have waited long enough to
      * We need this in order to calculate whether we have waited long enough to
      * show the next frame.
      * show the next frame.
      * @param { Number } frame_delay - The delay (in 1/100th of a second)
      * @param { Number } frame_delay - The delay (in 1/100th of a second)
@@ -148,10 +131,10 @@ export default class ConverseGif {
         if (!this.playing) {
         if (!this.playing) {
             return;
             return;
         }
         }
-        if ((timestamp - previous_timestamp) < frame_delay) {
+        if (timestamp - previous_timestamp < frame_delay) {
             this.hovering ? this.drawPauseIcon() : this.putFrame(this.frame_idx);
             this.hovering ? this.drawPauseIcon() : this.putFrame(this.frame_idx);
             // We need to wait longer
             // We need to wait longer
-            requestAnimationFrame(ts => this.onAnimationFrame(ts, previous_timestamp, frame_delay));
+            requestAnimationFrame((ts) => this.onAnimationFrame(ts, previous_timestamp, frame_delay));
             return;
             return;
         }
         }
         const next_frame = this.getNextFrameNo();
         const next_frame = this.getNextFrameNo();
@@ -160,8 +143,8 @@ export default class ConverseGif {
         }
         }
         this.frame_idx = next_frame;
         this.frame_idx = next_frame;
         this.putFrame(this.frame_idx);
         this.putFrame(this.frame_idx);
-        const delay = (this.frames[this.frame_idx]?.delay || 8) * DELAY_FACTOR;
-        requestAnimationFrame(ts => this.onAnimationFrame(ts, timestamp, delay));
+        const delay = (this.frames[this.frame_idx]?.delay || 8);
+        requestAnimationFrame((ts) => this.onAnimationFrame(ts, timestamp, delay));
     }
     }
 
 
     setSizes (w, h) {
     setSizes (w, h) {
@@ -175,19 +158,6 @@ export default class ConverseGif {
         this.offscreenCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
         this.offscreenCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
     }
     }
 
 
-    setFrameOffset (frame, offset) {
-        if (!this.frame_offsets[frame]) {
-            this.frame_offsets[frame] = offset;
-            return;
-        }
-        if (typeof offset.x !== 'undefined') {
-            this.frame_offsets[frame].x = offset.x;
-        }
-        if (typeof offset.y !== 'undefined') {
-            this.frame_offsets[frame].y = offset.y;
-        }
-    }
-
     doShowProgress (pos, length, draw) {
     doShowProgress (pos, length, draw) {
         if (draw && this.options.show_progress_bar) {
         if (draw && this.options.show_progress_bar) {
             let height = this.options.progress_bar_height;
             let height = this.options.progress_bar_height;
@@ -207,32 +177,20 @@ export default class ConverseGif {
     /**
     /**
      * Starts parsing the GIF stream data by calling `parseGIF` and passing in
      * Starts parsing the GIF stream data by calling `parseGIF` and passing in
      * a map of handler functions.
      * a map of handler functions.
-     * @param { String } data - The GIF file data, as returned by the server
+     * @param {ArrayBuffer} data - The GIF file data, as returned by the server
      */
      */
-    startParsing (data) {
-        const stream = new Stream(data);
-        /**
-         * @typedef { Object } GIFParserHandlers
-         * A map of callback functions passed `parseGIF`. These functions are
-         * called as various parts of the GIF file format are parsed.
-         * @property { Function } hdr - Callback to handle the GIF header data
-         * @property { Function } gce - Callback to handle the GIF Graphic Control Extension data
-         * @property { Function } com - Callback to handle the comment extension block
-         * @property { Function } img - Callback to handle image data
-         * @property { Function } eof - Callback once the end of file has been reached
-         */
-        const handler = {
-            'hdr': this.withProgress(stream, header => this.handleHeader(header)),
-            'gce': this.withProgress(stream, gce => this.handleGCE(gce)),
-            'com': this.withProgress(stream, ),
-            'img': this.withProgress(stream, img => this.doImg(img), true),
-            'eof': () => this.handleEOF(stream)
-        };
+    handleGIFResponse (data) {
         try {
         try {
-            parseGIF(stream, handler);
+            const gif = parseGIF(data);
+            this.hdr = gif.header;
+            this.lsd = gif.lsd;
+            this.setSizes(this.options.width ?? this.lsd.width, this.options.height ?? this.lsd.height);
+            this.frames = decompressFrames(gif, true);
         } catch (err) {
         } catch (err) {
-            this.showError('parse');
+            this.showError();
         }
         }
+        this.initPlayer();
+        !this.options.autoplay && this.drawPlayIcon();
     }
     }
 
 
     drawError () {
     drawError () {
@@ -240,23 +198,23 @@ export default class ConverseGif {
         this.ctx.fillRect(
         this.ctx.fillRect(
             0,
             0,
             0,
             0,
-            this.options.width ? this.options.width : this.hdr.width,
-            this.options.height ? this.options.height : this.hdr.height
+            this.options.width ? this.options.width : this.lsd.width,
+            this.options.height ? this.options.height : this.lsd.height
         );
         );
         this.ctx.strokeStyle = 'red';
         this.ctx.strokeStyle = 'red';
         this.ctx.lineWidth = 3;
         this.ctx.lineWidth = 3;
         this.ctx.moveTo(0, 0);
         this.ctx.moveTo(0, 0);
         this.ctx.lineTo(
         this.ctx.lineTo(
-            this.options.width ? this.options.width : this.hdr.width,
-            this.options.height ? this.options.height : this.hdr.height
+            this.options.width ? this.options.width : this.lsd.width,
+            this.options.height ? this.options.height : this.lsd.height
         );
         );
-        this.ctx.moveTo(0, this.options.height ? this.options.height : this.hdr.height);
-        this.ctx.lineTo(this.options.width ? this.options.width : this.hdr.width, 0);
+        this.ctx.moveTo(0, this.options.height ? this.options.height : this.lsd.height);
+        this.ctx.lineTo(this.options.width ? this.options.width : this.lsd.width, 0);
         this.ctx.stroke();
         this.ctx.stroke();
     }
     }
 
 
-    showError (errtype) {
-        this.load_error = errtype;
+    showError () {
+        this.load_error = true;
         this.hdr = {
         this.hdr = {
             width: this.gif_el.width,
             width: this.gif_el.width,
             height: this.gif_el.height,
             height: this.gif_el.height,
@@ -266,164 +224,49 @@ export default class ConverseGif {
         this.el.requestUpdate();
         this.el.requestUpdate();
     }
     }
 
 
-    handleHeader (header) {
-        this.hdr = header;
-        this.setSizes(
-            this.options.width ?? this.hdr.width,
-            this.options.height ?? this.hdr.height
-        );
-    }
-
     /**
     /**
-     * Handler for GIF Graphic Control Extension (GCE) data
-     */
-    handleGCE (gce) {
-        this.pushFrame();
-        this.clear();
-        this.frame_delay = gce.delayTime;
-        this.transparency = gce.transparencyGiven ? gce.transparencyIndex : null;
-        this.disposal_method = gce.disposalMethod;
-    }
-
-    /**
-     * Handler for when the end of the GIF's file has been reached
+     * Draws a gif frame at a specific index inside the canvas.
+     * @param {number|string} i - The frame index
      */
      */
-    handleEOF (stream) {
-        this.pushFrame();
-        this.doDecodeProgress(stream, false);
-        this.initPlayer();
-        !this.options.autoplay && this.drawPlayIcon();
-    }
-
-    pushFrame () {
-        if (!this.frame) return;
-        this.frames.push({
-            data: this.frame.getImageData(0, 0, this.hdr.width, this.hdr.height),
-            delay: this.frame_delay
-        });
-        this.frame_offsets.push({ x: 0, y: 0 });
-    }
-
-    doImg (img) {
-        this.frame = this.frame || this.offscreenCanvas.getContext('2d');
-        const currIdx = this.frames.length;
-
-        //ct = color table, gct = global color table
-        const ct = img.lctFlag ? img.lct : this.hdr.gct; // TODO: What if neither exists?
-
-        /*
-         *  Disposal method indicates the way in which the graphic is to
-         *  be treated after being displayed.
-         *
-         *  Values :    0 - No disposal specified. The decoder is
-         *                  not required to take any action.
-         *              1 - Do not dispose. The graphic is to be left
-         *                  in place.
-         *              2 - Restore to background color. The area used by the
-         *                  graphic must be restored to the background color.
-         *              3 - Restore to previous. The decoder is required to
-         *                  restore the area overwritten by the graphic with
-         *                  what was there prior to rendering the graphic.
-         *
-         *                  Importantly, "previous" means the frame state
-         *                  after the last disposal of method 0, 1, or 2.
-         */
-        if (currIdx > 0) {
-            if (this.last_disposal_method === 3) {
-                // Restore to previous
-                // If we disposed every frame including first frame up to this point, then we have
-                // no composited frame to restore to. In this case, restore to background instead.
-                if (this.disposal_restore_from_idx !== null) {
-                    this.frame.putImageData(this.frames[this.disposal_restore_from_idx].data, 0, 0);
-                } else {
-                    this.frame.clearRect(
-                        this.last_img.leftPos,
-                        this.last_img.topPos,
-                        this.last_img.width,
-                        this.last_img.height
-                    );
-                }
-            } else {
-                this.disposal_restore_from_idx = currIdx - 1;
-            }
-
-            if (this.last_disposal_method === 2) {
-                // Restore to background color
-                // Browser implementations historically restore to transparent; we do the same.
-                // http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079
-                this.frame.clearRect(
-                    this.last_img.leftPos,
-                    this.last_img.topPos,
-                    this.last_img.width,
-                    this.last_img.height
-                );
-            }
-        }
-        // else, Undefined/Do not dispose.
-        // frame contains final pixel data from the last frame; do nothing
-
-        //Get existing pixels for img region after applying disposal method
-        const imgData = this.frame.getImageData(img.leftPos, img.topPos, img.width, img.height);
-
-        //apply color table colors
-        img.pixels.forEach((pixel, i) => {
-            // imgData.data === [R,G,B,A,R,G,B,A,...]
-            if (pixel !== this.transparency) {
-                imgData.data[i * 4 + 0] = ct[pixel][0];
-                imgData.data[i * 4 + 1] = ct[pixel][1];
-                imgData.data[i * 4 + 2] = ct[pixel][2];
-                imgData.data[i * 4 + 3] = 255; // Opaque.
-            }
-        });
+    putFrame (i, show_pause_on_hover = true) {
+        if (!this.frames.length) return;
 
 
-        this.frame.putImageData(imgData, img.leftPos, img.topPos);
-
-        if (!this.ctx_scaled) {
-            this.ctx.scale(this.getCanvasScale(), this.getCanvasScale());
-            this.ctx_scaled = true;
+        i = parseInt(i.toString(), 10);
+        if (i > this.frames.length - 1 || i < 0) {
+            i = 0;
         }
         }
 
 
-        if (!this.last_img) {
-            // This is the first received image, so we draw it
-            this.ctx.drawImage(this.offscreenCanvas, 0, 0);
+        const frame = this.frames[i];
+        const dims = frame.dims;
+
+        if (
+            !this.frameImageData ||
+            dims.width != this.frameImageData.width ||
+            dims.height != this.frameImageData.height
+        ) {
+            this.offscreenCanvas.width = dims.width;
+            this.offscreenCanvas.height = dims.height;
+            this.frameImageData = this.offscreenCanvas.getContext('2d').createImageData(dims.width, dims.height);
         }
         }
-        this.last_img = img;
-    }
 
 
-    /**
-     * Draws a gif frame at a specific index inside the canvas.
-     * @param { Number } i - The frame index
-     */
-    putFrame (i, show_pause_on_hover=true) {
-        if (!this.frames.length) return
+        // set the patch data as an override
+        this.frameImageData.data.set(frame.patch);
 
 
-        i = parseInt(i, 10);
-        if (i > this.frames.length - 1 || i < 0) {
-            i = 0;
-        }
-        const offset = this.frame_offsets[i];
-        this.offscreenCanvas.getContext('2d').putImageData(this.frames[i].data, offset.x, offset.y);
+        this.offscreenCanvas.getContext('2d').putImageData(this.frameImageData, 0, 0);
         this.ctx.globalCompositeOperation = 'copy';
         this.ctx.globalCompositeOperation = 'copy';
-        this.ctx.drawImage(this.offscreenCanvas, 0, 0);
+        this.ctx.drawImage(this.offscreenCanvas, dims.left, dims.top);
 
 
         if (show_pause_on_hover && this.hovering) {
         if (show_pause_on_hover && this.hovering) {
             this.drawPauseIcon();
             this.drawPauseIcon();
         }
         }
     }
     }
 
 
-    clear () {
-        this.transparency = null;
-        this.last_disposal_method = this.disposal_method;
-        this.disposal_method = null;
-        this.frame = null;
-    }
-
     /**
     /**
      * Start playing the gif
      * Start playing the gif
      */
      */
     play () {
     play () {
         this.playing = true;
         this.playing = true;
-        requestAnimationFrame(ts => this.onAnimationFrame(ts, 0, 0));
+        requestAnimationFrame((ts) => this.onAnimationFrame(ts, 0, 0));
     }
     }
 
 
     /**
     /**
@@ -431,7 +274,7 @@ export default class ConverseGif {
      */
      */
     pause () {
     pause () {
         this.playing = false;
         this.playing = false;
-        requestAnimationFrame(() => this.drawPlayIcon())
+        requestAnimationFrame(() => this.drawPlayIcon());
     }
     }
 
 
     drawPauseIcon () {
     drawPauseIcon () {
@@ -447,40 +290,32 @@ export default class ConverseGif {
         this.ctx.fillStyle = 'rgb(0, 0, 0, 0.25)';
         this.ctx.fillStyle = 'rgb(0, 0, 0, 0.25)';
         this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
         this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
 
 
-        const icon_size = this.canvas.height*0.1;
+        const icon_size = this.canvas.height * 0.1;
 
 
         // Draw bars
         // Draw bars
-        this.ctx.lineWidth = this.canvas.height*0.04;
+        this.ctx.lineWidth = this.canvas.height * 0.04;
         this.ctx.beginPath();
         this.ctx.beginPath();
-        this.ctx.moveTo(this.canvas.width/2-icon_size/2, this.canvas.height/2-icon_size);
-        this.ctx.lineTo(this.canvas.width/2-icon_size/2, this.canvas.height/2+icon_size);
+        this.ctx.moveTo(this.canvas.width / 2 - icon_size / 2, this.canvas.height / 2 - icon_size);
+        this.ctx.lineTo(this.canvas.width / 2 - icon_size / 2, this.canvas.height / 2 + icon_size);
         this.ctx.fillStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.fillStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.stroke();
         this.ctx.stroke();
 
 
         this.ctx.beginPath();
         this.ctx.beginPath();
-        this.ctx.moveTo(this.canvas.width/2+icon_size/2, this.canvas.height/2-icon_size);
-        this.ctx.lineTo(this.canvas.width/2+icon_size/2, this.canvas.height/2+icon_size);
+        this.ctx.moveTo(this.canvas.width / 2 + icon_size / 2, this.canvas.height / 2 - icon_size);
+        this.ctx.lineTo(this.canvas.width / 2 + icon_size / 2, this.canvas.height / 2 + icon_size);
         this.ctx.fillStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.fillStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.stroke();
         this.ctx.stroke();
 
 
         // Draw circle
         // Draw circle
-        this.ctx.lineWidth = this.canvas.height*0.02;
+        this.ctx.lineWidth = this.canvas.height * 0.02;
         this.ctx.strokeStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.strokeStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.beginPath();
         this.ctx.beginPath();
-        this.ctx.arc(
-            this.canvas.width/2,
-            this.canvas.height/2,
-            icon_size*1.5,
-            0,
-            2*Math.PI
-        );
+        this.ctx.arc(this.canvas.width / 2, this.canvas.height / 2, icon_size * 1.5, 0, 2 * Math.PI);
         this.ctx.stroke();
         this.ctx.stroke();
     }
     }
 
 
     drawPlayIcon () {
     drawPlayIcon () {
-        if (this.playing) {
-            return;
-        }
+        if (this.playing) return;
 
 
         // Clear the potential pause button by re-rendering the current frame
         // Clear the potential pause button by re-rendering the current frame
         this.putFrame(this.frame_idx, false);
         this.putFrame(this.frame_idx, false);
@@ -492,52 +327,28 @@ export default class ConverseGif {
         this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
         this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
 
 
         // Draw triangle
         // Draw triangle
-        const triangle_size = this.canvas.height*0.1;
+        const triangle_size = this.canvas.height * 0.1;
         const region = new Path2D();
         const region = new Path2D();
-        region.moveTo(this.canvas.width/2+triangle_size, this.canvas.height/2); // start at the pointy end
-        region.lineTo(this.canvas.width/2-triangle_size/2, this.canvas.height/2+triangle_size);
-        region.lineTo(this.canvas.width/2-triangle_size/2, this.canvas.height/2-triangle_size);
+        region.moveTo(this.canvas.width / 2 + triangle_size, this.canvas.height / 2); // start at the pointy end
+        region.lineTo(this.canvas.width / 2 - triangle_size / 2, this.canvas.height / 2 + triangle_size);
+        region.lineTo(this.canvas.width / 2 - triangle_size / 2, this.canvas.height / 2 - triangle_size);
         region.closePath();
         region.closePath();
         this.ctx.fillStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.fillStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.fill(region);
         this.ctx.fill(region);
 
 
         // Draw circle
         // Draw circle
-        const circle_size = triangle_size*1.5;
-        this.ctx.lineWidth = this.canvas.height*0.02;
+        const circle_size = triangle_size * 1.5;
+        this.ctx.lineWidth = this.canvas.height * 0.02;
         this.ctx.strokeStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.strokeStyle = 'rgb(200, 200, 200, 0.75)';
         this.ctx.beginPath();
         this.ctx.beginPath();
-        this.ctx.arc(
-            this.canvas.width/2,
-            this.canvas.height/2,
-            circle_size,
-            0,
-            2*Math.PI
-        );
+        this.ctx.arc(this.canvas.width / 2, this.canvas.height / 2, circle_size, 0, 2 * Math.PI);
         this.ctx.stroke();
         this.ctx.stroke();
     }
     }
 
 
-    doDecodeProgress (stream, draw) {
-        this.doShowProgress(stream.pos, stream.data.length, draw);
-    }
-
-    /**
-     * @param{boolean=} draw Whether to draw progress bar or not;
-     *  this is not idempotent because of translucency.
-     *  Note that this means that the text will be unsynchronized
-     *  with the progress bar on non-frames;
-     *  but those are typically so small (GCE etc.) that it doesn't really matter
-     */
-    withProgress (stream, fn, draw) {
-        return block => {
-            fn?.(block);
-            this.doDecodeProgress(stream, draw);
-        };
-    }
-
     getCanvasScale () {
     getCanvasScale () {
         let scale;
         let scale;
-        if (this.options.max_width && this.hdr && this.hdr.width > this.options.max_width) {
-            scale = this.options.max_width / this.hdr.width;
+        if (this.options.max_width && this.hdr && this.lsd.width > this.options.max_width) {
+            scale = this.options.max_width / this.lsd.width;
         } else {
         } else {
             scale = 1;
             scale = 1;
         }
         }
@@ -547,22 +358,24 @@ export default class ConverseGif {
     /**
     /**
      * Makes an HTTP request to fetch a GIF
      * Makes an HTTP request to fetch a GIF
      * @param { String } url
      * @param { String } url
-     * @returns { Promise<String> } Returns a promise which resolves with the response data.
+     * @returns { Promise<ArrayBuffer> } Returns a promise which resolves with the response data.
      */
      */
     fetchGIF (url) {
     fetchGIF (url) {
         const promise = getOpenPromise();
         const promise = getOpenPromise();
         const h = new XMLHttpRequest();
         const h = new XMLHttpRequest();
         h.open('GET', url, true);
         h.open('GET', url, true);
+        h.responseType = 'arraybuffer';
+
         h?.overrideMimeType('text/plain; charset=x-user-defined');
         h?.overrideMimeType('text/plain; charset=x-user-defined');
         h.onload = () => {
         h.onload = () => {
             if (h.status != 200) {
             if (h.status != 200) {
-                this.showError('xhr - response');
+                this.showError();
                 return promise.reject();
                 return promise.reject();
             }
             }
             promise.resolve(h.response);
             promise.resolve(h.response);
         };
         };
-        h.onprogress = (e) => (e.lengthComputable && this.doShowProgress(e.loaded, e.total, true));
-        h.onerror = () => this.showError('xhr');
+        h.onprogress = (e) => e.lengthComputable && this.doShowProgress(e.loaded, e.total, true);
+        h.onerror = () => this.showError();
         h.send();
         h.send();
         return promise;
         return promise;
     }
     }

+ 0 - 319
src/shared/gif/utils.js

@@ -1,319 +0,0 @@
-/**
- * @copyright Shachaf Ben-Kiki and the Converse.js contributors
- * @description
- *  Started as a fork of Shachaf Ben-Kiki's jsgif library
- *  https://github.com/shachaf/jsgif
- * @license MIT License
- */
-
-function bitsToNum (ba) {
-    return ba.reduce(function (s, n) {
-        return s * 2 + n;
-    }, 0);
-}
-
-function byteToBitArr (bite) {
-    const a = [];
-    for (let i = 7; i >= 0; i--) {
-        a.push( !! (bite & (1 << i)));
-    }
-    return a;
-}
-
-function lzwDecode (minCodeSize, data) {
-    // TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?
-    let pos = 0; // Maybe this streaming thing should be merged with the Stream?
-    function readCode (size) {
-        let code = 0;
-        for (let i = 0; i < size; i++) {
-            if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) {
-                code |= 1 << i;
-            }
-            pos++;
-        }
-        return code;
-    }
-
-    const output = [];
-    const clearCode = 1 << minCodeSize;
-    const eoiCode = clearCode + 1;
-
-    let codeSize = minCodeSize + 1;
-    let dict = [];
-
-    const clear = function () {
-        dict = [];
-        codeSize = minCodeSize + 1;
-        for (let i = 0; i < clearCode; i++) {
-            dict[i] = [i];
-        }
-        dict[clearCode] = [];
-        dict[eoiCode] = null;
-    };
-
-    let code = clearCode;
-    let last;
-    clear();
-
-    while (true) { // eslint-disable-line no-constant-condition
-        last = code;
-        code = readCode(codeSize);
-
-        if (code === clearCode) {
-            clear();
-            continue;
-        }
-        if (code === eoiCode) break;
-
-        if (code < dict.length) {
-            if (last !== clearCode) {
-                dict.push(dict[last].concat(dict[code][0]));
-            }
-        }
-        else {
-            if (code !== dict.length) throw new Error('Invalid LZW code.');
-            dict.push(dict[last].concat(dict[last][0]));
-        }
-        output.push.apply(output, dict[code]);
-
-        if (dict.length === (1 << codeSize) && codeSize < 12) {
-            // If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long.
-            codeSize++;
-        }
-    }
-    // I don't know if this is technically an error, but some GIFs do it.
-    //if (Math.ceil(pos / 8) !== data.length) throw new Error('Extraneous LZW bytes.');
-    return output;
-}
-
-
-function readSubBlocks (st) {
-    let size, data;
-    data = '';
-    do {
-        size = st.readByte();
-        data += st.read(size);
-    } while (size !== 0);
-    return data;
-}
-
-/**
- * Parses GIF image color table information
- * @param { Stream } st
- * @param { Number } entries
- */
-function parseCT (st, entries) { // Each entry is 3 bytes, for RGB.
-    const ct = [];
-    for (let i = 0; i < entries; i++) {
-        ct.push(st.readBytes(3));
-    }
-    return ct;
-}
-
-/**
- * Parses GIF image information
- * @param { Stream } st
- * @param { ByteStream } img
- * @param { Function } [callback]
- */
-function parseImg (st, img, callback) {
-    function deinterlace (pixels, width) {
-        // Of course this defeats the purpose of interlacing. And it's *probably*
-        // the least efficient way it's ever been implemented. But nevertheless...
-        const newPixels = new Array(pixels.length);
-        const rows = pixels.length / width;
-        function cpRow (toRow, fromRow) {
-            const fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);
-            newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));
-        }
-
-        // See appendix E.
-        const offsets = [0, 4, 2, 1];
-        const steps = [8, 8, 4, 2];
-        let fromRow = 0;
-        for (let pass = 0; pass < 4; pass++) {
-            for (let toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
-                cpRow(toRow, fromRow)
-                fromRow++;
-            }
-        }
-        return newPixels;
-    }
-
-    img.leftPos = st.readUnsigned();
-    img.topPos = st.readUnsigned();
-    img.width = st.readUnsigned();
-    img.height = st.readUnsigned();
-
-    const bits = byteToBitArr(st.readByte());
-    img.lctFlag = bits.shift();
-    img.interlaced = bits.shift();
-    img.sorted = bits.shift();
-    img.reserved = bits.splice(0, 2);
-    img.lctSize = bitsToNum(bits.splice(0, 3));
-
-    if (img.lctFlag) {
-        img.lct = parseCT(st, 1 << (img.lctSize + 1));
-    }
-    img.lzwMinCodeSize = st.readByte();
-
-    const lzwData = readSubBlocks(st);
-    img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData);
-
-    if (img.interlaced) { // Move
-        img.pixels = deinterlace(img.pixels, img.width);
-    }
-    callback?.(img);
-}
-
-/**
- * Parses GIF header information
- * @param { Stream } st
- * @param { Function } [callback]
- */
-function parseHeader (st, callback) {
-    const hdr = {};
-    hdr.sig = st.read(3);
-    hdr.ver = st.read(3);
-    if (hdr.sig !== 'GIF') {
-        throw new Error('Not a GIF file.');
-    }
-    hdr.width = st.readUnsigned();
-    hdr.height = st.readUnsigned();
-
-    const bits = byteToBitArr(st.readByte());
-    hdr.gctFlag = bits.shift();
-    hdr.colorRes = bitsToNum(bits.splice(0, 3));
-    hdr.sorted = bits.shift();
-    hdr.gctSize = bitsToNum(bits.splice(0, 3));
-
-    hdr.bgColor = st.readByte();
-    hdr.pixelAspectRatio = st.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
-    if (hdr.gctFlag) {
-        hdr.gct = parseCT(st, 1 << (hdr.gctSize + 1));
-    }
-    callback?.(hdr);
-}
-
-function parseExt (st, block, handler) {
-
-    function parseGCExt (block) {
-        st.readByte(); // blocksize, always 4
-        const bits = byteToBitArr(st.readByte());
-        block.reserved = bits.splice(0, 3); // Reserved; should be 000.
-        block.disposalMethod = bitsToNum(bits.splice(0, 3));
-        block.userInput = bits.shift();
-        block.transparencyGiven = bits.shift();
-        block.delayTime = st.readUnsigned();
-        block.transparencyIndex = st.readByte();
-        block.terminator = st.readByte();
-        handler?.gce(block);
-    }
-
-    function parseComExt (block) {
-        block.comment = readSubBlocks(st);
-        handler.com && handler.com(block);
-    }
-
-    function parsePTExt (block) {
-        // No one *ever* uses this. If you use it, deal with parsing it yourself.
-        st.readByte(); // blocksize, always 12
-        block.ptHeader = st.readBytes(12);
-        block.ptData = readSubBlocks(st);
-        handler.pte && handler.pte(block);
-    }
-
-    function parseAppExt (block) {
-        function parseNetscapeExt (block) {
-            st.readByte(); // blocksize, always 3
-            block.unknown = st.readByte(); // ??? Always 1? What is this?
-            block.iterations = st.readUnsigned();
-            block.terminator = st.readByte();
-            handler.app && handler.app.NETSCAPE && handler.app.NETSCAPE(block);
-        }
-
-        function parseUnknownAppExt (block) {
-            block.appData = readSubBlocks(st);
-            // FIXME: This won't work if a handler wants to match on any identifier.
-            handler.app && handler.app[block.identifier] && handler.app[block.identifier](block);
-        }
-
-        st.readByte(); // blocksize, always 11
-        block.identifier = st.read(8);
-        block.authCode = st.read(3);
-        switch (block.identifier) {
-            case 'NETSCAPE':
-                parseNetscapeExt(block);
-                break;
-            default:
-                parseUnknownAppExt(block);
-                break;
-        }
-    }
-
-    function parseUnknownExt (block) {
-        block.data = readSubBlocks(st);
-        handler.unknown && handler.unknown(block);
-    }
-
-    block.label = st.readByte();
-    switch (block.label) {
-        case 0xF9:
-            block.extType = 'gce';
-            parseGCExt(block);
-            break;
-        case 0xFE:
-            block.extType = 'com';
-            parseComExt(block);
-            break;
-        case 0x01:
-            block.extType = 'pte';
-            parsePTExt(block);
-            break;
-        case 0xFF:
-            block.extType = 'app';
-            parseAppExt(block);
-            break;
-        default:
-            block.extType = 'unknown';
-            parseUnknownExt(block);
-            break;
-    }
-}
-
-/**
- * @param { Stream } st
- * @param { GIFParserHandlers } handler
- */
-function parseBlock (st, handler) {
-    const block = {}
-    block.sentinel = st.readByte();
-    switch (String.fromCharCode(block.sentinel)) { // For ease of matching
-        case '!':
-            block.type = 'ext';
-            parseExt(st, block, handler);
-            break;
-        case ',':
-            block.type = 'img';
-            parseImg(st, block, handler?.img);
-            break;
-        case ';':
-            block.type = 'eof';
-            handler?.eof(block);
-            break;
-        default:
-            throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0.
-    }
-    if (block.type !== 'eof') setTimeout(() => parseBlock(st, handler), 0);
-}
-
-/**
- * Takes a Stream and parses it for GIF data, calling the relevant handler
- * methods on the passed in `handler` object.
- * @param { Stream } st
- * @param { GIFParserHandlers } handler
- */
-export function parseGIF (st, handler={}) {
-    parseHeader(st, handler?.hdr);
-    setTimeout(() => parseBlock(st, handler), 0);
-}

+ 0 - 5
src/shared/rich-text.js

@@ -283,11 +283,6 @@ export class RichText extends String {
 
 
     /**
     /**
      * Parse the text and add template references for rendering the "rich" parts.
      * Parse the text and add template references for rendering the "rich" parts.
-     *
-     * @param { RichText } text
-     * @param { Boolean } show_images - Should URLs of images be rendered as `<img>` tags?
-     * @param { Function } onImgLoad
-     * @param { Function } onImgClick
      **/
      **/
     async addTemplates () {
     async addTemplates () {
         /**
         /**

+ 5 - 0
tsconfig.json

@@ -11,6 +11,11 @@
     "target": "es2016",
     "target": "es2016",
     "module": "esnext",
     "module": "esnext",
 
 
+    "lib": [
+      "ES2020",
+      "dom"
+    ],
+
     "allowJs": true,
     "allowJs": true,
     "checkJs": true,
     "checkJs": true,