diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 96c2f1a..d827927 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -7,6 +7,9 @@
"": {
"name": "frontend",
"version": "0.0.0",
+ "dependencies": {
+ "fflate": "^0.8.2"
+ },
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0",
@@ -139,6 +142,17 @@
"integrity": "sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==",
"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": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@@ -691,6 +705,11 @@
"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": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -1452,6 +1471,14 @@
"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": {
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.8.tgz",
@@ -1605,6 +1632,17 @@
"integrity": "sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg==",
"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": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@@ -1929,6 +1967,11 @@
"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": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -2421,6 +2464,14 @@
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"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": {
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.8.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 8a3a423..d887841 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -20,5 +20,8 @@
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vite": "^3.0.7"
+ },
+ "dependencies": {
+ "fflate": "^0.8.2"
}
}
diff --git a/frontend/package.json.md5 b/frontend/package.json.md5
index 47ea3d2..3e95015 100644
--- a/frontend/package.json.md5
+++ b/frontend/package.json.md5
@@ -1 +1 @@
-2abc6cc777dbbae09f2d92ba2a0d453d
\ No newline at end of file
+ca3651e79b25974c2fcf59de2912276b
\ No newline at end of file
diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte
index 2a82cac..5797959 100644
--- a/frontend/src/App.svelte
+++ b/frontend/src/App.svelte
@@ -16,6 +16,7 @@
import { Close } from "carbon-icons-svelte"
import StackPreview from './sections/StackPreview.svelte'
+ import type { Canvas } from './types/canvas'
let theme: 'white'|'g10'|'g80'|'g90'|'g100' = 'g90'
@@ -25,9 +26,9 @@
let showImport: boolean = false
let importValid: boolean = false
- let importImage: HTMLImageElement = null
let importFile: data.StackistFileV1 = null
let importFilepath: string = ''
+ let importCanvas: Canvas = null
let showPreview: boolean = false
@@ -41,7 +42,7 @@
filepath: importFilepath,
title: importFilepath,
data: importFile,
- image: importImage,
+ canvas: importCanvas,
}]
console.log(files)
}
@@ -86,7 +87,7 @@
{#each files as file}
-
+
{/each}
@@ -108,8 +109,8 @@
bind:open={showImport}
bind:valid={importValid}
bind:file={importFile}
- bind:img={importImage}
bind:filepath={importFilepath}
+ bind:canvas={importCanvas}
/>
diff --git a/frontend/src/sections/Editor2D.svelte b/frontend/src/sections/Editor2D.svelte
index ca28ba0..3de45f8 100644
--- a/frontend/src/sections/Editor2D.svelte
+++ b/frontend/src/sections/Editor2D.svelte
@@ -2,8 +2,9 @@
import { onMount } from 'svelte'
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 frame: data.Frame
export let layer: data.Layer
@@ -44,8 +45,8 @@
}
if (offsetX === undefined || offsetY === undefined) {
// Adjust offset to center image on first LOAD.
- offsetX = rootCanvas.width/2 - img.width/2
- offsetY = rootCanvas.height/2 - img.height/2
+ offsetX = rootCanvas.width/2 - file.canvas.width/2
+ offsetY = rootCanvas.height/2 - file.canvas.height/2
}
}
@@ -78,15 +79,14 @@
ctx.save()
ctx.imageSmoothingEnabled = false
ctx.scale(zoom, zoom)
- //ctx.transform(1, 0, 0, 1, -img.width/2, -img.height/2)
{
ctx.beginPath()
ctx.fillStyle = '#888888'
- ctx.rect(offsetX, offsetY, img.width, img.height)
+ ctx.rect(offsetX, offsetY, file.canvas.width, file.canvas.height)
ctx.fill()
- let rows = img.height / checkerboardSize
- let cols = img.width / checkerboardSize
+ let rows = file.canvas.height / checkerboardSize
+ let cols = file.canvas.width / checkerboardSize
ctx.beginPath()
ctx.fillStyle = '#444444'
for (let r = 0; r < rows; r++) {
@@ -105,7 +105,7 @@
}
// TODO: Draw the current layer of the current frame.
- ctx.drawImage(img, offsetX, offsetY)
+ ctx.drawImage(file.canvas.canvas, offsetX, offsetY)
ctx.restore()
}
@@ -134,13 +134,13 @@
}
function capOffset() {
- if (offsetX < -img.width+30) {
- offsetX = -img.width+30
+ if (offsetX < -file.canvas.width+30) {
+ offsetX = -file.canvas.width+30
} else if (offsetX > canvas.width-30) {
offsetX = canvas.width-30
}
- if (offsetY < -img.height+30) {
- offsetY = -img.height+30
+ if (offsetY < -file.canvas.height+30) {
+ offsetY = -file.canvas.height+30
} else if (offsetY > canvas.height-30) {
offsetY = canvas.height-30
}
diff --git a/frontend/src/sections/Importer.svelte b/frontend/src/sections/Importer.svelte
index 48b2db2..604cc83 100644
--- a/frontend/src/sections/Importer.svelte
+++ b/frontend/src/sections/Importer.svelte
@@ -3,6 +3,9 @@
import { data } from '../../wailsjs/go/models.js'
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 { Form, FormGroup, InlineNotification, Tile, Truncate } from 'carbon-components-svelte'
import { Grid, Row, Column } from "carbon-components-svelte"
@@ -30,7 +33,8 @@
let rowBasedFrames: boolean = true
export let file: data.StackistFileV1
export let filepath: string = ''
- export let img: HTMLImageElement
+ export let canvas: Canvas
+ let img: HTMLImageElement
let path: string = ''
let error: string = ""
let error2: string = ""
@@ -40,7 +44,7 @@
let groups: number = 0
let animations: number = 0
- function loadImage(base64: number[]): Promise {
+ function loadImage(base64: string): Promise {
return new Promise((resolve, reject) => {
img = new Image()
img.onload = () => resolve(img)
@@ -52,9 +56,45 @@
async function openFile() {
try {
filepath = await GetFilePath()
- let bytes = await OpenFileBytes(filepath)
+ let bytes = (await OpenFileBytes(filepath)) as unknown as string
path = /[^/\\]*$/.exec(filepath)[0]
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()
} catch(err) {
error = "open"
diff --git a/frontend/src/sections/StackPreview.svelte b/frontend/src/sections/StackPreview.svelte
index 88e48e7..16118df 100644
--- a/frontend/src/sections/StackPreview.svelte
+++ b/frontend/src/sections/StackPreview.svelte
@@ -38,7 +38,7 @@
ctx.save()
ctx.translate(x, y)
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()
y -= 1 * layerDistance
}
diff --git a/frontend/src/types/canvas.ts b/frontend/src/types/canvas.ts
new file mode 100644
index 0000000..f0658a0
--- /dev/null
+++ b/frontend/src/types/canvas.ts
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/types/file.ts b/frontend/src/types/file.ts
index b69dc31..9399327 100644
--- a/frontend/src/types/file.ts
+++ b/frontend/src/types/file.ts
@@ -1,8 +1,9 @@
import type { data } from '../../wailsjs/go/models.ts'
+import type { Canvas } from './canvas.ts'
export class LoadedFile {
filepath: string
title: string
- image: HTMLImageElement
+ canvas: Canvas
data: data.StackistFileV1
}
\ No newline at end of file
diff --git a/frontend/src/types/png.ts b/frontend/src/types/png.ts
new file mode 100644
index 0000000..410e832
--- /dev/null
+++ b/frontend/src/types/png.ts
@@ -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()
+ }
+};