UNPKG

libpag

Version:
167 lines (147 loc) 5.89 kB
import { measureText } from '../utils/measure-text'; import { defaultFontNames, getFontFamilies } from '../utils/font-family'; import { getCanvas2D } from '../utils/canvas'; import type { Rect } from '../types'; export class ScalerContext { public static canvas: HTMLCanvasElement | OffscreenCanvas; public static context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; public static setCanvas(canvas: HTMLCanvasElement | OffscreenCanvas) { ScalerContext.canvas = canvas; } public static setContext(context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) { ScalerContext.context = context; } public static isUnicodePropertyEscapeSupported(): boolean { try { // eslint-disable-next-line prefer-regex-literals const regex = new RegExp("\\p{L}", "u"); return true; } catch (e) { return false; } } public static isEmoji(text: string): boolean { let emojiRegExp: RegExp; if (this.isUnicodePropertyEscapeSupported()) { emojiRegExp = /\p{Extended_Pictographic}|[#*0-9]\uFE0F?\u20E3|[\uD83C\uDDE6-\uD83C\uDDFF]/u; } else { emojiRegExp = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/; } return emojiRegExp.test(text); } private readonly fontName: string; private readonly fontStyle: string; private readonly size: number; private readonly fauxBold: boolean; private readonly fauxItalic: boolean; private fontBoundingBoxMap: { key: string; value: Rect }[] = []; public constructor(fontName: string, fontStyle: string, size: number, fauxBold = false, fauxItalic = false) { this.fontName = fontName; this.fontStyle = fontStyle; this.size = size; this.fauxBold = fauxBold; this.fauxItalic = fauxItalic; this.loadCanvas(); } public fontString() { const attributes = []; // css font-style if (this.fauxItalic) { attributes.push('italic'); } // css font-weight if (this.fauxBold) { attributes.push('bold'); } // css font-size attributes.push(`${this.size}px`); // css font-family const fallbackFontNames = defaultFontNames.concat(); fallbackFontNames.unshift(...getFontFamilies(this.fontName, this.fontStyle)); attributes.push(`${fallbackFontNames.join(',')}`); return attributes.join(' '); } public getTextAdvance(text: string) { const { context } = ScalerContext; context.font = this.fontString(); return context.measureText(text).width; } public getTextBounds(text: string) { const { context } = ScalerContext; context.font = this.fontString(); const metrics = this.measureText(context, text); const bounds: Rect = { left: Math.floor(-metrics.actualBoundingBoxLeft), top: Math.floor(-metrics.actualBoundingBoxAscent), right: Math.ceil(metrics.actualBoundingBoxRight), bottom: Math.ceil(metrics.actualBoundingBoxDescent), }; return bounds; } public generateFontMetrics() { const { context } = ScalerContext; context.font = this.fontString(); const metrics = this.measureText(context, '中'); const capHeight = metrics.actualBoundingBoxAscent; const xMetrics = this.measureText(context, 'x'); const xHeight = xMetrics.actualBoundingBoxAscent; return { ascent: -metrics.fontBoundingBoxAscent, descent: metrics.fontBoundingBoxDescent, xHeight, capHeight, }; } public generateImage(text: string, bounds: Rect) { const canvas = getCanvas2D(bounds.right - bounds.left, bounds.bottom - bounds.top); const context = canvas.getContext('2d') as CanvasRenderingContext2D; context.font = this.fontString(); context.fillText(text, -bounds.left, -bounds.top); return canvas; } protected loadCanvas() { if (!ScalerContext.canvas) { ScalerContext.setCanvas(getCanvas2D(10, 10)); // https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently ScalerContext.setContext( (ScalerContext.canvas as HTMLCanvasElement | OffscreenCanvas).getContext('2d', { willReadFrequently: true }) as | CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, ); } } private measureText(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, text: string): TextMetrics { const metrics = ctx.measureText(text); if (metrics?.actualBoundingBoxAscent) return metrics; ctx.canvas.width = this.size * 1.5; ctx.canvas.height = this.size * 1.5; const pos = [0, this.size]; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.font = this.fontString(); ctx.fillText(text, pos[0], pos[1]); const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); const { left, top, right, bottom } = measureText(imageData); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); let fontMeasure: Rect; const fontBoundingBox = this.fontBoundingBoxMap.find((item) => item.key === this.fontName); if (fontBoundingBox) { fontMeasure = fontBoundingBox.value; } else { ctx.font = this.fontString(); ctx.fillText('测', pos[0], pos[1]); const fontImageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); fontMeasure = measureText(fontImageData); this.fontBoundingBoxMap.push({ key: this.fontName, value: fontMeasure }); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); } return { actualBoundingBoxAscent: pos[1] - top, actualBoundingBoxRight: right - pos[0], actualBoundingBoxDescent: bottom - pos[1], actualBoundingBoxLeft: pos[0] - left, fontBoundingBoxAscent: fontMeasure.bottom - fontMeasure.top, fontBoundingBoxDescent: 0, width: fontMeasure.right - fontMeasure.left, }; } }