Implement custom PNG decode; use WIP weirdness
parent
26e119421d
commit
a4ee106ffe
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
2abc6cc777dbbae09f2d92ba2a0d453d
|
ca3651e79b25974c2fcf59de2912276b
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { 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
|
||||||
}
|
}
|
|
@ -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