Skip to content

Commit

Permalink
Fix loading fonts in web-worker (#1322)
Browse files Browse the repository at this point in the history
* Fix loading fonts in web-worker

* add docs for workers
  • Loading branch information
hipstersmoothie authored Sep 2, 2024
1 parent e1010dd commit 6047422
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 32 deletions.
57 changes: 57 additions & 0 deletions packages/docs/src/content/docs/guides/browser.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,60 @@ function handleFile(e: React.ChangeEvent<HTMLInputElement>) {

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);
});
```
6 changes: 3 additions & 3 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
Expand All @@ -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
Expand All @@ -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;
}
1 change: 1 addition & 0 deletions plugins/plugin-print/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
87 changes: 76 additions & 11 deletions plugins/plugin-print/src/load-bitmap-font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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<string, any>;
pages: string[];
} {
}

function parseFont(file: string, data: Buffer | string): LoadedFont {
if (isBinary(data)) {
if (typeof data === "string") {
data = Buffer.from(data, "binary");
Expand All @@ -52,13 +59,68 @@ function parseFont(
return parseASCII(data);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function parseNumbersInObject<T extends Record<string, any>>(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<LoadedFont> {
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<string, any>, 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();

Expand All @@ -69,11 +131,9 @@ async function loadBitmapFontData(bufferOrUrl: string | Buffer) {
}

type RawFont = Awaited<ReturnType<typeof loadBitmapFontData>>;
export type ResolveBmFont = Omit<BmFont, "pages"> & Pick<RawFont, "pages">;

export async function loadBitmapFont(
bufferOrUrl: string | Buffer,
): Promise<Omit<BmFont, "pages"> & Pick<RawFont, "pages">> {
const font = await loadBitmapFontData(bufferOrUrl);
export async function processBitmapFont(file: string, font: LoadedFont) {
const chars: Record<string, BmCharacter> = {};
const kernings: Record<string, BmKerning> = {};

Expand All @@ -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))
)
),
};
}
25 changes: 8 additions & 17 deletions plugins/plugin-print/src/load-font.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,22 +22,14 @@ 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)) {
fileOrBuffer = await fs.readFile(file);
}
}

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);
}
11 changes: 10 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6047422

Please sign in to comment.