diff --git a/packages/rendering/src/font/font-renderer.ts b/packages/rendering/src/font/font-renderer.ts index bd5d6d775..8a3153388 100644 --- a/packages/rendering/src/font/font-renderer.ts +++ b/packages/rendering/src/font/font-renderer.ts @@ -27,6 +27,10 @@ const GRADIENT_TOP_OVERLAY = "rgb(255 255 255 / 0.85)"; const GRADIENT_BOTTOM_OVERLAY = "rgb(0 0 0 / 0.60)"; type CharacterSizes = Record; +type FontImage = { + canvas: Canvas; + scale: number; +}; interface Sizes { ascii: CharacterSizes; @@ -34,14 +38,10 @@ interface Sizes { } export class FontRenderer { - private images: Map; - private canvases: WeakMap; - private scales: WeakMap; + private images: Map; public constructor(private gradient: boolean) { this.images = new Map(); - this.canvases = new WeakMap(); - this.scales = new WeakMap(); } public async loadImages(fontPath: string) { @@ -59,13 +59,10 @@ export class FontRenderer { ctx.drawImage(image, 0, 0); - this.canvases.set(ctx, canvas); - this.scales.set(ctx, image.width / 256); - - this.images.set( - file.replace("unicode_page_", "").replace(".png", ""), - ctx - ); + this.images.set(file.replace("unicode_page_", "").replace(".png", ""), { + canvas, + scale: image.width / 256, + }); } } @@ -198,21 +195,6 @@ export class FontRenderer { this.images.get(`${unicode[0]}${unicode[1]}`); } - private getTextureScale(image: CanvasRenderingContext2D) { - return this.scales.get(image) ?? image.canvas.width / 256; - } - - private getImageData( - image: CanvasRenderingContext2D, - x: number, - y: number, - width: number, - height: number - ) { - const ctx = this.canvases.get(image)?.getContext("2d") ?? image; - return ctx.getImageData(x, y, width, height); - } - private getCharacterIndexLocation(unicode: string, isAscii: boolean) { if (isAscii) { const y = positions.findIndex((row) => row.includes(unicode)); @@ -239,19 +221,26 @@ export class FontRenderer { const { x, y } = this.getCharacterIndexLocation(unicode, isAscii); - const scale = this.getTextureScale(image); + const scale = image.scale; const characterSize = sizes[isAscii ? "ascii" : "unicode"][unicode.toUpperCase()]; - const startOffset = characterSize?.start ?? 0; - const width = characterSize?.width ?? 0; + if (!characterSize?.width) return null; + + const startOffset = characterSize.start ?? 0; + const width = characterSize.width; + const scaledWidth = width * scale; + const height = 16 * scale; + + if (!Number.isFinite(scaledWidth) || scaledWidth <= 0) return null; + if (!Number.isFinite(height) || height <= 0) return null; return { x: (startOffset + x * 16) * scale, y: y * 16 * scale, - width: width * scale, - height: 16 * scale, + width: scaledWidth, + height, scale, isAscii, image, @@ -327,7 +316,9 @@ export class FontRenderer { size, } = metadata; - const imageData = this.getImageData(image, charX, charY, width, height); + const imageData = image.canvas + .getContext("2d") + .getImageData(charX, charY, width, height); ctx.filter = this.gradient ? "brightness(15%)" : "brightness(25%)"; diff --git a/packages/rendering/tests/font-renderer.spec.ts b/packages/rendering/tests/font-renderer.spec.ts new file mode 100644 index 000000000..9e13fb7fe --- /dev/null +++ b/packages/rendering/tests/font-renderer.spec.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import { FontRenderer } from "../src/font/font-renderer.js"; +import { createCanvas } from "../src/canvas.js"; +import { expect, it, suite } from "vitest"; + +suite("FontRenderer", () => { + it("ignores unsupported glyphs with a loaded unicode page", () => { + const page = createCanvas(256, 256); + const target = createCanvas(16, 16).getContext("2d"); + + const renderer = new FontRenderer(false); + renderer["images"] = new Map([["1f", { canvas: page, scale: 1 }]]); + + expect(() => renderer.fillText(target, renderer.lex("🌙"), 0, 0)).not.toThrow(); + }); + + it("ignores zero-width glyph metadata", () => { + const page = createCanvas(256, 256); + const target = createCanvas(16, 16).getContext("2d"); + + const renderer = new FontRenderer(false); + renderer["images"] = new Map([["12", { canvas: page, scale: 1 }]]); + + expect(() => renderer.fillText(target, renderer.lex("\u1249"), 0, 0)).not.toThrow(); + }); + + it("ignores glyphs with zero scaled dimensions", () => { + const page = createCanvas(256, 256); + const target = createCanvas(16, 16).getContext("2d"); + + const renderer = new FontRenderer(false); + renderer["images"] = new Map([["ascii", { canvas: page, scale: 0 }]]); + + expect(() => renderer.fillText(target, renderer.lex("A"), 0, 0)).not.toThrow(); + }); +}); diff --git a/packages/rendering/vitest.config.ts b/packages/rendering/vitest.config.ts index b0cea40f6..073ab261c 100644 --- a/packages/rendering/vitest.config.ts +++ b/packages/rendering/vitest.config.ts @@ -7,5 +7,17 @@ */ import { config } from "../../vitest.shared.js"; +import { mergeConfig } from "vitest/config"; +import { resolve } from "node:path"; -export default await config("./.swcrc"); +export default mergeConfig(await config("./.swcrc"), { + resolve: { + alias: { + "#colors": resolve(import.meta.dirname, "src/colors/index.ts"), + "#font": resolve(import.meta.dirname, "src/font/index.ts"), + "#hooks": resolve(import.meta.dirname, "src/hooks/index.ts"), + "#intrinsics": resolve(import.meta.dirname, "src/intrinsics/index.ts"), + "#jsx": resolve(import.meta.dirname, "src/jsx/index.ts"), + }, + }, +});