UNPKG

smoosic

Version:

<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i

357 lines (330 loc) 12 kB
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010. // MIT License import { FontInfo, VexFlow } from '../common/vex'; const VF = VexFlow; /** * @internal */ export interface FontGlyph { xMin: number, xMax: number, yMin: number, yMax: number, ha: number, leftSideBearing: number, advanceWidth: number } /** * @internal */ export interface TextFormatterInfo extends Record<string, unknown> { family: string; resolution?: number; glyphs?: Record<string, FontGlyph>; serifs: boolean; monospaced: boolean; italic: boolean; bold: boolean; maxSizeGlyph?: string; superscriptOffset?: number; subscriptOffset?: number; description: string; } /** * Y information, 0 is baseline, yMin is lowest point. * @internal */ export interface yExtent { yMin: number; yMax: number; height: number; } /** * Text widths are stored in a cache, so we don't have to recompute widths * for the same font + string combination. * * The cache is first keyed by the font information. The key is of the form: * `${family}-${size}-${weight}-${style}` * The second level key is the specific text to be measured. * * The stored value is the measured width in `em` units. * textWidth == textWidthCache[cacheKey][textToMeasure] */ const textWidthCache: Record<string, Record<string, number | undefined> | undefined> = {}; const textHeightCache: Record<string, Record<string, yExtent | undefined> | undefined> = {}; /** * Applications may register additional fonts via `TextFormatter.registerInfo(info)`. * The metrics for those fonts will be made available to the application. */ const registry: Record<string, TextFormatterInfo> = {}; /** * @category SuiRender */ export class TextFormatter { /** * Return all registered font families. */ static getFontFamilies(): TextFormatterInfo[] { const registeredFonts: TextFormatterInfo[] = []; for (const fontFamily in registry) { const formatterInfo = registry[fontFamily]; registeredFonts.push({ ...formatterInfo }); } return registeredFonts; } /** * Call `TextFormatter.registerInfo(info)` to register font information before using this method. * * This method creates a formatter for the font that most closely matches the requested font. * We compare font family, bold, and italic attributes. * This method will return a fallback formatter if there are no matches. */ static create(requestedFont: FontInfo = {}): TextFormatter { if (!requestedFont.family) { requestedFont.family = 'Sans Serif'; } // TODO: One potential (small) optimization is to cache the TextFormatter object // returned for each font info. We would probably want to clear the cache if // the registry is ever updated. const candidates: TextFormatterInfo[] = []; // The incoming font family is a string of comma-separated font family names. // (e.g., `PetalumaScript, Arial, sans-serif`). const requestedFamilies = requestedFont.family.split(/\s*,\s*/); for (const requestedFamily of requestedFamilies) { for (const fontFamily in registry) { // Support cases where the registry contains 'Roboto Slab Medium', // but the requestedFont.family is 'Roboto Slab'. if (fontFamily.startsWith(requestedFamily)) { candidates.push(registry[fontFamily]); } } if (candidates.length > 0) { break; } } let formatter; if (candidates.length === 0) { // No match, so return a fallback text formatter. formatter = new TextFormatter(Object.values(registry)[0]); } else if (candidates.length === 1) { formatter = new TextFormatter(candidates[0]); } else { const bold = VF.Font.isBold(requestedFont.weight); const italic = VF.Font.isItalic(requestedFont.style); const perfectMatch = candidates.find((f) => f.bold === bold && f.italic === italic); if (perfectMatch) { formatter = new TextFormatter(perfectMatch); } else { const partialMatch = candidates.find((f) => f.italic === italic || f.bold === bold); if (partialMatch) { formatter = new TextFormatter(partialMatch); } else { formatter = new TextFormatter(candidates[0]); } } } const fontSize = requestedFont.size; if (typeof fontSize !== 'undefined') { const fontSizeInPt = VF.Font.convertSizeToPointValue(fontSize); formatter.setFontSize(fontSizeInPt); } return formatter; } /** * @param fontFamily used as a key to the font registry. * @returns the same info object that was passed in via `TextFormatter.registerInfo(info)` */ static getInfo(fontFamily: string): TextFormatterInfo | undefined { return registry[fontFamily]; } /** * Apps may register their own fonts and metrics, and those metrics * will be available to the app for formatting. * * Metrics can be generated from a font file using fontgen_text.js in the tools/fonts directory. * @param info * @param overwrite */ static registerInfo(info: TextFormatterInfo, overwrite: boolean = false): void { const fontFamily = info.family; const currFontInfo = registry[fontFamily]; if (currFontInfo === undefined || overwrite) { registry[fontFamily] = info; } } /** Font family. */ protected family: string = ''; /** Specified in `pt` units. */ protected size: number = 14; /** Font metrics are extracted at 1000 upem (units per em). */ protected resolution: number = 1000; /** * For text formatting, we do not require glyph outlines, but instead rely on glyph * bounding box metrics such as: * ``` * { * x_min: 48, * x_max: 235, * y_min: -17, * y_max: 734, * ha: 751, * leftSideBearing: 48, * advanceWidth: 286, * } * ``` */ protected glyphs: Record<string, FontGlyph> = {}; protected description?: string; protected serifs: boolean = false; protected monospaced: boolean = false; protected italic: boolean = false; protected bold: boolean = false; protected superscriptOffset: number = 0; protected subscriptOffset: number = 0; protected maxSizeGlyph: string = '@'; // This is an internal key used to index the `textWidthCache`. protected cacheKey: string = ''; /** * Use `TextFormatter.create(...)` to build an instance from information previously * registered via `TextFormatter.registerInfo(info)`. */ private constructor(formatterInfo: TextFormatterInfo) { this.updateParams(formatterInfo); } get localHeightCache(): Record<string, yExtent | undefined> { if (textHeightCache[this.cacheKey] === undefined) { textHeightCache[this.cacheKey] = {}; } return textHeightCache[this.cacheKey] ?? {}; } updateParams(params: TextFormatterInfo): void { if (params.family) this.family = params.family; if (params.resolution) this.resolution = params.resolution; if (params.glyphs) this.glyphs = params.glyphs; if (params.serifs) this.serifs = params.serifs; if (params.monospaced) this.monospaced = params.monospaced; if (params.italic) this.italic = params.italic; if (params.bold) this.bold = params.bold; if (params.maxSizeGlyph) this.maxSizeGlyph = params.maxSizeGlyph; if (params.superscriptOffset) this.superscriptOffset = params.superscriptOffset; if (params.subscriptOffset) this.subscriptOffset = params.subscriptOffset; this.updateCacheKey(); } /** Create a hash with the current font data, so we can cache computed widths. */ updateCacheKey(): void { const family = this.family.replace(/\s+/g, '_'); const size = this.size; const weight = this.bold ? VF.FontWeight.BOLD : VF.FontWeight.NORMAL; const style = this.italic ? VF.FontStyle.ITALIC : VF.FontStyle.NORMAL; // Use the same key format as SVGContext. this.cacheKey = `${family}%${size}%${weight}%${style}`; } /** * The glyphs table is indexed by the character (e.g., 'C', '@'). * See: robotoslab_glyphs.ts & petalumascript_glyphs.ts. */ getGlyphMetrics(character: string): FontGlyph { if (this.glyphs[character]) { return this.glyphs[character]; } else { return this.glyphs[this.maxSizeGlyph]; } } get maxHeight(): number { const metrics = this.getGlyphMetrics(this.maxSizeGlyph); return (metrics.ha / this.resolution) * this.fontSizeInPixels; } /** * Retrieve the character's advanceWidth as a fraction of an `em` unit. * For the space character ' ' as defined in the: * petalumascript_glyphs.ts: 250 advanceWidth in the 1000 unitsPerEm font returns 0.25. * robotoslab_glyphs.ts: 509 advanceWidth in the 2048 unitsPerEm font returns 0.2485. */ getWidthForCharacterInEm(c: string): number { const metrics = this.getGlyphMetrics(c); if (!metrics) { // An arbitrary number, close to the `em` width of the '#' and '5' characters in PetalumaScript. return 0.65; } else { const advanceWidth = metrics.advanceWidth ?? 0; return advanceWidth / this.resolution; } } /** * Retrieve the character's y bounds (ymin, ymax) and height. */ getYForCharacterInPx(c: string): yExtent { const metrics = this.getGlyphMetrics(c); const rv = { yMin: 0, yMax: this.maxHeight, height: this.maxHeight }; if (!metrics) { return rv; } else { if (typeof metrics.yMin === 'number') { rv.yMin = (metrics.yMin / this.resolution) * this.fontSizeInPixels; } if (typeof metrics.yMax === 'number') { rv.yMax = (metrics.yMax / this.resolution) * this.fontSizeInPixels; } rv.height = rv.yMax - rv.yMin; return rv; } } getYForStringInPx(str: string): yExtent { const entry = this.localHeightCache; const extent = { yMin: 0, yMax: this.maxHeight, height: this.maxHeight }; const cache = entry[str]; if (cache !== undefined) { return cache; } for (let i = 0; i < str.length; ++i) { const curY = this.getYForCharacterInPx(str[i]); extent.yMin = Math.min(extent.yMin, curY.yMin); extent.yMax = Math.max(extent.yMax, curY.yMax); extent.height = extent.yMax - extent.yMin; } entry[str] = extent; return extent; } /** * Retrieve the total width of `text` in `em` units. */ getWidthForTextInEm(text: string): number { const key = this.cacheKey; // Get the cache for this specific font family, size, weight, style combination. // The cache contains previously computed widths for different `text` strings. let cachedWidths = textWidthCache[key]; if (cachedWidths === undefined) { cachedWidths = {}; textWidthCache[key] = cachedWidths; } let width = cachedWidths[text]; if (width === undefined) { width = 0; for (let i = 0; i < text.length; ++i) { width += this.getWidthForCharacterInEm(text[i]); } cachedWidths[text] = width; } return width; } /** The width of the text (in `em`) is scaled by the font size (in `px`). */ getWidthForTextInPx(text: string): number { return this.getWidthForTextInEm(text) * this.fontSizeInPixels; } /** * @param size in pt. */ setFontSize(size: number): this { this.size = size; // The width cache key depends on the current font size. this.updateCacheKey(); return this; } /** `this.size` is specified in points. Convert to pixels. */ get fontSizeInPixels(): number { return this.size * VF.Font.scaleToPxFrom.pt; } getResolution(): number { return this.resolution; } }