Implement indexed png exporting

main
kts of kettek 2024-04-25 02:04:14 -07:00
parent 7e0025846e
commit b557272c04
4 changed files with 195 additions and 109 deletions

View File

@ -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",

View File

@ -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"
}
}

View File

@ -1 +1 @@
ca3651e79b25974c2fcf59de2912276b
f001f3ad84549a4a16ef6303845139bd

View File

@ -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))
})
}
}