From cf74fa531c74b6fdbe13fc405e28bf942316fe76 Mon Sep 17 00:00:00 2001 From: Josh Greig Date: Sun, 21 May 2023 01:06:14 -0400 Subject: [PATCH] optimized memory usage by maintaining encoded GIF data in a Uint8Array instead of a classical untyped Array Since a Uint8Array uses only 1 byte per element and every number is likely a 64-bit floating point number, Uint8Array should be a much more compact way to store the data. This change was tested by running encoding the same frames of a long gif animation that previously crashed the browser every time on the same laptop, same browser, same version of Windows... --- DynamicByteArray.js | 49 +++++++++++++++++++++++++++++++++++++++++++++ GIFEncoder.js | 14 ++++++------- 2 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 DynamicByteArray.js diff --git a/DynamicByteArray.js b/DynamicByteArray.js new file mode 100644 index 0000000..0624fbd --- /dev/null +++ b/DynamicByteArray.js @@ -0,0 +1,49 @@ +/** + * This class represents an array of bytes in a compact, memory-efficient format. + * This was written to be used by GIFEncoder so it could encode larger GIF files than possible with a non-typed Array of number. + * @author Josh Greig + */ +function DynamicByteArray() { + var arr = new Uint8Array(1000); + var len = 0; + + function expandCapacity() { + var newCapacity = arr.length * 2; + // If the capacity is huge, the risk of running out of memory is higher + // so we want to expand in 50% intervals instead of 100% intervals. + if (newCapacity > 50000000) { + newCapacity = arr.length * 1.5; + } + var newArr = new Uint8Array(newCapacity); + for (let i = 0; i < arr.length; i++) { + newArr[i] = arr[i]; + } + arr = newArr; + } + + DynamicByteArray.prototype.get = function(index) { + return arr[index]; + }; + + DynamicByteArray.prototype.getLength = function() { + return len; + } + + DynamicByteArray.prototype.toCompactUint8Array = function() { + if (arr.length !== len) { + const result = new Uint8Array(len); + for (let i = 0; i < len; i++) { + result[i] = arr[i]; + } + arr = result; + } + return arr; + }; + + DynamicByteArray.prototype.writeByte = function(val) { + if (len >= arr.length) { + expandCapacity(); + } + arr[len++] = val; + }; +} \ No newline at end of file diff --git a/GIFEncoder.js b/GIFEncoder.js index f238e9b..d701f7d 100755 --- a/GIFEncoder.js +++ b/GIFEncoder.js @@ -13,27 +13,27 @@ GIFEncoder = function() { chr[i] = String.fromCharCode(i); function ByteArray() { - this.bin = []; + this.bin = new DynamicByteArray(); } ByteArray.prototype.getData = function() { - for (var v = '', l = this.bin.length, i = 0; i < l; i++) - v += chr[this.bin[i]]; + for (var v = '', l = this.bin.getLength(), i = 0; i < l; i++) + v += chr[this.bin.get(i)]; return v; }; ByteArray.prototype.writeByte = function(val) { - this.bin.push(val); + this.bin.writeByte(val); }; ByteArray.prototype.writeUTFBytes = function(string) { for (var l = string.length, i = 0; i < l; i++) - this.writeByte(string.charCodeAt(i)); + this.bin.writeByte(string.charCodeAt(i)); }; ByteArray.prototype.writeBytes = function(array, offset, length) { for (var l = length || array.length, i = offset || 0; i < l; i++) - this.writeByte(array[i]); + this.bin.writeByte(array[i]); }; var exports = {}; @@ -206,7 +206,7 @@ GIFEncoder = function() { filename= filename !== undefined ? ( filename.endsWith(".gif")? filename: filename+".gif" ): "download.gif"; var templink = document.createElement("a"); templink.download=filename; - templink.href= URL.createObjectURL(new Blob([new Uint8Array(out.bin)], {type : "image/gif" } )); + templink.href= URL.createObjectURL(new Blob([out.bin.toCompactUint8Array()], {type : "image/gif" } )); templink.click(); } }