Add mural compression

This commit is contained in:
2025-02-11 22:18:40 +01:00
parent 8d5cdf79d8
commit 006786ea29
11 changed files with 550 additions and 163 deletions
+7 -6
View File
@@ -1,10 +1,10 @@
import React from "react";
import { Store } from "../../lib/store";
import { Mural, MuralEx, MuralStatus } from "../../interfaces";
import { MuralEx, MuralOld, MuralStatus } from "../../interfaces";
import styled from "styled-components";
import { A, Border, Btn, Flex, SELECTED_COLOR } from "../styles";
import { CanvasToCanvasJSX } from "./canvasToCanvasJSX";
import { formatNumber, getMuralHeight, getMuralWidth, getPixelStatusMural } from "../../lib/utils";
import { convertOldMuralToNewMural, formatNumber, getMuralHeight, getMuralWidth, getPixelStatusMural } from "../../lib/utils";
import {
IconDefinition, faDownload, faLayerGroup, faLocation, faPenToSquare, faRefresh, faTrash
} from "@fortawesome/free-solid-svg-icons";
@@ -14,7 +14,7 @@ import { BIN_FORMATS } from "../importMural";
import saveAs from "file-saver";
import { Popup } from "./Popup";
import { Palette } from "../../lib/palette";
import { serializeMural } from "../serializer";
import { Mural } from "../../lib/mural";
const Margin = styled.div`
margin: 2px;
@@ -212,14 +212,15 @@ export class MuralView extends React.Component<Props, State> {
// this.props.store.updateMural(this.props.mural);
// }
};
onExport = () => {
const rawMural: Mural = {
onExport = async () => {
const rawMural: MuralOld = {
name: this.props.mural.name,
x: this.props.mural.x,
y: this.props.mural.y,
pixels: this.props.mural.pixels,
};
const buffer = serializeMural(rawMural);
const mural = convertOldMuralToNewMural(rawMural);
const buffer = await mural.getBuffer();
const blob = new Blob([buffer], { type: "octet/stream" });
const saveName = rawMural.name
+35 -9
View File
@@ -1,6 +1,6 @@
import React from "react";
import { Mural, RGB } from "../interfaces";
import { MuralOld, RGB } from "../interfaces";
import { canvasToImageData, get2DArrHeight, get2DArrWidth, getColorScore, getExtension,
imageDataToPaletteIndices, imageToCanvas, loadImageSource, processNumberEvent,
readAsArrayBuffer,
@@ -14,7 +14,7 @@ import { Palette } from "../lib/palette";
import { FileInput } from "../lib/fileinput";
import { Store } from "../lib/store";
import { Coordinates } from "../lib/coordinates";
import { deserializeMural } from "./serializer";
import { Mural } from "../lib/mural";
export const TEXT_FORMATS = ["muraljson", "json"];
export const BIN_FORMATS = ["pcm", "bin"];
@@ -89,11 +89,11 @@ export async function importArtWork(store: Store, cords: Coordinates, palette: P
const file = await importFile();
if (file) {
if (file.type === "mural") {
return file.data as Mural;
return file.data as MuralOld;
} else {
const img = file.data as HTMLImageElement;
const pixels = await imageToMural(img, palette);
const mural = await new Promise<Mural>((resolve, reject)=> {
const mural = await new Promise<MuralOld>((resolve, reject)=> {
store.setOverlayModify({
pixels,
muralObj: {
@@ -483,23 +483,49 @@ async function importFile() {
const name = ex.text;
if (TEXT_FORMATS.includes(etn)) {
const content = await readAsString(fileData);
const mural = JSON.parse(content) as Mural;
const mural = JSON.parse(content) as MuralOld;
if (!mural.name) {
mural.name = await Popup
.prompt("Missing name for this mural. Please enter it manually", name) || "";
}
validateMural(mural);
return {
type: "mural",
data: mural,
};
} else if (BIN_FORMATS.includes(etn)) {
const buffer = await readAsArrayBuffer(fileData);
const mural = deserializeMural(buffer);
validateMural(mural);
const mural = await Mural.from(new Uint8Array(buffer));
const pixelBuffer = mural.pixelBuffer;
const pixels = Array.from({ length: mural.h }, () => Array(mural.w).fill(0));
let i = 0;
let yy = -1;
leg:
for (;;) {
yy++;
if (pixelBuffer[i] === undefined) {
break;
}
// const ref = [];
// pixels.push(ref);
for (let xx = 0; xx < mural.w; xx++) {
const item = pixelBuffer[i++];
if (item != undefined) {
pixels[yy][xx] = item;
if(item > 15) {
console.log(item);
}
//ref.push(item);
} else {
break leg;
}
}
}
return {
type: "mural",
data: mural,
data: {
name, x: mural.x, y: mural.y, pixels,
},
};
} else {
const readData = await readAsDataUrl(fileData);
-123
View File
@@ -1,123 +0,0 @@
import { Mural } from "../interfaces";
const MAGIC_REPEATING = 200;
const TRANSPARENT = 201;
const VERSION = 1;
export function compressRepeatingArray(index: number, buffer: number[] /* 0xFF */) {
const a = buffer[index];
const b = buffer[index + 1];
const c = buffer[index + 2];
const pixelIndex = a === -1 ? TRANSPARENT : a;
if (a === b && b === c) {
let i = 0;
for (; i < Math.min(buffer.length - index, 0xFF); i++) {
if (a !== buffer[index + i]) {
break;
}
}
return [MAGIC_REPEATING, i - 1, pixelIndex];
}
return [pixelIndex];
}
export function decompressRepeatingArray(buffer: ArrayLike<number>) {
const output: number[] = [];
const addValue = (value: number) => {
output.push(value === TRANSPARENT ? -1 : value);
};
for (let i = 0; i < buffer.length; i++) {
const item = buffer[i];
if (item === MAGIC_REPEATING) {
const length = buffer[++i];
const value = buffer[++i];
for (let j = 0; j < length; j++) {
addValue(value);
}
} else {
addValue(item);
}
}
return output;
}
export function serializeMural(mural: Mural) {
const encoder = new TextEncoder();
const nameBinary = encoder.encode(mural.name);
const array: number[] = [];
for (let i = 0; i < mural.pixels.length; i++) {
for (let j = 0; j < mural.pixels[i].length; j++) {
array.push(mural.pixels[i][j]);
}
}
const pixelEncodeBuffer: number[] = [];
for (let i = 0; i < array.length; i++) {
const rtn = compressRepeatingArray(i, array);
if (rtn.length === 1) {
pixelEncodeBuffer.push(rtn[0]);
} else {
pixelEncodeBuffer.push(rtn[0], rtn[1], rtn[2]);
i += rtn[1] - 1;
}
}
const pixelBuffer = new Uint8Array(pixelEncodeBuffer);
const firstHalf = 1 + 4 + 4 + 1 + 2 + nameBinary.byteLength;
const buffer = new ArrayBuffer(firstHalf + pixelBuffer.byteLength);
const view = new DataView(buffer);
view.setUint8(0, VERSION);
view.setInt32(1, mural.x, true);
view.setInt32(5, mural.y, true);
view.setUint16(9, mural.pixels[0].length, true);
view.setUint8(11, nameBinary.length);
new Uint8Array(buffer).set(nameBinary, 12);
new Uint8Array(buffer).set(pixelBuffer, firstHalf);
return buffer;
}
export function deserializeMural(buffer: ArrayBuffer): Mural {
const view = new DataView(buffer);
const version = view.getUint8(0);
if (version !== VERSION) {
throw new Error("Unknown version");
}
const x = view.getInt32(1, true);
const y = view.getInt32(5, true);
const width = view.getUint16(9, true);
const nameLength = view.getUint8(11);
const nameBytes = new Uint8Array(buffer, 12, nameLength);
const decoder = new TextDecoder();
const name = decoder.decode(nameBytes);
const pixelBuffer = new Uint8Array(buffer, 12 + nameLength);
const pixelArray: number[] = decompressRepeatingArray(pixelBuffer);
const pixels: number[][] = [];
leg:
for (;;) {
if (!pixelArray.length) {
break;
}
const ref: number[] = [];
pixels.push(ref);
for (let j = 0; j < width; j++) {
const item = pixelArray.shift();
if (item != undefined) {
ref.push(item);
} else {
break leg;
}
}
}
return {
name,
x,
y,
pixels
} as Mural;
}
+3
View File
@@ -6,8 +6,11 @@ import { waitForDraw } from "./lib/utils";
import { Storage } from "./lib/storage";
import { Store } from "./lib/store";
import { PixelPlaced } from "./lib/pixelPlaced";
import process from "process";
async function main() {
globalThis.process = process;
const palette = new Palette();
while(!palette.init()) { await waitForDraw();}
const storage = new Storage(ENVIRONMENT === "browser-extension");
+2 -2
View File
@@ -4,7 +4,7 @@ export interface RGB {
b: number;
}
export interface Mural {
export interface MuralOld {
name: string;
pixels: number[][];
x: number;
@@ -28,7 +28,7 @@ export interface SelectedMural {
h: number;
}
export interface MuralEx extends Mural {
export interface MuralEx extends MuralOld {
ref: HTMLCanvasElement;
pixelCount: number;
}
+15 -7
View File
@@ -56,7 +56,10 @@ export class Coordinates {
private get centerCanvas() {
const hww = window.innerWidth / 2;
const hhw = window.innerHeight / 2;
const canvasObject = [...document.getElementsByTagName("canvas")].filter(c => c.classList.contains("leaflet-tile")).map(c => {
const canvasObject = [
...document.getElementsByTagName("canvas")]
.filter(c => c.classList.contains("leaflet-tile"))
.map(c => {
const bounds = c.getBoundingClientRect();
return {
canvas: c,
@@ -70,6 +73,7 @@ export class Coordinates {
},
};
}).find(r => hww > r.bounds.left && hww < r.bounds.right && hhw > r.bounds.top && hhw < r.bounds.bottom);
if (canvasObject) {
const ratio = canvasObject.canvas.width / CHUNK_SIZE;
if (ratio === 2) { // scale -1
@@ -137,12 +141,16 @@ export class Coordinates {
const pathNames = location.pathname.split("/").filter(e => e);
const cordsRaw = pathNames[0];
if (cordsRaw) {
const cords = cordsRaw.match(/-?\d+/g)!.map(n => parseInt(n));
if (typeof cords[0] === "number" && typeof cords[1] === "number" && typeof cords[2] === "number") {
this._ux = cords[0];
this._uy = cords[1];
this._uScale = cords[2];
return cords;
const cordsMatch = cordsRaw.match(/-?\d+/g);
if (cordsMatch) {
const cords = cordsMatch.map(n => parseInt(n));
if (typeof cords[0] === "number" && typeof cords[1] === "number" && typeof cords[2] === "number") {
this._ux = cords[0];
this._uy = cords[1];
this._uScale = cords[2];
return cords;
}
console.log(cordsMatch);
}
}
return null;
+178
View File
@@ -0,0 +1,178 @@
import zlib from "zlib";
import { Buffer } from "buffer";
export class Mural {
private static readonly ENCODE_VERSION = 1;
constructor(
private _name: string,
private _x: number,
private _y: number,
private _w: number,
private _h: number,
private _b: Int8Array
) {
this.validate();
}
getBuffer() {
this.validate();
const encoder = new TextEncoder();
const nameBinary = encoder.encode(this._name);
const pixelBuffer = this._b;
const firstHalf = 1 + 4 + 4 + 1 + 2 + 2 + 1 + nameBinary.byteLength;
const buffer = new ArrayBuffer(firstHalf + pixelBuffer.byteLength);
const view = new DataView(buffer);
view.setUint8(0, Mural.ENCODE_VERSION);
view.setInt32(1, this._x);
view.setInt32(5, this._y);
view.setUint16(9, this._h);
view.setUint16(11, this._w);
view.setUint8(13, nameBinary.length);
new Uint8Array(buffer).set(nameBinary, 15);
new Uint8Array(buffer)
.set(new Uint8Array(this._b.buffer, this._b.byteOffset, this._b.byteLength), firstHalf);
return new Promise<Uint8Array>((resolve, reject) => {
zlib.deflate(Buffer.from(buffer), (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
static async from(raw: Uint8Array) {
const buffer = await new Promise<Buffer>((resolve, reject) => {
zlib.inflate(raw, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
const view = new DataView(buffer.buffer);
const version = view.getUint8(0);
if (version !== Mural.ENCODE_VERSION) {
throw new Error("Unknown version");
}
const x = view.getInt32(1);
const y = view.getInt32(5);
const height = view.getUint16(9);
const width = view.getUint16(11);
const nameLength = view.getUint8(13);
const nameBytes = buffer.subarray(15, 15 + nameLength);
const decoder = new TextDecoder();
const name = decoder.decode(nameBytes);
const pixelBuffer = new Int8Array(
buffer.buffer,
buffer.byteOffset + 15 + nameLength,
buffer.byteLength - (15 + nameLength)
);
return new Mural(name, x, y, width, height, pixelBuffer);
}
private validate2() {
try {
this.validate();
return true;
} catch (_) {
return false;
}
}
private validate() {
if (this._w * this._h > 0x7FFFFFFF) {
throw new Error("Size too big");
}
if (this._w > 0xFFFF) {
throw new Error("Width to big");
}
if (this._h > 0xFFFF) {
throw new Error("Height to big");
}
if (Math.abs(this._x) > 0x7FFFFFFF) {
throw new Error("X to big");
}
if (Math.abs(this._y) > 0x7FFFFFFF) {
throw new Error("X to big");
}
}
getPixel(x: number, y: number) {
return this._b[this.getIndex(x, y)];
}
setPixel(x: number, y: number, value: number) {
this._b[this.getIndex(x, y)] = value;
}
private getIndex(x: number, y: number) {
return (y * this._w) + x;
}
private fixBuffer() {
const size = this._w * this._h;
if (this._b.length !== size){
const copy = this._b;
this._b = new Int8Array(size);
this._b.set(copy.subarray(0, size));
}
}
get name() {
return this._name;
}
set name(value: string) {
const copy = this._name;
this._name = value;
if (!this.validate2()) {
this._name = copy;
}
}
get x() {
return this._x;
}
set x(value: number) {
const copy = this._x;
this._x = value;
if (!this.validate2()) {
this._x = copy;
}
}
get y() {
return this._y;
}
set y(value: number) {
const copy = this._y;
this._y = value;
if (!this.validate2()) {
this._y = copy;
}
}
get w() {
return this._w;
}
set w(value: number) {
const copy = this._w;
this._w = value;
if (!this.validate2()) {
this.fixBuffer();
} else {
this._w = copy;
}
}
get h() {
return this._h;
}
set h(value: number) {
const copy = this._h;
this._h = value;
if (!this.validate2()) {
this.fixBuffer();
} else {
this._h = copy;
}
}
get pixelBuffer() {
return this._b;
}
}
+25 -5
View File
@@ -1,6 +1,7 @@
import { clone, isInteger } from "lodash";
import { Mural, MuralStatus, RGB } from "../interfaces";
import { MuralOld, MuralStatus, RGB } from "../interfaces";
import { fetchCombineTiledImage } from "./canvashot";
import { Mural } from "./mural";
export const CHUNK_SIZE = 512;
@@ -129,11 +130,11 @@ export function get2DArrWidth(arr2D: number[][]) {
return (arr2D[0] && arr2D[0].length) || 0;
}
export function getMuralHeight(mural: Mural) {
export function getMuralHeight(mural: MuralOld) {
return get2DArrHeight(mural.pixels);
}
export function getMuralWidth(mural: Mural) {
export function getMuralWidth(mural: MuralOld) {
return get2DArrWidth(mural.pixels);
}
@@ -161,7 +162,7 @@ export function canvasToImage(canvas: HTMLCanvasElement, alt?: string) {
});
}
export function validateMural(mural: Mural) {
export function validateMural(mural: MuralOld) {
if (typeof mural === "object") {
const muralClone = clone(mural);
if (Array.isArray(muralClone)) {
@@ -239,7 +240,7 @@ export function flatQuantizeImageData(imageData: ImageData, palette: RGB[]) {
}
}
export async function getPixelStatusMural(mural: Mural, palette: RGB[]): Promise<MuralStatus> {
export async function getPixelStatusMural(mural: MuralOld, palette: RGB[]): Promise<MuralStatus> {
const width = getMuralWidth(mural);
const height = getMuralHeight(mural);
const tile = await fetchCombineTiledImage(mural.x, mural.y, width, height);
@@ -424,3 +425,22 @@ export async function fetchTile(x: number, y: number) {
});
});
}
export function isOldMural(mural: MuralOld | Mural): mural is MuralOld {
return "pixels" in mural;
}
export function convertOldMuralToNewMural(mural: MuralOld) {
const height = mural.pixels.length;
const width = mural.pixels[0].length;
const predictedSize = mural.pixels.length * mural.pixels[0].length;
const array = new Int8Array(predictedSize);
let k = 0;
for (let i = 0; i < mural.pixels.length; i++) {
for (let j = 0; j < mural.pixels[i].length; j++) {
array[k++] = mural.pixels[i][j];
}
}
return new Mural(mural.name, mural.x, mural.y, width, height, array);
}