Implement custom PNG decode; use WIP weirdness

main
kts of kettek 2024-02-13 21:03:42 -08:00
parent 26e119421d
commit a4ee106ffe
10 changed files with 583 additions and 22 deletions

View File

@ -7,6 +7,9 @@
"": { "": {
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": {
"fflate": "^0.8.2"
},
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0", "@tsconfig/svelte": "^3.0.0",
@ -139,6 +142,17 @@
"integrity": "sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==", "integrity": "sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==",
"dev": true "dev": true
}, },
"node_modules/@types/node": {
"version": "20.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
"integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/pug": { "node_modules/@types/pug": {
"version": "2.0.10", "version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@ -691,6 +705,11 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -1452,6 +1471,14 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/vite": { "node_modules/vite": {
"version": "3.2.8", "version": "3.2.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.8.tgz",
@ -1605,6 +1632,17 @@
"integrity": "sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==", "integrity": "sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==",
"dev": true "dev": true
}, },
"@types/node": {
"version": "20.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
"integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==",
"dev": true,
"optional": true,
"peer": true,
"requires": {
"undici-types": "~5.26.4"
}
},
"@types/pug": { "@types/pug": {
"version": "2.0.10", "version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@ -1929,6 +1967,11 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
},
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -2421,6 +2464,14 @@
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true "dev": true
}, },
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"optional": true,
"peer": true
},
"vite": { "vite": {
"version": "3.2.8", "version": "3.2.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.8.tgz",

View File

@ -20,5 +20,8 @@
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"vite": "^3.0.7" "vite": "^3.0.7"
},
"dependencies": {
"fflate": "^0.8.2"
} }
} }

View File

@ -1 +1 @@
2abc6cc777dbbae09f2d92ba2a0d453d ca3651e79b25974c2fcf59de2912276b

View File

@ -16,6 +16,7 @@
import { Close } from "carbon-icons-svelte" import { Close } from "carbon-icons-svelte"
import StackPreview from './sections/StackPreview.svelte' import StackPreview from './sections/StackPreview.svelte'
import type { Canvas } from './types/canvas'
let theme: 'white'|'g10'|'g80'|'g90'|'g100' = 'g90' let theme: 'white'|'g10'|'g80'|'g90'|'g100' = 'g90'
@ -25,9 +26,9 @@
let showImport: boolean = false let showImport: boolean = false
let importValid: boolean = false let importValid: boolean = false
let importImage: HTMLImageElement = null
let importFile: data.StackistFileV1 = null let importFile: data.StackistFileV1 = null
let importFilepath: string = '' let importFilepath: string = ''
let importCanvas: Canvas = null
let showPreview: boolean = false let showPreview: boolean = false
@ -41,7 +42,7 @@
filepath: importFilepath, filepath: importFilepath,
title: importFilepath, title: importFilepath,
data: importFile, data: importFile,
image: importImage, canvas: importCanvas,
}] }]
console.log(files) console.log(files)
} }
@ -86,7 +87,7 @@
<svelte:fragment slot="content"> <svelte:fragment slot="content">
{#each files as file} {#each files as file}
<TabContent> <TabContent>
<Editor2D img={file.image} refresh={refresh} /> <Editor2D bind:file={file} refresh={refresh} />
</TabContent> </TabContent>
{/each} {/each}
</svelte:fragment> </svelte:fragment>
@ -108,8 +109,8 @@
bind:open={showImport} bind:open={showImport}
bind:valid={importValid} bind:valid={importValid}
bind:file={importFile} bind:file={importFile}
bind:img={importImage}
bind:filepath={importFilepath} bind:filepath={importFilepath}
bind:canvas={importCanvas}
/> />
</ComposedModal> </ComposedModal>

View File

@ -2,8 +2,9 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import type { data } from '../../wailsjs/go/models.ts' import type { data } from '../../wailsjs/go/models.ts'
import type { LoadedFile } from '../types/file'
export let img: HTMLImageElement export let file: LoadedFile
export let animation: data.Animation export let animation: data.Animation
export let frame: data.Frame export let frame: data.Frame
export let layer: data.Layer export let layer: data.Layer
@ -44,8 +45,8 @@
} }
if (offsetX === undefined || offsetY === undefined) { if (offsetX === undefined || offsetY === undefined) {
// Adjust offset to center image on first LOAD. // Adjust offset to center image on first LOAD.
offsetX = rootCanvas.width/2 - img.width/2 offsetX = rootCanvas.width/2 - file.canvas.width/2
offsetY = rootCanvas.height/2 - img.height/2 offsetY = rootCanvas.height/2 - file.canvas.height/2
} }
} }
@ -78,15 +79,14 @@
ctx.save() ctx.save()
ctx.imageSmoothingEnabled = false ctx.imageSmoothingEnabled = false
ctx.scale(zoom, zoom) ctx.scale(zoom, zoom)
//ctx.transform(1, 0, 0, 1, -img.width/2, -img.height/2)
{ {
ctx.beginPath() ctx.beginPath()
ctx.fillStyle = '#888888' ctx.fillStyle = '#888888'
ctx.rect(offsetX, offsetY, img.width, img.height) ctx.rect(offsetX, offsetY, file.canvas.width, file.canvas.height)
ctx.fill() ctx.fill()
let rows = img.height / checkerboardSize let rows = file.canvas.height / checkerboardSize
let cols = img.width / checkerboardSize let cols = file.canvas.width / checkerboardSize
ctx.beginPath() ctx.beginPath()
ctx.fillStyle = '#444444' ctx.fillStyle = '#444444'
for (let r = 0; r < rows; r++) { for (let r = 0; r < rows; r++) {
@ -105,7 +105,7 @@
} }
// TODO: Draw the current layer of the current frame. // TODO: Draw the current layer of the current frame.
ctx.drawImage(img, offsetX, offsetY) ctx.drawImage(file.canvas.canvas, offsetX, offsetY)
ctx.restore() ctx.restore()
} }
@ -134,13 +134,13 @@
} }
function capOffset() { function capOffset() {
if (offsetX < -img.width+30) { if (offsetX < -file.canvas.width+30) {
offsetX = -img.width+30 offsetX = -file.canvas.width+30
} else if (offsetX > canvas.width-30) { } else if (offsetX > canvas.width-30) {
offsetX = canvas.width-30 offsetX = canvas.width-30
} }
if (offsetY < -img.height+30) { if (offsetY < -file.canvas.height+30) {
offsetY = -img.height+30 offsetY = -file.canvas.height+30
} else if (offsetY > canvas.height-30) { } else if (offsetY > canvas.height-30) {
offsetY = canvas.height-30 offsetY = canvas.height-30
} }

View File

@ -3,6 +3,9 @@
import { data } from '../../wailsjs/go/models.js' import { data } from '../../wailsjs/go/models.js'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { IndexedPNG } from '../types/png'
import { Canvas } from '../types/canvas'
import { Button, NumberInput, Checkbox, RadioButtonGroup, RadioButton } from 'carbon-components-svelte' import { Button, NumberInput, Checkbox, RadioButtonGroup, RadioButton } from 'carbon-components-svelte'
import { Form, FormGroup, InlineNotification, Tile, Truncate } from 'carbon-components-svelte' import { Form, FormGroup, InlineNotification, Tile, Truncate } from 'carbon-components-svelte'
import { Grid, Row, Column } from "carbon-components-svelte" import { Grid, Row, Column } from "carbon-components-svelte"
@ -30,7 +33,8 @@
let rowBasedFrames: boolean = true let rowBasedFrames: boolean = true
export let file: data.StackistFileV1 export let file: data.StackistFileV1
export let filepath: string = '' export let filepath: string = ''
export let img: HTMLImageElement export let canvas: Canvas
let img: HTMLImageElement
let path: string = '' let path: string = ''
let error: string = "" let error: string = ""
let error2: string = "" let error2: string = ""
@ -40,7 +44,7 @@
let groups: number = 0 let groups: number = 0
let animations: number = 0 let animations: number = 0
function loadImage(base64: number[]): Promise<HTMLImageElement> { function loadImage(base64: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
img = new Image() img = new Image()
img.onload = () => resolve(img) img.onload = () => resolve(img)
@ -52,9 +56,45 @@
async function openFile() { async function openFile() {
try { try {
filepath = await GetFilePath() filepath = await GetFilePath()
let bytes = await OpenFileBytes(filepath) let bytes = (await OpenFileBytes(filepath)) as unknown as string
path = /[^/\\]*$/.exec(filepath)[0] path = /[^/\\]*$/.exec(filepath)[0]
img = await loadImage(bytes) img = await loadImage(bytes)
console.log('oops', bytes, typeof bytes)
console.log('gonna do it...')
let arr = Uint8Array.from(atob(bytes), (v) => v.charCodeAt(0))
console.log('arr', arr)
let png = new IndexedPNG(arr)
console.log('hmm', png)
await png.decode()
canvas = new Canvas(png.width, png.height)
console.log('made canvas', canvas)
if (png.pixelBitlength === 32) {
console.log('yee', png.decodedPixels)
for (let i = 0; i < png.decodedPixels.length; i += 4) {
let y = Math.floor(i / (png.width * 4))
let x = (i / 4) % png.width
canvas.setPixelRGBA(x, y, png.decodedPixels[i], png.decodedPixels[i+1], png.decodedPixels[i+2], png.decodedPixels[i+3])
}
} else if (png.pixelBitlength === 24) {
// RGB
} else if (png.pixelBitlength === 8) {
canvas.setPaletteFromUint8Array(png.decodedPalette)
for (let i = 0; i < png.decodedPixels.length; i++) {
let y = Math.floor(i / (png.width * 4))
let x = (i / 4) % png.width
canvas.setPixel(x, y, png.decodedPixels[i])
}
} else {
error = "pixel format"
error2 = "unsupported pixel format"
return
}
console.log('wowww', canvas)
canvas.refreshCanvas()
recalc() recalc()
} catch(err) { } catch(err) {
error = "open" error = "open"

View File

@ -38,7 +38,7 @@
ctx.save() ctx.save()
ctx.translate(x, y) ctx.translate(x, y)
ctx.rotate(rotation * Math.PI / 180) ctx.rotate(rotation * Math.PI / 180)
ctx.drawImage(file.image, layer.x, layer.y, file.data.width, file.data.height, -file.data.width/2, -file.data.height/2, file.data.width, file.data.height) ctx.drawImage(file.canvas.canvas, layer.x, layer.y, file.data.width, file.data.height, -file.data.width/2, -file.data.height/2, file.data.width, file.data.height)
ctx.restore() ctx.restore()
y -= 1 * layerDistance y -= 1 * layerDistance
} }

View File

@ -0,0 +1,83 @@
export class Canvas {
width: number
height: number
palette: Uint32Array // 32-bit RGBA palette
pixels: Uint8Array // 8-bit indices into the palette
canvas: HTMLCanvasElement
imageData: ImageData
constructor(width: number, height: number) {
this.width = width
this.height = height
this.pixels = new Uint8Array(width * height)
this.palette = new Uint32Array(0)
this.canvas = document.createElement('canvas')
this.imageData = new ImageData(width, height)
}
clear() {
for (let i = 0; i < this.pixels.length; i++) {
this.pixels[i] = 0
}
}
refreshCanvas() {
this.canvas.width = this.width
this.canvas.height = this.height
let ctx = this.canvas.getContext('2d')
ctx.putImageData(this.imageData, 0, 0)
}
setPalette(palette: Uint32Array) {
this.palette = palette
}
setPaletteFromUint8Array(palette: Uint8Array) {
this.palette = new Uint32Array(palette.length / 4)
for (let i = 0; i < palette.length; i += 4) {
this.palette[i / 4] = (palette[i + 3] << 24) | (palette[i] << 16) | (palette[i + 1] << 8) | palette[i + 2]
}
}
setPixelsFromUint8Array(pixels: Uint8Array) {
this.pixels = pixels
this.imageData = new ImageData(this.width, this.height)
for (let i = 0; i < pixels.length; i++) {
let color = this.palette[pixels[i]]
this.imageData.data[i * 4 + 0] = color & 0xFF
this.imageData.data[i * 4 + 1] = (color >> 8) & 0xFF
this.imageData.data[i * 4 + 2] = (color >> 16) & 0xFF
this.imageData.data[i * 4 + 3] = (color >> 24) & 0xFF
}
}
setPixel(x: number, y: number, index: number) {
this.pixels[y * this.width + x] = index
let color = this.palette[index]
this.imageData.data[(y * this.width + x) * 4 + 0] = color & 0xFF
this.imageData.data[(y * this.width + x) * 4 + 1] = (color >> 8) & 0xFF
this.imageData.data[(y * this.width + x) * 4 + 2] = (color >> 16) & 0xFF
this.imageData.data[(y * this.width + x) * 4 + 3] = (color >> 24) & 0xFF
}
setPixelRGBA(x: number, y: number, r: number, g: number, b: number, a: number) {
this.pixels[y * this.width + x] = this.addPaletteColor(r, g, b, a)
this.imageData.data[(y * this.width + x) * 4 + 0] = r
this.imageData.data[(y * this.width + x) * 4 + 1] = g
this.imageData.data[(y * this.width + x) * 4 + 2] = b
this.imageData.data[(y * this.width + x) * 4 + 3] = a
}
addPaletteColor(r: number, g: number, b: number, a: number): number {
// Check if the color is already in the palette
for (let i = 0; i < this.palette.length; i++) {
let v = new Uint32Array([(a << 24) | (b << 16) | (g << 8) | r])[0]
if (this.palette.at(i) === v) {
return i
}
}
// Add the color to the palette
const index = this.palette.length
let v = new Uint32Array([(a << 24) | (b << 16) | (g << 8) | r])[0]
let newPalette = new Uint32Array(this.palette.length + 1)
newPalette.set(this.palette)
newPalette[this.palette.length] = v
this.palette = newPalette
return index
}
}

View File

@ -1,8 +1,9 @@
import type { data } from '../../wailsjs/go/models.ts' import type { data } from '../../wailsjs/go/models.ts'
import type { Canvas } from './canvas.ts'
export class LoadedFile { export class LoadedFile {
filepath: string filepath: string
title: string title: string
image: HTMLImageElement canvas: Canvas
data: data.StackistFileV1 data: data.StackistFileV1
} }

View File

@ -0,0 +1,382 @@
import { unzlibSync, zlibSync } from 'fflate'
const range = (left, right, inclusive) => {
let range = [];
let ascending = left < right;
let end = !inclusive ? right : ascending ? right + 1 : right - 1;
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i);
}
return range;
}
export class IndexedPNG {
private data: Uint8Array
private pos: number
private palette: number[]
private imgData: Uint8Array
private transparency: { indexed?: number[], grayscale?: number, rgb?: number[] }
private text: { [key: string]: string }
public width: number
public height: number
private bits: number
private colorType: number
private compressionMethod: number
private filterMethod: number
private interlaceMethod: number
private colors: number
private hasAlphaChannel: boolean
public pixelBitlength: number
private colorSpace: string
public decodedPalette: Uint8Array
public decodedPixels: Uint8Array
constructor(data: Uint8Array) {
this.data = data;
this.pos = 8; // Skip the default header
this.palette = [];
this.transparency = {};
this.text = {};
console.log('IndexedPNG', data)
this.process()
}
process() {
const imgDataBuf = [];
let i: number;
while (true) {
let end: number;
const chunkSize = this.readUInt32();
const section = ((() => {
const result = [];
for (i = 0; i < 4; i++) {
result.push(String.fromCharCode(this.data[this.pos++]));
}
return result;
})()).join('');
switch (section) {
case 'IHDR':
// we can grab interesting values from here (like width, height, etc)
this.width = this.readUInt32();
this.height = this.readUInt32();
this.bits = this.data[this.pos++];
this.colorType = this.data[this.pos++];
this.compressionMethod = this.data[this.pos++];
this.filterMethod = this.data[this.pos++];
this.interlaceMethod = this.data[this.pos++];
break;
case 'PLTE':
this.palette = this.read(chunkSize);
break;
case 'IDAT':
for (i = 0, end = chunkSize; i < end; i++) {
imgDataBuf.push(this.data[this.pos++]);
}
break;
case 'tRNS':
// This chunk can only occur once and it must occur after the
// PLTE chunk and before the IDAT chunk.
this.transparency = {};
switch (this.colorType) {
case 3:
// Indexed color, RGB. Each byte in this chunk is an alpha for
// the palette index in the PLTE ("palette") chunk up until the
// last non-opaque entry. Set up an array, stretching over all
// palette entries which will be 0 (opaque) or 1 (transparent).
this.transparency.indexed = this.read(chunkSize);
//var short = 255 - this.transparency.indexed.length;
var short = this.transparency.indexed.length-1;
if (short > 0) {
var asc: boolean
var end1: number
for (
i = 0, end1 = short, asc = 0 <= end1; asc
? i < end1
: i > end1; asc
? i++
: i--) {
this.transparency.indexed.push(255);
}
}
break;
case 0:
// Greyscale. Corresponding to entries in the PLTE chunk.
// Grey is two bytes, range 0 .. (2 ^ bit-depth) - 1
this.transparency.grayscale = this.read(chunkSize)[0];
break;
case 2:
// True color with proper alpha channel.
this.transparency.rgb = this.read(chunkSize);
break;
}
break;
case 'tEXt':
var text = this.read(chunkSize);
var index = text.indexOf(0);
var key = String.fromCharCode(...Array.from(text.slice(0, index) || []));
this.text[key] = String.fromCharCode(...Array.from(text.slice(index + 1) || []));
break;
case 'IEND':
// we've got everything we need!
this.colors = (() => {
switch (this.colorType) {
case 0:
case 3:
case 4:
return 1;
case 2:
case 6:
return 3;
}
})();
this.hasAlphaChannel = [4, 6].includes(this.colorType);
var colors = this.colors + (
this.hasAlphaChannel
? 1
: 0);
this.pixelBitlength = this.bits * colors;
this.colorSpace = (() => {
switch (this.colors) {
case 1:
return 'DeviceGray';
case 3:
return 'DeviceRGB';
}
})();
this.imgData = new Uint8Array(imgDataBuf);
return;
break;
default:
// unknown (or unimportant) section, skip it
this.pos += chunkSize;
}
this.pos += 4; // Skip the CRC
if (this.pos > this.data.length) {
throw new Error("Incomplete or corrupt IndexedPNG file");
}
}
}
read(bytes) {
return (range(0, bytes, false).map((i) => this.data[this.pos++]));
}
readUInt32() {
const b1 = this.data[this.pos++] << 24;
const b2 = this.data[this.pos++] << 16;
const b3 = this.data[this.pos++] << 8;
const b4 = this.data[this.pos++];
return b1 | b2 | b3 | b4;
}
readUInt16() {
const b1 = this.data[this.pos++] << 8;
const b2 = this.data[this.pos++];
return b1 | b2;
}
async decodePixels() {
let data: Uint8Array
try {
data = unzlibSync(this.imgData)
} catch (err) {
throw err
}
const pixelBytes = this.pixelBitlength / 8;
const scanlineLength = pixelBytes * this.width;
const pixels = new Uint8Array(scanlineLength * this.height)
const {length} = data;
let row = 0;
let pos = 0;
let c = 0;
while (pos < length) {
var byte,
col,
i,
left,
upper;
var end;
var end1;
var end2;
var end3;
var end4;
switch (data[pos++]) {
case 0: // None
for (i = 0, end = scanlineLength; i < end; i++) {
pixels[c++] = data[pos++];
}
break;
case 1: // Sub
for (i = 0, end1 = scanlineLength; i < end1; i++) {
byte = data[pos++];
left = i < pixelBytes
? 0
: pixels[c - pixelBytes];
pixels[c++] = (byte + left) % 256;
}
break;
case 2: // Up
for (i = 0, end2 = scanlineLength; i < end2; i++) {
byte = data[pos++];
col = (i - (i % pixelBytes)) / pixelBytes;
upper = row && pixels[((row - 1) * scanlineLength) + (col * pixelBytes) + (i % pixelBytes)];
pixels[c++] = (upper + byte) % 256;
}
break;
case 3: // Average
for (i = 0, end3 = scanlineLength; i < end3; i++) {
byte = data[pos++];
col = (i - (i % pixelBytes)) / pixelBytes;
left = i < pixelBytes
? 0
: pixels[c - pixelBytes];
upper = row && pixels[((row - 1) * scanlineLength) + (col * pixelBytes) + (i % pixelBytes)];
pixels[c++] = (byte + Math.floor((left + upper) / 2)) % 256;
}
break;
case 4: // Paeth
for (i = 0, end4 = scanlineLength; i < end4; i++) {
var paeth,
upperLeft;
byte = data[pos++];
col = (i - (i % pixelBytes)) / pixelBytes;
left = i < pixelBytes
? 0
: pixels[c - pixelBytes];
if (row === 0) {
upper = (upperLeft = 0);
} else {
upper = pixels[((row - 1) * scanlineLength) + (col * pixelBytes) + (i % pixelBytes)];
upperLeft = col && pixels[((row - 1) * scanlineLength) + ((col - 1) * pixelBytes) + (i % pixelBytes)];
}
const p = (left + upper) - upperLeft;
const pa = Math.abs(p - left);
const pb = Math.abs(p - upper);
const pc = Math.abs(p - upperLeft);
if ((pa <= pb) && (pa <= pc)) {
paeth = left;
} else if (pb <= pc) {
paeth = upper;
} else {
paeth = upperLeft;
}
pixels[c++] = (byte + paeth) % 256;
}
break;
default:
throw new Error(`Invalid filter algorithm: ${data[pos - 1]}`);
}
row++;
}
return pixels
}
decodePalette() {
const {palette} = this;
const transparency = this.transparency.indexed || [];
const ret = new Uint8Array((palette.length/3) * 4)
let pos = 0;
let c = 0;
for (let i = 0, end = palette.length; i < end; i += 3) {
var left;
ret[pos++] = palette[i];
ret[pos++] = palette[i + 1];
ret[pos++] = palette[i + 2];
ret[pos++] = (left = transparency[c++]) != null
? left
: 255;
}
return ret;
}
async toPNGData(options) {
const palette = options.palette || this.decodedPalette
if (!this.decodedPixels) {
await this.decode()
}
if (options.clip) {
// Ensure some sane defaults
if (options.clip.x == undefined) options.clip.x = 0
if (options.clip.y == undefined) options.clip.y = 0
if (options.clip.w == undefined) options.clip.w = this.width - options.clip.x
if (options.clip.h == undefined) options.clip.h = this.height - options.clip.y
// Now check for user errors.
if (options.clip.x < 0 || options.clip.x >= this.width) throw new Error("clip.x is out of bounds")
if (options.clip.y < 0 || options.clip.y >= this.height) throw new Error("clip.y is out of bounds")
if (options.clip.w <= 0 || options.clip.w > this.width) throw new Error("clip.w is out of bounds")
if (options.clip.h <= 0 || options.clip.h > this.height) throw new Error("clip.h is out of bounds")
// Now we can get our clipped array.
const pixels = new Uint8ClampedArray(options.clip.w*options.clip.h * 4)
for (let x = 0; x < options.clip.w; x++) {
for (let y = 0; y < options.clip.h; y++) {
let i = (x + y * options.clip.w) * 4
let index = this.decodedPixels[(x + options.clip.x) + ( (y + options.clip.y) * this.width)] * 4
pixels[i++] = palette[index]
pixels[i++] = palette[index+1]
pixels[i++] = palette[index+2]
pixels[i++] = palette[index+3]
}
}
return { pixels: pixels, width: options.clip.w }
} else {
// Allocate RGBA buffer
const pixels = new Uint8ClampedArray(this.decodedPixels.length * 4)
let j = 0
for (let i = 0; i < this.decodedPixels.length; i++) {
let index = this.decodedPixels[i] * 4
pixels[j++] = palette[index] // R
pixels[j++] = palette[index+1] // G
pixels[j++] = palette[index+2] // B
pixels[j++] = palette[index+3] // A
}
return { pixels: pixels, width: this.width }
}
}
async toImageData(options) {
let data = await this.toPNGData(options)
return new ImageData(data.pixels, data.width)
}
async decode() {
this.decodedPalette = this.decodePalette()
this.decodedPixels = await this.decodePixels()
}
};