leonitousconforti/tinyburg

View on GitHub
packages/doorman/templates/generate-font-characters.ts

Summary

Maintainability
A
0 mins
Test Coverage
import type { ICropRegion } from "../src/image-operations/crop-image.js";
import type { Image } from "../src/image-operations/image.js";

import fs from "node:fs";
import path from "node:url";
import sharp from "sharp";
import { addChannel } from "../src/image-operations/add-channels.js";
import { dropChannel } from "../src/image-operations/drop-channel.js";
import { grayscaleImage } from "../src/image-operations/grayscale-image.js";
import { ImageType, imageTypeFromChannelsForPng } from "../src/image-operations/image.js";
import { maskImage } from "../src/image-operations/mask-image.js";
import { thresholdImage } from "../src/image-operations/threshold-image.js";

// Load the silkscreen image and metadata. These need to be extracted from the game's
// resource files, there are lots of free open source tools out there that do this.
import silkscreen from "./silkscreen.json";
const silkscreenImage = sharp(path.fileURLToPath(new URL("silkscreen.png", import.meta.url)));
const silkscreenMetadata = await silkscreenImage.metadata();

// Ensure that all output folders have been created
for (const folder of ["./png", "./raw", "./metadata"]) {
    const folderPath = path.fileURLToPath(new URL(folder, import.meta.url));
    if (!fs.existsSync(folderPath)) fs.mkdirSync(folderPath, { recursive: true });
}

// For every character on the silkscreen
for (const silkscreenData of silkscreen) {
    const characterInSilkscreenRegion: ICropRegion = {
        top: silkscreenData.y,
        left: silkscreenData.x,
        width: Math.min(silkscreenMetadata.width! - silkscreenData.x, silkscreenData.width!),
        height: Math.min(silkscreenMetadata.height! - silkscreenData.y, silkscreenData.height!),
    };

    // The space character actually has a width and height of zero
    // which will cause problems later, we just ignore it here
    if (characterInSilkscreenRegion.width <= 0 || characterInSilkscreenRegion.height <= 0) continue;

    // Setup output file paths
    const characterPngPath = new URL(`png/char_${silkscreenData.char_id}.png`, import.meta.url);
    const characterRawPath = new URL(`raw/char_${silkscreenData.char_id}.bin`, import.meta.url);
    const characterMetadataPath = new URL(`metadata/char_${silkscreenData.char_id}.json`, import.meta.url);

    // Prep the character image
    const characterMessySharp = silkscreenImage.clone().extract(characterInSilkscreenRegion);
    const characterMessySharpMetadata = await characterMessySharp.metadata();
    const characterMessyRawBuffer = await characterMessySharp.raw().toBuffer();
    const characterMessyImage: Image = {
        pixels: characterMessyRawBuffer,
        width: characterInSilkscreenRegion.width,
        height: characterInSilkscreenRegion.height,
        channels: characterMessySharpMetadata.channels!,
        format: imageTypeFromChannelsForPng(characterMessySharpMetadata.channels!),
    };

    // Clean the character image by converting it to masking it with the alpha image and then add the alpha back
    const { droppedChannelImage, modifiedSourceImage } = dropChannel(characterMessyImage, 4, ImageType.RGB);
    const characterMasked = maskImage(modifiedSourceImage, droppedChannelImage, 100, true);
    const characterAlphaGrayscaled = grayscaleImage(characterMasked);
    const characterAlphaThresholded = thresholdImage(characterAlphaGrayscaled, 1);
    const characterClean = addChannel(characterMasked, characterAlphaThresholded, ImageType.RGB_A);

    // Save data to files
    const char = sharp(characterClean.pixels, { raw: characterClean as sharp.CreateRaw });
    const charMetadata = await char.png().toFile(path.fileURLToPath(characterPngPath));
    fs.writeFileSync(path.fileURLToPath(characterRawPath), await char.raw().toBuffer());
    fs.writeFileSync(path.fileURLToPath(characterMetadataPath), JSON.stringify(charMetadata));
}