diff --git a/.gitmodules b/.gitmodules index 223631db..0a720b50 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "src/handlers/terraria-wld-parser"] path = src/handlers/terraria-wld-parser url = https://github.com/ConnorTippets/terraria-world-file-ts.git +[submodule "src/handlers/gimper"] + path = src/handlers/gimper + url = https://github.com/ConnorTippets/gimper.git diff --git a/src/handlers/gimper b/src/handlers/gimper new file mode 160000 index 00000000..fe96bd9e --- /dev/null +++ b/src/handlers/gimper @@ -0,0 +1 @@ +Subproject commit fe96bd9e8efa33cbb8b23f134207ecf3e6dfecbd diff --git a/src/handlers/index.ts b/src/handlers/index.ts index e8ade25b..4058d2ad 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -59,6 +59,7 @@ import tarHandler from "./tar.ts"; import otaHandler from "./ota.ts"; import comicsHandler from "./comics.ts"; import terrariaWldHandler from "./terrariawld.ts"; +import xcfHandler from "./xcf.ts"; const handlers: FormatHandler[] = []; try { handlers.push(new svgTraceHandler()) } catch (_) { }; @@ -123,5 +124,6 @@ try { handlers.push(new tarHandler()) } catch (_) { }; try { handlers.push(new otaHandler()) } catch (_) { }; try { handlers.push(new comicsHandler()) } catch (_) { }; try { handlers.push(new terrariaWldHandler()) } catch (_) { }; +try { handlers.push(new xcfHandler()) } catch (_) { }; export default handlers; diff --git a/src/handlers/xcf.ts b/src/handlers/xcf.ts new file mode 100644 index 00000000..38f57907 --- /dev/null +++ b/src/handlers/xcf.ts @@ -0,0 +1,120 @@ +import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts"; +import CommonFormats from "src/CommonFormats.ts"; +import XCF from "./gimper/src/main.js"; + +class xcfHandler implements FormatHandler { + + public name: string = "xcf"; + public supportedFormats?: FileFormat[]; + public ready: boolean = false; + + #canvas?: HTMLCanvasElement; + #ctx?: CanvasRenderingContext2D; + + async init() { + this.supportedFormats = [ + { + name: "eXperimental Computing Facility (GIMP)", + format: "xcf", + extension: "xcf", + mime: "image/x-xcf", + from: true, + to: false, + internal: "xcf", + category: "image", + lossless: true + }, + CommonFormats.PNG.builder("png") + .markLossless() + .allowFrom(false) + .allowTo(true), + ]; + + this.#canvas = document.createElement("canvas"); + const ctx = this.#canvas.getContext("2d"); + if (!ctx) { + throw new Error("Failed to create 2D rendering context."); + } + this.#ctx = ctx; + + this.ready = true; + } + + async doConvert( + inputFiles: FileData[], + inputFormat: FileFormat, + outputFormat: FileFormat + ): Promise { + if (!this.ready || !this.#canvas || !this.#ctx) { + throw new Error("Handler not initialized!"); + } + + const outputFiles: FileData[] = []; + + if (inputFormat.internal !== "xcf" || outputFormat.internal !== "png") { + throw Error("Invalid input/output format."); + } + + for (const inputFile of inputFiles) { + const xcf = await XCF.from_bytes(new Uint8Array(inputFile.bytes)); + + if (xcf.layers.length === 0) { + throw Error("No layers to convert."); + } + + for (let i = 0; i < xcf.layers.length; i++) { + const layer = xcf.layers[i]; + const bpp = layer.hierarchy.bpp; + + if (![3, 4].includes(bpp)) { + throw Error("Only RGB and RGBA in 8-bit precision is supported."); + } + + this.#canvas.width = layer.width; + this.#canvas.height = layer.height; + this.#ctx.clearRect(0, 0, layer.width, layer.height); + + const pixel_data = await xcf.getLayerPixels(i); + + const image_data = this.#ctx.createImageData(layer.width, layer.height); + + for (let y = 0; y < layer.height; y++) { + for (let x = 0; x < layer.width; x++) { + const pixel = pixel_data[y][x]; + const [r, g, b] = bpp === 4 ? pixel.slice(0, -1) : pixel; + + let a = 255; + if (bpp === 4) { + a = pixel.at(-1)!; + } + + const i = (y * layer.width + x) * 4; + image_data.data[i] = r; + image_data.data[i + 1] = g; + image_data.data[i + 2] = b; + image_data.data[i + 3] = a; + } + } + + this.#ctx.putImageData(image_data, 0, 0); + + const bytes: Uint8Array = await new Promise((resolve, reject) => { + this.#canvas!.toBlob(blob => { + if (!blob) { + return reject("Canvas output failed"); + } + blob.arrayBuffer().then(buffer => resolve(new Uint8Array(buffer))); + }, "image/png"); + }); + + const name = inputFile.name.split(".").slice(0, -1).join(".") + `_${layer.name}` + "." + outputFormat.extension; + outputFiles.push({ bytes, name }); + } + } + + return outputFiles; + } + +} + +export default xcfHandler; \ No newline at end of file