diff --git a/src/handlers/aperturePicture.ts b/src/handlers/aperturePicture.ts index e1c2ac83..84f67661 100644 --- a/src/handlers/aperturePicture.ts +++ b/src/handlers/aperturePicture.ts @@ -1,28 +1,41 @@ -import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts"; +import { + initializeImageMagick, + Magick, + MagickFormat, + MagickImageCollection, + MagickReadSettings, + MagickGeometry, + QuantizeSettings +} from "@imagemagick/magick-wasm"; + +import type { + FileData, + FileFormat, + FormatHandler +} from "../FormatHandler.ts"; import CommonFormats from "src/CommonFormats.ts"; class aperturePictureHandler implements FormatHandler { public name: string = "aperturePicture"; - public supportedFormats?: FileFormat[]; + public supportedFormats ? : FileFormat[]; public ready: boolean = false; async init() { - this.supportedFormats = [ - { + this.supportedFormats = [{ name: "Aperture Picture Format", format: "apf", extension: "apf", mime: "image/x-aperture-picture", from: true, - to: false, + to: true, internal: "apf", category: ["image"], lossless: true, }, CommonFormats.BMP.builder("bmp") - .allowFrom(false) - .allowTo(true) - .markLossless(), + .allowFrom(true) + .allowTo(true) + .markLossless(), ]; this.ready = true; } @@ -31,27 +44,67 @@ class aperturePictureHandler implements FormatHandler { inputFiles: FileData[], inputFormat: FileFormat, outputFormat: FileFormat, - ): Promise { + ): Promise < FileData[] > { const outputFiles: FileData[] = []; const decoder = new TextDecoder(); - for (const file of inputFiles) { - const text = decoder.decode(file.bytes); - const lines = text.split(/\r?\n/); - if (lines[0] !== "APERTURE IMAGE FORMAT (c) 1985") - throw new Error("File is not an APF file"); - - const SK = parseInt(lines[1]); - const data = lines.slice(2).join(""); - const bitmap = decodeAPF(data, SK); - const bmp = bitmapTo1BitBMP(bitmap, 320, 200); - - outputFiles.push({ - bytes: bmp, - name: file.name.replace(/\.[^/.]+$/, "") + ".bmp", - }); - } + if (inputFormat.internal === "apf") { + for (const file of inputFiles) { + const text = decoder.decode(file.bytes); + const lines = text.split(/\r?\n/); + if (lines[0] !== "APERTURE IMAGE FORMAT (c) 1985") + throw new Error("File is not an APF file"); + + const SK = parseInt(lines[1]); + const data = lines.slice(2).join(""); + const bitmap = decodeAPF(data, SK); + const bmp = bitmapTo1BitBMP(bitmap, 320, 200); + + outputFiles.push({ + bytes: bmp, + name: file.name.replace(/\.[^/.]+$/, "") + ".bmp", + }); + } + } else if (inputFormat.internal === "bmp") { // we're just throwing science at the wall to see what sticks + const w = 320, + h = 200; + await initializeImageMagick(); + + const inputMagickFormat = inputFormat.internal as MagickFormat; + const inputSettings = new MagickReadSettings(); + inputSettings.format = inputMagickFormat; + + for (const inputFile of inputFiles) { + MagickImageCollection.use(fileCollection => { + fileCollection.read(inputFile.bytes); + for (const image of fileCollection) { + if (!image) break; + // this is to crush the image into 320x200 with 2 colors. + image.resize(new MagickGeometry(320, 200)); + image.grayscale(); + const Qset = new QuantizeSettings(); + Qset.colors = 2; + Qset.ditherMethod = 1; + image.quantize(Qset); // 2 colors + + let data = null // now you're thinking with NoneTypes + image.getPixels(pixels => { + data = pixels.toByteArray(0, 0, 320, 200, "r") + }); + + const apf = encodeAPF(data); // can we get some form of output? + outputFiles.push({ + bytes: apf, + name: inputFile.name.replace(/\.[^/.]+$/, "") + ".apf", + }); + break + }; + }); + }; + } else { + throw new Error("Input not APF or BMP") + } return outputFiles; } } @@ -88,6 +141,65 @@ function decodeAPF(data: string, SK: number): Uint8Array { return bmp; } +// split array into 200 arrays that are then reversed, converted into 255s and 0s, and turned back into 1 array +function APFarray(data: Uint8Array): Uint8Array { + let newarray = []; + let temparray = []; + let pal = []; + let ispalflipped = false; + for (const b of data) { + if (!pal.includes(b)) { // get used colors + pal.push(b) + } + if (pal.length === 2) { + break + } + } + if (pal[0] > pal[1]) { + ispalflipped = true + } + for (const b of data) { + let uh = 0 + if (ispalflipped) { + uh = (1 - pal.indexOf(b)) * 255 + } else { + uh = pal.indexOf(b) * 255 + } + temparray.push(uh) + if (temparray.length === 320) { + newarray.push(temparray) + temparray = [] // clear temp array and add it to the new array + } + } + + let revarray = newarray.reverse().flat() + return revarray; +} + +function encodeAPF(data: Uint8Array): String { + let apf = "APERTURE IMAGE FORMAT (c) 1985\n1\n" // header and ls of 1 + const q_data = APFarray(data); + console.log(q_data) + let runlen = 0 + let currun = 0 + + for (const p of q_data) { + if (p === currun) { + runlen += 1 + if (runlen == 94) { + runlen = 0; + apf += "~ " + } + } else { + apf += String.fromCharCode(runlen + 32) + currun = p + runlen = 1 + } + } + apf += String.fromCharCode(runlen + 32); + return apf; +} + function bitmapTo1BitBMP( // note i initially used 24-bit BMPs but the filesize was much larger for a b&w image so i decided to go with a 1-bit BMP. unfortunately this means the code to make it is much larger because i have to create a larger header for the colour palette and i have to do some bit shifting because of the nature of writing to bits instead of bytes whereas I could just write in a loop for 24-bit. worth it for the smaller file size though trust me bitmap: Uint8Array, width: number, @@ -144,5 +256,4 @@ function bitmapTo1BitBMP( // note i initially used 24-bit BMPs but the filesize export default aperturePictureHandler; -// logical next step is to go from BMP to APF but that is far beyond my level of knowledge. if anyone wants to take a crack at it the original basic code is in the old ARG Wiki at http://portalwiki.asshatter.org/index.php/Aperture_Image_Format.html#GW-Basic_AMF.2FAPF_Viewer_Source -// if anyone wants to implement basic 1-bit colour, the .amf format is very simple, covered at http://portalwiki.asshatter.org/index.php/Aperture_Menu_Format.html. All you'd need to implement that is to check line 0 is APERTURE MENU FORMAT (c) 1985 and then line 1 is the colour info, comma separated. Idk what the RGB mappings for them are but the number meanings are at https://en.wikibooks.org/wiki/QBasic/Text_Output#Color_by_Number \ No newline at end of file +// if anyone wants to implement basic 1-bit colour, the .amf format is very simple, covered at http://portalwiki.asshatter.org/index.php/Aperture_Menu_Format.html. All you'd need to implement that is to check line 0 is APERTURE MENU FORMAT (c) 1985 and then line 1 is the colour info, comma separated. Idk what the RGB mappings for them are but the number meanings are at https://en.wikibooks.org/wiki/QBasic/Text_Output#Color_by_Number