diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 51150a5..6e0b2c1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,8 @@ "name": "frontend", "version": "0.0.0", "dependencies": { - "easy-crc": "^1.1.0", + "buffer": "^6.0.3", + "crc-32": "^1.2.2", "fflate": "^0.8.2" }, "devDependencies": { @@ -189,6 +190,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -220,6 +240,29 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -283,6 +326,17 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -318,11 +372,6 @@ "node": ">=8" } }, - "node_modules/easy-crc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/easy-crc/-/easy-crc-1.1.0.tgz", - "integrity": "sha512-cvtERLTu8mYt1pOVVekJ4E0VdwyuKgL+VAfE3LY+Lw762i9M1bSEikm6fyGD2grxOdBfI0HgOmSi3HQneKr33A==" - }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -813,6 +862,25 @@ "node": ">= 0.4" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/immutable": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", @@ -1680,6 +1748,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1705,6 +1778,15 @@ "fill-range": "^7.0.1" } }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1754,6 +1836,11 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1775,11 +1862,6 @@ "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true }, - "easy-crc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/easy-crc/-/easy-crc-1.1.0.tgz", - "integrity": "sha512-cvtERLTu8mYt1pOVVekJ4E0VdwyuKgL+VAfE3LY+Lw762i9M1bSEikm6fyGD2grxOdBfI0HgOmSi3HQneKr33A==" - }, "es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -2055,6 +2137,11 @@ "function-bind": "^1.1.2" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "immutable": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index bbddc4b..513d301 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,8 @@ "vite": "^3.0.7" }, "dependencies": { - "easy-crc": "^1.1.0", + "buffer": "^6.0.3", + "crc-32": "^1.2.2", "fflate": "^0.8.2" } } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 3e95015..1386cbf 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -ca3651e79b25974c2fcf59de2912276b \ No newline at end of file +f001f3ad84549a4a16ef6303845139bd \ No newline at end of file diff --git a/frontend/src/types/canvas.ts b/frontend/src/types/canvas.ts index 1c37cb0..23526a3 100644 --- a/frontend/src/types/canvas.ts +++ b/frontend/src/types/canvas.ts @@ -1,6 +1,7 @@ import type { PixelPosition } from "./shapes" -import { unzlibSync, zlibSync } from 'fflate' -import { crc32 } from 'easy-crc' +import { zlibSync } from 'fflate' +import * as crc32 from 'crc-32' +import { Buffer } from 'buffer/' import { SaveFileBytes } from "../../wailsjs/go/main/App.js" /** @@ -360,101 +361,98 @@ export class Canvas { }) } - throw new Error('indexed export not yet implemented') + // Otherwise do some lazy indexed PNG generation. FIXME: This is ham-fisted and not the proper way to encode PNGs... + return new Promise((resolve, reject) => { + let out = Buffer.alloc(0) - // Otherwise do some lazy indexed PNG generation. - let out = Buffer.alloc(0) - - let buffer = Buffer.alloc(21) - let bufferOffset = 0 - let chunkStart = 0 - let chunkEnd = 0 - - // Write header - buffer = Buffer.alloc(8) - bufferOffset = buffer.writeUInt8(0x89, bufferOffset) - bufferOffset = buffer.writeUInt8(0x50, bufferOffset) - bufferOffset = buffer.writeUInt8(0x4E, bufferOffset) - bufferOffset = buffer.writeUInt8(0x47, bufferOffset) - bufferOffset = buffer.writeUInt8(0x0D, bufferOffset) - bufferOffset = buffer.writeUInt8(0x0A, bufferOffset) - bufferOffset = buffer.writeUInt8(0x1A, bufferOffset) - bufferOffset = buffer.writeUInt8(0x0A, bufferOffset) - out = Buffer.concat([out, buffer]) - - // Chunk o'clock (length, type, data, crc. crc = network-order CRC-32 of type + data) - // Write IHDR - buffer = Buffer.alloc(12 + 13), bufferOffset = 0 - bufferOffset = buffer.writeUInt32BE(13, bufferOffset) - chunkStart = bufferOffset - bufferOffset += buffer.write('IHDR', bufferOffset, 'latin1') - bufferOffset = buffer.writeUInt32BE(this.width, bufferOffset) - bufferOffset = buffer.writeUInt32BE(this.height, bufferOffset) - bufferOffset = buffer.writeUInt8(8, bufferOffset) // Indexed 8-bit depth for palette entries - bufferOffset = buffer.writeUInt8(3, bufferOffset) // Color type of indexed - bufferOffset = buffer.writeUInt8(0, bufferOffset) // Compression method DEFLATE - bufferOffset = buffer.writeUInt8(0, bufferOffset) // Filter method of 0 - bufferOffset = buffer.writeUInt8(0, bufferOffset) // No interlace method - chunkEnd = bufferOffset - bufferOffset = buffer.writeUInt32BE(crc32('CRC-32', buffer.slice(chunkStart, chunkEnd)), bufferOffset) - out = Buffer.concat([out, buffer]) - - // TODO: Actually implement this code ripped from my own garbage encoder simple-indexed-png-encode. - - // Write PLTE - /*let palettesSize = this.palette.length - this.palette.length/4 - buffer = Buffer.alloc(12 + palettesSize), bufferOffset = 0 - bufferOffset = buffer.writeUInt32BE(palettesSize, bufferOffset) - chunkStart = bufferOffset - bufferOffset += buffer.write('PLTE', bufferOffset, 'latin1') - for (let i = 0; i < palettes.length; i++) { - bufferOffset = buffer.writeUInt8(palettes[i++], bufferOffset) // R - bufferOffset = buffer.writeUInt8(palettes[i++], bufferOffset) // G - bufferOffset = buffer.writeUInt8(palettes[i++], bufferOffset) // B - // Alpha is skipped. - } - chunkEnd = bufferOffset - bufferOffset = buffer.writeUInt32BE(crc32('CRC-32', buffer.slice(chunkStart, chunkEnd)), bufferOffset) - out = Buffer.concat([out, buffer]) - // Write tRNS - palettesSize = palettes.length/4 - buffer = Buffer.alloc(12 + palettesSize), bufferOffset = 0 - bufferOffset = buffer.writeUInt32BE(palettesSize, bufferOffset) - chunkStart = bufferOffset - bufferOffset += buffer.write('tRNS', bufferOffset, 'latin1') - for (let i = 3; i < palettes.length; i += 4) { - bufferOffset = buffer.writeUInt8(palettes[i], bufferOffset) // A - } - chunkEnd = bufferOffset - bufferOffset = buffer.writeUInt32BE(crc32('CRC-32', buffer.slice(chunkStart, chunkEnd)), bufferOffset) - out = Buffer.concat([out, buffer]) - // Build our deflate data - let data = Buffer.alloc(pixels.length + height) - let dataOffset = 0 - for (let i = 0; i < height; i++) { - dataOffset = data.writeUInt8(0, dataOffset) // No Filter - for (let j = 0, end = width; j < end; j++) { - dataOffset = data.writeUInt8(pixels[i*width+j], dataOffset) + let buffer = Buffer.alloc(21) + let bufferOffset = 0 + let chunkStart = 0 + let chunkEnd = 0 + // Write header + buffer = Buffer.alloc(8) + bufferOffset = buffer.writeUInt8(0x89, bufferOffset) + bufferOffset = buffer.writeUInt8(0x50, bufferOffset) + bufferOffset = buffer.writeUInt8(0x4E, bufferOffset) + bufferOffset = buffer.writeUInt8(0x47, bufferOffset) + bufferOffset = buffer.writeUInt8(0x0D, bufferOffset) + bufferOffset = buffer.writeUInt8(0x0A, bufferOffset) + bufferOffset = buffer.writeUInt8(0x1A, bufferOffset) + bufferOffset = buffer.writeUInt8(0x0A, bufferOffset) + out = Buffer.concat([out, buffer]) + // Chunk o'clock (length, type, data, crc. crc = network-order CRC-32 of type + data) + // Write IHDR + buffer = Buffer.alloc(12 + 13), bufferOffset = 0 + bufferOffset = buffer.writeUInt32BE(13, bufferOffset) + chunkStart = bufferOffset + bufferOffset += buffer.write('IHDR', bufferOffset) + bufferOffset = buffer.writeUInt32BE(this.width, bufferOffset) + bufferOffset = buffer.writeUInt32BE(this.height, bufferOffset) + bufferOffset = buffer.writeUInt8(8, bufferOffset) // Indexed 8-bit depth for palette entries + bufferOffset = buffer.writeUInt8(3, bufferOffset) // Color type of indexed + bufferOffset = buffer.writeUInt8(0, bufferOffset) // Compression method DEFLATE + bufferOffset = buffer.writeUInt8(0, bufferOffset) // Filter method of 0 + bufferOffset = buffer.writeUInt8(0, bufferOffset) // No interlace method + chunkEnd = bufferOffset + bufferOffset = buffer.writeInt32BE(crc32.buf(buffer.slice(chunkStart, chunkEnd)), bufferOffset) + out = Buffer.concat([out, buffer]) + // Write PLTE + let palettesSize = this.palette.length * 3 + buffer = Buffer.alloc(12 + palettesSize), bufferOffset = 0 + bufferOffset = buffer.writeUInt32BE(palettesSize, bufferOffset) + chunkStart = bufferOffset + bufferOffset += buffer.write('PLTE', bufferOffset) + for (let i = 0; i < this.palette.length; i++) { + bufferOffset = buffer.writeUInt8(this.palette[i] & 0xFF, bufferOffset) + bufferOffset = buffer.writeUInt8((this.palette[i] >> 8) & 0xFF, bufferOffset) + bufferOffset = buffer.writeUInt8((this.palette[i] >> 16) & 0xFF, bufferOffset) + // Alpha is skipped. } - } - let deflatedData = await deflateAsync(data) - // Write IDAT // Our DEFLATE and 0->raw scanline data - buffer = Buffer.alloc(8), bufferOffset = 0 - bufferOffset = buffer.writeUInt32BE(deflatedData.length, bufferOffset) - chunkStart = bufferOffset - bufferOffset += buffer.write('IDAT', bufferOffset, 'latin1') - buffer = Buffer.concat([buffer, deflatedData, Buffer.alloc(4)]) - bufferOffset = bufferOffset + deflatedData.length - chunkEnd = bufferOffset - bufferOffset = buffer.writeUInt32BE(crc32('CRC-32', buffer.slice(chunkStart, chunkEnd)), bufferOffset) - out = Buffer.concat([out, buffer]) - // Write IEND - buffer = Buffer.alloc(12), bufferOffset = 0 - bufferOffset = buffer.writeUInt32BE(0, bufferOffset) - chunkStart = bufferOffset - bufferOffset += buffer.write('IEND', bufferOffset, 'latin1') - chunkEnd = bufferOffset - bufferOffset = buffer.writeUInt32BE(crc32('CRC-32', buffer.slice(chunkStart, chunkEnd)), bufferOffset) - out = Buffer.concat([out, buffer])*/ + chunkEnd = bufferOffset + bufferOffset = buffer.writeInt32BE(crc32.buf(buffer.slice(chunkStart, chunkEnd)), bufferOffset) + out = Buffer.concat([out, buffer]) + // Write tRNS + palettesSize = this.palette.length + buffer = Buffer.alloc(12 + palettesSize), bufferOffset = 0 + bufferOffset = buffer.writeUInt32BE(palettesSize, bufferOffset) + chunkStart = bufferOffset + bufferOffset += buffer.write('tRNS', bufferOffset) + for (let i = 0; i < this.palette.length; i++) { + bufferOffset = buffer.writeUInt8((this.palette[i] >> 24) & 0xFF, bufferOffset) + } + chunkEnd = bufferOffset + bufferOffset = buffer.writeInt32BE(crc32.buf(buffer.slice(chunkStart, chunkEnd)), bufferOffset) + out = Buffer.concat([out, buffer]) + // Build our deflate data + let data = Buffer.alloc(this.pixels.length + this.height) + let dataOffset = 0 + for (let i = 0; i < this.height; i++) { + dataOffset = data.writeUInt8(0, dataOffset) // No Filter + for (let j = 0, end = this.width; j < end; j++) { + dataOffset = data.writeUInt8(this.pixels[i*this.width+j], dataOffset) + } + } + //let deflatedData = deflateSync(data, {level: 9}) + let deflatedData = zlibSync(data, {level: 9}) + // Write IDAT // Our DEFLATE and 0->raw scanline data + buffer = Buffer.alloc(8), bufferOffset = 0 + bufferOffset = buffer.writeUInt32BE(deflatedData.length, bufferOffset) + chunkStart = bufferOffset + bufferOffset += buffer.write('IDAT', bufferOffset) + buffer = Buffer.concat([buffer, deflatedData, Buffer.alloc(4)]) + bufferOffset = bufferOffset + deflatedData.length + chunkEnd = bufferOffset + bufferOffset = buffer.writeInt32BE(crc32.buf(buffer.slice(chunkStart, chunkEnd)), bufferOffset) + out = Buffer.concat([out, buffer]) + // Write IEND + buffer = Buffer.alloc(12), bufferOffset = 0 + bufferOffset = buffer.writeUInt32BE(0, bufferOffset) + chunkStart = bufferOffset + bufferOffset += buffer.write('IEND', bufferOffset) + chunkEnd = bufferOffset + bufferOffset = buffer.writeInt32BE(crc32.buf(buffer.slice(chunkStart, chunkEnd)), bufferOffset) + out = Buffer.concat([out, buffer]) + resolve(new Uint8Array(out)) + }) } } \ No newline at end of file