Implement custom PNG decode; use WIP weirdness
parent
26e119421d
commit
a4ee106ffe
|
@ -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",
|
||||
|
|
|
@ -20,5 +20,8 @@
|
|||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"fflate": "^0.8.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
2abc6cc777dbbae09f2d92ba2a0d453d
|
||||
ca3651e79b25974c2fcf59de2912276b
|
|
@ -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 @@
|
|||
<svelte:fragment slot="content">
|
||||
{#each files as file}
|
||||
<TabContent>
|
||||
<Editor2D img={file.image} refresh={refresh} />
|
||||
<Editor2D bind:file={file} refresh={refresh} />
|
||||
</TabContent>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
|
@ -108,8 +109,8 @@
|
|||
bind:open={showImport}
|
||||
bind:valid={importValid}
|
||||
bind:file={importFile}
|
||||
bind:img={importImage}
|
||||
bind:filepath={importFilepath}
|
||||
bind:canvas={importCanvas}
|
||||
/>
|
||||
</ComposedModal>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<HTMLImageElement> {
|
||||
function loadImage(base64: string): Promise<HTMLImageElement> {
|
||||
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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue