Spaces:
Running
Running
| import { HorizontalAlign, VerticalAlign } from "@jimp/core"; | |
| import { JimpClass } from "@jimp/types"; | |
| import { methods as blitMethods } from "@jimp/plugin-blit"; | |
| import { z } from "zod"; | |
| import { measureText, measureTextHeight, splitLines } from "./measure-text.js"; | |
| import { BmCharacter, BmFont } from "./types.js"; | |
| export { measureText, measureTextHeight } from "./measure-text.js"; | |
| export * from "./types.js"; | |
| const PrintOptionsSchema = z.object({ | |
| /** the x position to draw the image */ | |
| x: z.number(), | |
| /** the y position to draw the image */ | |
| y: z.number(), | |
| /** the text to print */ | |
| text: z.union([ | |
| z.union([z.string(), z.number()]), | |
| z.object({ | |
| text: z.union([z.string(), z.number()]), | |
| alignmentX: z.nativeEnum(HorizontalAlign).optional(), | |
| alignmentY: z.nativeEnum(VerticalAlign).optional(), | |
| }), | |
| ]), | |
| /** the boundary width to draw in */ | |
| maxWidth: z.number().optional(), | |
| /** the boundary height to draw in */ | |
| maxHeight: z.number().optional(), | |
| /** a callback for when complete that ahs the end co-ordinates of the text */ | |
| cb: z | |
| .function(z.tuple([z.object({ x: z.number(), y: z.number() })])) | |
| .optional(), | |
| }); | |
| export type PrintOptions = z.infer<typeof PrintOptionsSchema>; | |
| function xOffsetBasedOnAlignment<I extends JimpClass>( | |
| font: BmFont<I>, | |
| line: string, | |
| maxWidth: number, | |
| alignment: HorizontalAlign | |
| ) { | |
| if (alignment === HorizontalAlign.LEFT) { | |
| return 0; | |
| } | |
| if (alignment === HorizontalAlign.CENTER) { | |
| return (maxWidth - measureText(font, line)) / 2; | |
| } | |
| return maxWidth - measureText(font, line); | |
| } | |
| function drawCharacter<I extends JimpClass>( | |
| image: I, | |
| font: BmFont<I>, | |
| x: number, | |
| y: number, | |
| char: BmCharacter | |
| ) { | |
| if (char.width > 0 && char.height > 0) { | |
| const characterPage = font.pages[char.page]; | |
| if (characterPage) { | |
| image = blitMethods.blit(image, { | |
| src: characterPage, | |
| x: x + char.xoffset, | |
| y: y + char.yoffset, | |
| srcX: char.x, | |
| srcY: char.y, | |
| srcW: char.width, | |
| srcH: char.height, | |
| }); | |
| } | |
| } | |
| return image; | |
| } | |
| function printText<I extends JimpClass>( | |
| image: I, | |
| font: BmFont<I>, | |
| x: number, | |
| y: number, | |
| text: string, | |
| defaultCharWidth: number | |
| ) { | |
| for (let i = 0; i < text.length; i++) { | |
| const stringChar = text[i]!; | |
| let char; | |
| if (font.chars[stringChar]) { | |
| char = stringChar; | |
| } else if (/\s/.test(stringChar)) { | |
| char = ""; | |
| } else { | |
| char = "?"; | |
| } | |
| const fontChar = font.chars[char] || { xadvance: undefined }; | |
| const fontKerning = font.kernings[char]; | |
| if (fontChar) { | |
| drawCharacter(image, font, x, y, fontChar as BmCharacter); | |
| } | |
| const nextChar = text[i + 1]; | |
| const kerning = | |
| fontKerning && nextChar && fontKerning[nextChar] | |
| ? fontKerning[nextChar] || 0 | |
| : 0; | |
| x += kerning + (fontChar.xadvance || defaultCharWidth); | |
| } | |
| } | |
| export const methods = { | |
| /** | |
| * Draws a text on a image on a given boundary | |
| * @param font a bitmap font loaded from `Jimp.loadFont` command | |
| * @param x the x position to start drawing the text | |
| * @param y the y position to start drawing the text | |
| * @param text the text to draw (string or object with `text`, `alignmentX`, and/or `alignmentY`) | |
| * @example | |
| * ```ts | |
| * import { Jimp } from "jimp"; | |
| * | |
| * const image = await Jimp.read("test/image.png"); | |
| * const font = await Jimp.loadFont(Jimp.FONT_SANS_32_BLACK); | |
| * | |
| * image.print({ font, x: 10, y: 10, text: "Hello world!" }); | |
| * ``` | |
| */ | |
| print<I extends JimpClass>( | |
| image: I, | |
| { | |
| font, | |
| ...options | |
| }: PrintOptions & { | |
| /** the BMFont instance */ | |
| font: BmFont<I>; | |
| } | |
| ) { | |
| let { | |
| // eslint-disable-next-line prefer-const | |
| x, | |
| y, | |
| text, | |
| // eslint-disable-next-line prefer-const | |
| maxWidth = Infinity, | |
| // eslint-disable-next-line prefer-const | |
| maxHeight = Infinity, | |
| // eslint-disable-next-line prefer-const | |
| cb = () => {}, | |
| } = PrintOptionsSchema.parse(options); | |
| let alignmentX: HorizontalAlign; | |
| let alignmentY: VerticalAlign; | |
| if ( | |
| typeof text === "object" && | |
| text.text !== null && | |
| text.text !== undefined | |
| ) { | |
| alignmentX = text.alignmentX || HorizontalAlign.LEFT; | |
| alignmentY = text.alignmentY || VerticalAlign.TOP; | |
| ({ text } = text); | |
| } else { | |
| alignmentX = HorizontalAlign.LEFT; | |
| alignmentY = VerticalAlign.TOP; | |
| text = text.toString(); | |
| } | |
| if (typeof text === "number") { | |
| text = text.toString(); | |
| } | |
| if (maxHeight !== Infinity && alignmentY === VerticalAlign.BOTTOM) { | |
| y += maxHeight - measureTextHeight(font, text, maxWidth); | |
| } else if (maxHeight !== Infinity && alignmentY === VerticalAlign.MIDDLE) { | |
| y += maxHeight / 2 - measureTextHeight(font, text, maxWidth) / 2; | |
| } | |
| const defaultCharWidth = Object.entries(font.chars).find( | |
| (c) => c[1].xadvance | |
| )?.[1].xadvance; | |
| if (typeof defaultCharWidth !== "number") { | |
| throw new Error("Could not find default character width"); | |
| } | |
| const { lines, longestLine } = splitLines(font, text, maxWidth); | |
| lines.forEach((line) => { | |
| const lineString = line.join(" "); | |
| const alignmentWidth = xOffsetBasedOnAlignment( | |
| font, | |
| lineString, | |
| maxWidth, | |
| alignmentX | |
| ); | |
| printText( | |
| image, | |
| font, | |
| x + alignmentWidth, | |
| y, | |
| lineString, | |
| defaultCharWidth | |
| ); | |
| y += font.common.lineHeight; | |
| }); | |
| cb.bind(image)({ x: x + longestLine, y }); | |
| return image; | |
| }, | |
| }; | |