From 60474228dcb991a9771b0a344101ca3212070faa Mon Sep 17 00:00:00 2001 From: Andrew Lisowski Date: Mon, 2 Sep 2024 15:28:21 -0700 Subject: [PATCH] Fix loading fonts in web-worker (#1322) * Fix loading fonts in web-worker * add docs for workers --- .../docs/src/content/docs/guides/browser.mdx | 57 ++++++++++++ packages/types/src/index.ts | 6 +- plugins/plugin-print/package.json | 1 + plugins/plugin-print/src/load-bitmap-font.ts | 87 ++++++++++++++++--- plugins/plugin-print/src/load-font.ts | 25 ++---- pnpm-lock.yaml | 11 ++- 6 files changed, 155 insertions(+), 32 deletions(-) diff --git a/packages/docs/src/content/docs/guides/browser.mdx b/packages/docs/src/content/docs/guides/browser.mdx index 55c72ec7..140c9a3d 100644 --- a/packages/docs/src/content/docs/guides/browser.mdx +++ b/packages/docs/src/content/docs/guides/browser.mdx @@ -63,3 +63,60 @@ function handleFile(e: React.ChangeEvent) { input.addEventListener("change", handleFile); ``` + +## Using Fonts + +Jimp supports loading fonts from a URL or a file path. +You must host the fonts and will not be able to use the ones included in the node version of Jimp. + +> PRs welcome! + +## Web Workers + +Jimp can be slow and you don't want that running on the main thread. +Workers can make this experience a lot better. + +First define a worker. +This is where you should import jimp and do your image transformations. + +```ts +import { Jimp, loadFont } from "jimp"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const ctx: Worker = self as any; + +ctx.addEventListener("message", async (e) => { + // Initialize Jimp + const image = await Jimp.fromBuffer(e.data.image); + const options = e.data.options; + + // Manipulate the image + if (options.blur) { + image.blur(options.blur); + } + + // Return the result + ctx.postMessage({ base64: await image.getBase64("image/png") }); +}); +``` + +Then you can use the worker. + +```ts +const fileData: ArrayBuffer = new ArrayBuffer(); // Your image data +const worker = new Worker(new URL("./jimp.worker.ts", import.meta.url), { + type: "module", +}); + +worker.postMessage({ + image: fileData, + options: { + blur: 8 + }, +}); + +worker.addEventListener("message", (e) => { + setOutput(e.data.base64); + setIsLoading(false); +}); +``` \ No newline at end of file diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3fc4fadc..f716262f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -38,7 +38,7 @@ export interface RGBAColor { export const JimpClassSchema = z.object({ bitmap: z.object({ - data: z.instanceof(Buffer), + data: z.union([z.instanceof(Buffer), z.instanceof(Uint8Array)]), width: z.number(), height: z.number(), }), @@ -60,7 +60,7 @@ export interface JimpClass { w: number, h: number, // eslint-disable-next-line @typescript-eslint/no-explicit-any - cb: (x: number, y: number, idx: number) => any, + cb: (x: number, y: number, idx: number) => any ): JimpClass; scan( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -69,6 +69,6 @@ export interface JimpClass { w?: number, h?: number, // eslint-disable-next-line @typescript-eslint/no-explicit-any - f?: (x: number, y: number, idx: number) => any, + f?: (x: number, y: number, idx: number) => any ): JimpClass; } diff --git a/plugins/plugin-print/package.json b/plugins/plugin-print/package.json index 788798f6..06554924 100644 --- a/plugins/plugin-print/package.json +++ b/plugins/plugin-print/package.json @@ -82,6 +82,7 @@ "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", + "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" }, "publishConfig": { diff --git a/plugins/plugin-print/src/load-bitmap-font.ts b/plugins/plugin-print/src/load-bitmap-font.ts index c7bdf2e3..35950c12 100644 --- a/plugins/plugin-print/src/load-bitmap-font.ts +++ b/plugins/plugin-print/src/load-bitmap-font.ts @@ -2,7 +2,15 @@ import parseASCII from "parse-bmfont-ascii"; import parseXML from "parse-bmfont-xml"; import readBinary from "parse-bmfont-binary"; import { BmCharacter, BmKerning, BmFont, BmCommonProps } from "./types.js"; +import png from "@jimp/js-png"; +import { createJimp } from "@jimp/core"; +import path from "path"; +import { convertXML } from "simple-xml-to-json"; +export const isWebWorker = + typeof self !== "undefined" && self.document === undefined; + +const CharacterJimp = createJimp({ formats: [png] }); const HEADER = Buffer.from([66, 77, 70, 3]); function isBinary(buf: Buffer | string) { @@ -20,17 +28,16 @@ function isBinary(buf: Buffer | string) { ); } -function parseFont( - file: string, - data: Buffer | string, -): { +export interface LoadedFont { chars: BmCharacter[]; kernings: BmKerning[]; common: BmCommonProps; // eslint-disable-next-line @typescript-eslint/no-explicit-any info: Record; pages: string[]; -} { +} + +function parseFont(file: string, data: Buffer | string): LoadedFont { if (isBinary(data)) { if (typeof data === "string") { data = Buffer.from(data, "binary"); @@ -52,13 +59,68 @@ function parseFont( return parseASCII(data); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function parseNumbersInObject>(obj: T) { + for (const key in obj) { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (obj as any)[key] = parseInt(obj[key], 10); + } catch { + // do nothing + } + + if (typeof obj[key] === "object") { + parseNumbersInObject(obj[key]); + } + } + + return obj; +} + /** * * @param bufferOrUrl A URL to a file or a buffer * @returns */ -async function loadBitmapFontData(bufferOrUrl: string | Buffer) { - if (typeof bufferOrUrl === "string") { +export async function loadBitmapFontData( + bufferOrUrl: string | Buffer +): Promise { + if (isWebWorker && typeof bufferOrUrl === "string") { + const res = await fetch(bufferOrUrl); + const text = await res.text(); + const json = convertXML(text); + + const font = json.font.children.reduce( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (acc: Record, i: any) => ({ ...acc, ...i }), + {} + ); + const pages: LoadedFont["pages"] = []; + const chars: LoadedFont["chars"] = []; + const kernings: LoadedFont["kernings"] = []; + + for (let i = 0; i < font.pages.children.length; i++) { + const p = font.pages.children[i].page; + const id = parseInt(p.id, 10); + pages[id] = parseNumbersInObject(p.file); + } + + for (let i = 0; i < font.chars.children.length; i++) { + chars.push(parseNumbersInObject(font.chars.children[i].char)); + } + + for (let i = 0; i < font.kernings.children.length; i++) { + kernings.push(parseNumbersInObject(font.kernings.children[i].kerning)); + } + + return { + info: font.info, + common: font.common, + pages, + chars, + kernings, + } satisfies LoadedFont; + } else if (typeof bufferOrUrl === "string") { const res = await fetch(bufferOrUrl); const text = await res.text(); @@ -69,11 +131,9 @@ async function loadBitmapFontData(bufferOrUrl: string | Buffer) { } type RawFont = Awaited>; +export type ResolveBmFont = Omit & Pick; -export async function loadBitmapFont( - bufferOrUrl: string | Buffer, -): Promise & Pick> { - const font = await loadBitmapFontData(bufferOrUrl); +export async function processBitmapFont(file: string, font: LoadedFont) { const chars: Record = {}; const kernings: Record = {}; @@ -94,5 +154,10 @@ export async function loadBitmapFont( ...font, chars, kernings, + pages: await Promise.all( + font.pages.map(async (page) => + CharacterJimp.read(path.join(path.dirname(file), page)) + ) + ), }; } diff --git a/plugins/plugin-print/src/load-font.ts b/plugins/plugin-print/src/load-font.ts index 40ace14e..fbd46edd 100644 --- a/plugins/plugin-print/src/load-font.ts +++ b/plugins/plugin-print/src/load-font.ts @@ -1,9 +1,8 @@ -import { loadBitmapFont } from "./load-bitmap-font.js"; -import { createJimp } from "@jimp/core"; -import png from "@jimp/js-png"; -import path from "path"; - -const CharacterJimp = createJimp({ formats: [png] }); +import { + isWebWorker, + loadBitmapFontData, + processBitmapFont, +} from "./load-bitmap-font.js"; /** * Loads a Bitmap Font from a file. @@ -23,7 +22,7 @@ const CharacterJimp = createJimp({ formats: [png] }); export async function loadFont(file: string) { let fileOrBuffer: string | Buffer = file; - if (typeof window === "undefined") { + if (typeof window === "undefined" && !isWebWorker) { const { existsSync, promises: fs } = await import("fs"); if (existsSync(file)) { @@ -31,14 +30,6 @@ export async function loadFont(file: string) { } } - const font = await loadBitmapFont(fileOrBuffer); - - return { - ...font, - pages: await Promise.all( - font.pages.map(async (page) => - CharacterJimp.read(path.join(path.dirname(file), page)), - ), - ), - }; + const data = await loadBitmapFontData(fileOrBuffer); + return processBitmapFont(file, data); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 426a77b6..386065c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1392,6 +1392,9 @@ importers: parse-bmfont-xml: specifier: ^1.1.6 version: 1.1.6 + simple-xml-to-json: + specifier: ^1.2.2 + version: 1.2.2 zod: specifier: ^3.23.8 version: 3.23.8 @@ -5842,6 +5845,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-xml-to-json@1.2.2: + resolution: {integrity: sha512-bmJJf5YiYL60eOQk3gaVxbM6vgYuwrFydCEAA2x3jccHUTsAffiPyblS/yQGr8GDUQVxSDm3WwLNL5HmRqDUcg==} + engines: {node: '>=14.20.0'} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -8258,7 +8265,7 @@ snapshots: magic-string: 0.30.11 msw: 2.4.1(typescript@5.5.4) sirv: 2.0.4 - vitest: 2.0.5(@types/node@18.19.48)(@vitest/browser@2.0.5)(terser@5.30.3) + vitest: 2.0.5(@types/node@22.5.2)(@vitest/browser@2.0.5)(terser@5.30.3) ws: 8.18.0 optionalDependencies: playwright: 1.46.1 @@ -12167,6 +12174,8 @@ snapshots: dependencies: is-arrayish: 0.3.2 + simple-xml-to-json@1.2.2: {} + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.25