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() + } +};