{"version":3,"file":"TextMetrics.mjs","sources":["../src/TextMetrics.ts"],"sourcesContent":["import { settings } from '@pixi/core';\n\nimport type { ICanvas, ICanvasRenderingContext2D, ICanvasRenderingContext2DSettings } from '@pixi/core';\nimport type { TextStyle, TextStyleWhiteSpace } from './TextStyle';\n\n// The type for Intl.Segmenter is only available since TypeScript 4.7.2, so let's make a polyfill for it.\ninterface ISegmentData\n{\n    segment: string;\n}\ninterface ISegments\n{\n    [Symbol.iterator](): IterableIterator<ISegmentData>;\n}\ninterface ISegmenter\n{\n    segment(input: string): ISegments;\n}\ninterface IIntl\n{\n    Segmenter?: {\n        prototype: ISegmenter;\n        new(): ISegmenter;\n    };\n}\n\n/**\n * Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}.\n * @typedef {object} FontMetrics\n * @property {number} ascent - The ascent distance\n * @property {number} descent - The descent distance\n * @property {number} fontSize - Font size from ascent to descent\n * @memberof PIXI.TextMetrics\n * @private\n */\n\n/**\n * A number, or a string containing a number.\n * @memberof PIXI\n * @typedef {object} IFontMetrics\n * @property {number} ascent - Font ascent\n * @property {number} descent - Font descent\n * @property {number} fontSize - Font size\n */\ninterface IFontMetrics\n{\n    ascent: number;\n    descent: number;\n    fontSize: number;\n}\n\ntype CharacterWidthCache = Record<string, number>;\n\n// Default settings used for all getContext calls\nconst contextSettings: ICanvasRenderingContext2DSettings = {\n    // TextMetrics requires getImageData readback for measuring fonts.\n    willReadFrequently: true,\n};\n\n/**\n * The TextMetrics object represents the measurement of a block of text with a specified style.\n * @example\n * import { TextMetrics, TextStyle } from 'pixi.js';\n *\n * const style = new TextStyle({\n *     fontFamily: 'Arial',\n *     fontSize: 24,\n *     fill: 0xff1010,\n *     align: 'center',\n * });\n * const textMetrics = TextMetrics.measureText('Your text', style);\n * @memberof PIXI\n */\nexport class TextMetrics\n{\n    /** The text that was measured. */\n    public text: string;\n\n    /** The style that was measured. */\n    public style: TextStyle;\n\n    /** The measured width of the text. */\n    public width: number;\n\n    /** The measured height of the text. */\n    public height: number;\n\n    /** An array of lines of the text broken by new lines and wrapping is specified in style. */\n    public lines: string[];\n\n    /** An array of the line widths for each line matched to `lines`. */\n    public lineWidths: number[];\n\n    /** The measured line height for this style. */\n    public lineHeight: number;\n\n    /** The maximum line width for all measured lines. */\n    public maxLineWidth: number;\n\n    /** The font properties object from TextMetrics.measureFont. */\n    public fontProperties: IFontMetrics;\n\n    /**\n     * String used for calculate font metrics.\n     * These characters are all tall to help calculate the height required for text.\n     */\n    public static METRICS_STRING = '|ÉqÅ';\n\n    /** Baseline symbol for calculate font metrics. */\n    public static BASELINE_SYMBOL = 'M';\n\n    /** Baseline multiplier for calculate font metrics. */\n    public static BASELINE_MULTIPLIER = 1.4;\n\n    /** Height multiplier for setting height of canvas to calculate font metrics. */\n    public static HEIGHT_MULTIPLIER = 2.0;\n\n    /**\n     * A Unicode \"character\", or \"grapheme cluster\", can be composed of multiple Unicode code points,\n     * such as letters with diacritical marks (e.g. `'\\u0065\\u0301'`, letter e with acute)\n     * or emojis with modifiers (e.g. `'\\uD83E\\uDDD1\\u200D\\uD83D\\uDCBB'`, technologist).\n     * The new `Intl.Segmenter` API in ES2022 can split the string into grapheme clusters correctly. If it is not available,\n     * PixiJS will fallback to use the iterator of String, which can only spilt the string into code points.\n     * If you want to get full functionality in environments that don't support `Intl.Segmenter` (such as Firefox),\n     * you can use other libraries such as [grapheme-splitter]{@link https://www.npmjs.com/package/grapheme-splitter}\n     * or [graphemer]{@link https://www.npmjs.com/package/graphemer} to create a polyfill. Since these libraries can be\n     * relatively large in size to handle various Unicode grapheme clusters properly, PixiJS won't use them directly.\n     */\n    public static graphemeSegmenter: (s: string) => string[] = (() =>\n    {\n        if (typeof (Intl as IIntl)?.Segmenter === 'function')\n        {\n            const segmenter = new (Intl as IIntl).Segmenter();\n\n            return (s: string) => [...segmenter.segment(s)].map((x) => x.segment);\n        }\n\n        return (s: string) => [...s];\n    })();\n\n    public static _experimentalLetterSpacingSupported?: boolean;\n\n    /**\n     * Checking that we can use modern canvas 2D API.\n     *\n     * Note: This is an unstable API, Chrome < 94 use `textLetterSpacing`, later versions use `letterSpacing`.\n     * @see PIXI.TextMetrics.experimentalLetterSpacing\n     * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/letterSpacing\n     * @see https://developer.chrome.com/origintrials/#/view_trial/3585991203293757441\n     */\n    public static get experimentalLetterSpacingSupported(): boolean\n    {\n        let result = TextMetrics._experimentalLetterSpacingSupported;\n\n        if (result !== undefined)\n        {\n            const proto = settings.ADAPTER.getCanvasRenderingContext2D().prototype;\n\n            result\n                = TextMetrics._experimentalLetterSpacingSupported\n                = 'letterSpacing' in proto || 'textLetterSpacing' in proto;\n        }\n\n        return result;\n    }\n\n    /**\n     * New rendering behavior for letter-spacing which uses Chrome's new native API. This will\n     * lead to more accurate letter-spacing results because it does not try to manually draw\n     * each character. However, this Chrome API is experimental and may not serve all cases yet.\n     * @see PIXI.TextMetrics.experimentalLetterSpacingSupported\n     */\n    public static experimentalLetterSpacing = false;\n\n    /** Cache of {@see PIXI.TextMetrics.FontMetrics} objects. */\n    private static _fonts: Record<string, IFontMetrics> = {};\n\n    /** Cache of new line chars. */\n    private static _newlines: number[] = [\n        0x000A, // line feed\n        0x000D, // carriage return\n    ];\n\n    /** Cache of breaking spaces. */\n    private static _breakingSpaces: number[] = [\n        0x0009, // character tabulation\n        0x0020, // space\n        0x2000, // en quad\n        0x2001, // em quad\n        0x2002, // en space\n        0x2003, // em space\n        0x2004, // three-per-em space\n        0x2005, // four-per-em space\n        0x2006, // six-per-em space\n        0x2008, // punctuation space\n        0x2009, // thin space\n        0x200A, // hair space\n        0x205F, // medium mathematical space\n        0x3000, // ideographic space\n    ];\n\n    private static __canvas: ICanvas;\n    private static __context: ICanvasRenderingContext2D;\n\n    /**\n     * @param text - the text that was measured\n     * @param style - the style that was measured\n     * @param width - the measured width of the text\n     * @param height - the measured height of the text\n     * @param lines - an array of the lines of text broken by new lines and wrapping if specified in style\n     * @param lineWidths - an array of the line widths for each line matched to `lines`\n     * @param lineHeight - the measured line height for this style\n     * @param maxLineWidth - the maximum line width for all measured lines\n     * @param {PIXI.IFontMetrics} fontProperties - the font properties object from TextMetrics.measureFont\n     */\n    constructor(text: string, style: TextStyle, width: number, height: number, lines: string[], lineWidths: number[],\n        lineHeight: number, maxLineWidth: number, fontProperties: IFontMetrics)\n    {\n        this.text = text;\n        this.style = style;\n        this.width = width;\n        this.height = height;\n        this.lines = lines;\n        this.lineWidths = lineWidths;\n        this.lineHeight = lineHeight;\n        this.maxLineWidth = maxLineWidth;\n        this.fontProperties = fontProperties;\n    }\n\n    /**\n     * Measures the supplied string of text and returns a Rectangle.\n     * @param text - The text to measure.\n     * @param style - The text style to use for measuring\n     * @param wordWrap - Override for if word-wrap should be applied to the text.\n     * @param canvas - optional specification of the canvas to use for measuring.\n     * @returns Measured width and height of the text.\n     */\n    public static measureText(\n        text: string,\n        style: TextStyle,\n        wordWrap?: boolean,\n        canvas: ICanvas = TextMetrics._canvas\n    ): TextMetrics\n    {\n        wordWrap = (wordWrap === undefined || wordWrap === null) ? style.wordWrap : wordWrap;\n        const font = style.toFontString();\n        const fontProperties = TextMetrics.measureFont(font);\n\n        // fallback in case UA disallow canvas data extraction\n        // (toDataURI, getImageData functions)\n        if (fontProperties.fontSize === 0)\n        {\n            fontProperties.fontSize = style.fontSize as number;\n            fontProperties.ascent = style.fontSize as number;\n        }\n\n        const context = canvas.getContext('2d', contextSettings);\n\n        context.font = font;\n\n        const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text;\n        const lines = outputText.split(/(?:\\r\\n|\\r|\\n)/);\n        const lineWidths = new Array<number>(lines.length);\n        let maxLineWidth = 0;\n\n        for (let i = 0; i < lines.length; i++)\n        {\n            const lineWidth = TextMetrics._measureText(lines[i], style.letterSpacing, context);\n\n            lineWidths[i] = lineWidth;\n            maxLineWidth = Math.max(maxLineWidth, lineWidth);\n        }\n        let width = maxLineWidth + style.strokeThickness;\n\n        if (style.dropShadow)\n        {\n            width += style.dropShadowDistance;\n        }\n\n        const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness;\n        let height\n            = Math.max(lineHeight, fontProperties.fontSize + (style.strokeThickness * 2)) + style.leading\n            + ((lines.length - 1) * (lineHeight + style.leading));\n\n        if (style.dropShadow)\n        {\n            height += style.dropShadowDistance;\n        }\n\n        return new TextMetrics(\n            text,\n            style,\n            width,\n            height,\n            lines,\n            lineWidths,\n            lineHeight + style.leading,\n            maxLineWidth,\n            fontProperties\n        );\n    }\n\n    private static _measureText(\n        text: string,\n        letterSpacing: number,\n        context: ICanvasRenderingContext2D\n    )\n    {\n        let useExperimentalLetterSpacing = false;\n\n        if (TextMetrics.experimentalLetterSpacingSupported)\n        {\n            if (TextMetrics.experimentalLetterSpacing)\n            {\n                context.letterSpacing = `${letterSpacing}px`;\n                context.textLetterSpacing = `${letterSpacing}px`;\n                useExperimentalLetterSpacing = true;\n            }\n            else\n            {\n                context.letterSpacing = '0px';\n                context.textLetterSpacing = '0px';\n            }\n        }\n\n        let width = context.measureText(text).width;\n\n        if (width > 0)\n        {\n            if (useExperimentalLetterSpacing)\n            {\n                width -= letterSpacing;\n            }\n            else\n            {\n                width += (TextMetrics.graphemeSegmenter(text).length - 1) * letterSpacing;\n            }\n        }\n\n        return width;\n    }\n\n    /**\n     * Applies newlines to a string to have it optimally fit into the horizontal\n     * bounds set by the Text object's wordWrapWidth property.\n     * @param text - String to apply word wrapping to\n     * @param style - the style to use when wrapping\n     * @param canvas - optional specification of the canvas to use for measuring.\n     * @returns New string with new lines applied where required\n     */\n    private static wordWrap(\n        text: string,\n        style: TextStyle,\n        canvas: ICanvas = TextMetrics._canvas\n    ): string\n    {\n        const context = canvas.getContext('2d', contextSettings);\n\n        let width = 0;\n        let line = '';\n        let lines = '';\n\n        const cache: CharacterWidthCache = Object.create(null);\n        const { letterSpacing, whiteSpace } = style;\n\n        // How to handle whitespaces\n        const collapseSpaces = TextMetrics.collapseSpaces(whiteSpace);\n        const collapseNewlines = TextMetrics.collapseNewlines(whiteSpace);\n\n        // whether or not spaces may be added to the beginning of lines\n        let canPrependSpaces = !collapseSpaces;\n\n        // There is letterSpacing after every char except the last one\n        // t_h_i_s_' '_i_s_' '_a_n_' '_e_x_a_m_p_l_e_' '_!\n        // so for convenience the above needs to be compared to width + 1 extra letterSpace\n        // t_h_i_s_' '_i_s_' '_a_n_' '_e_x_a_m_p_l_e_' '_!_\n        // ________________________________________________\n        // And then the final space is simply no appended to each line\n        const wordWrapWidth = style.wordWrapWidth + letterSpacing;\n\n        // break text into words, spaces and newline chars\n        const tokens = TextMetrics.tokenize(text);\n\n        for (let i = 0; i < tokens.length; i++)\n        {\n            // get the word, space or newlineChar\n            let token = tokens[i];\n\n            // if word is a new line\n            if (TextMetrics.isNewline(token))\n            {\n                // keep the new line\n                if (!collapseNewlines)\n                {\n                    lines += TextMetrics.addLine(line);\n                    canPrependSpaces = !collapseSpaces;\n                    line = '';\n                    width = 0;\n                    continue;\n                }\n\n                // if we should collapse new lines\n                // we simply convert it into a space\n                token = ' ';\n            }\n\n            // if we should collapse repeated whitespaces\n            if (collapseSpaces)\n            {\n                // check both this and the last tokens for spaces\n                const currIsBreakingSpace = TextMetrics.isBreakingSpace(token);\n                const lastIsBreakingSpace = TextMetrics.isBreakingSpace(line[line.length - 1]);\n\n                if (currIsBreakingSpace && lastIsBreakingSpace)\n                {\n                    continue;\n                }\n            }\n\n            // get word width from cache if possible\n            const tokenWidth = TextMetrics.getFromCache(token, letterSpacing, cache, context);\n\n            // word is longer than desired bounds\n            if (tokenWidth > wordWrapWidth)\n            {\n                // if we are not already at the beginning of a line\n                if (line !== '')\n                {\n                    // start newlines for overflow words\n                    lines += TextMetrics.addLine(line);\n                    line = '';\n                    width = 0;\n                }\n\n                // break large word over multiple lines\n                if (TextMetrics.canBreakWords(token, style.breakWords))\n                {\n                    // break word into characters\n                    const characters = TextMetrics.wordWrapSplit(token);\n\n                    // loop the characters\n                    for (let j = 0; j < characters.length; j++)\n                    {\n                        let char = characters[j];\n                        let lastChar = char;\n\n                        let k = 1;\n\n                        // we are not at the end of the token\n                        while (characters[j + k])\n                        {\n                            const nextChar = characters[j + k];\n\n                            // should not split chars\n                            if (!TextMetrics.canBreakChars(lastChar, nextChar, token, j, style.breakWords))\n                            {\n                                // combine chars & move forward one\n                                char += nextChar;\n                            }\n                            else\n                            {\n                                break;\n                            }\n\n                            lastChar = nextChar;\n                            k++;\n                        }\n\n                        j += k - 1;\n\n                        const characterWidth = TextMetrics.getFromCache(char, letterSpacing, cache, context);\n\n                        if (characterWidth + width > wordWrapWidth)\n                        {\n                            lines += TextMetrics.addLine(line);\n                            canPrependSpaces = false;\n                            line = '';\n                            width = 0;\n                        }\n\n                        line += char;\n                        width += characterWidth;\n                    }\n                }\n\n                // run word out of the bounds\n                else\n                {\n                    // if there are words in this line already\n                    // finish that line and start a new one\n                    if (line.length > 0)\n                    {\n                        lines += TextMetrics.addLine(line);\n                        line = '';\n                        width = 0;\n                    }\n\n                    const isLastToken = i === tokens.length - 1;\n\n                    // give it its own line if it's not the end\n                    lines += TextMetrics.addLine(token, !isLastToken);\n                    canPrependSpaces = false;\n                    line = '';\n                    width = 0;\n                }\n            }\n\n            // word could fit\n            else\n            {\n                // word won't fit because of existing words\n                // start a new line\n                if (tokenWidth + width > wordWrapWidth)\n                {\n                    // if its a space we don't want it\n                    canPrependSpaces = false;\n\n                    // add a new line\n                    lines += TextMetrics.addLine(line);\n\n                    // start a new line\n                    line = '';\n                    width = 0;\n                }\n\n                // don't add spaces to the beginning of lines\n                if (line.length > 0 || !TextMetrics.isBreakingSpace(token) || canPrependSpaces)\n                {\n                    // add the word to the current line\n                    line += token;\n\n                    // update width counter\n                    width += tokenWidth;\n                }\n            }\n        }\n\n        lines += TextMetrics.addLine(line, false);\n\n        return lines;\n    }\n\n    /**\n     * Convienience function for logging each line added during the wordWrap method.\n     * @param line    - The line of text to add\n     * @param newLine - Add new line character to end\n     * @returns A formatted line\n     */\n    private static addLine(line: string, newLine = true): string\n    {\n        line = TextMetrics.trimRight(line);\n\n        line = (newLine) ? `${line}\\n` : line;\n\n        return line;\n    }\n\n    /**\n     * Gets & sets the widths of calculated characters in a cache object\n     * @param key            - The key\n     * @param letterSpacing  - The letter spacing\n     * @param cache          - The cache\n     * @param context        - The canvas context\n     * @returns The from cache.\n     */\n    private static getFromCache(key: string, letterSpacing: number, cache: CharacterWidthCache,\n        context: ICanvasRenderingContext2D): number\n    {\n        let width = cache[key];\n\n        if (typeof width !== 'number')\n        {\n            width = TextMetrics._measureText(key, letterSpacing, context) + letterSpacing;\n            cache[key] = width;\n        }\n\n        return width;\n    }\n\n    /**\n     * Determines whether we should collapse breaking spaces.\n     * @param whiteSpace - The TextStyle property whiteSpace\n     * @returns Should collapse\n     */\n    private static collapseSpaces(whiteSpace: TextStyleWhiteSpace): boolean\n    {\n        return (whiteSpace === 'normal' || whiteSpace === 'pre-line');\n    }\n\n    /**\n     * Determines whether we should collapse newLine chars.\n     * @param whiteSpace - The white space\n     * @returns should collapse\n     */\n    private static collapseNewlines(whiteSpace: TextStyleWhiteSpace): boolean\n    {\n        return (whiteSpace === 'normal');\n    }\n\n    /**\n     * Trims breaking whitespaces from string.\n     * @param text - The text\n     * @returns Trimmed string\n     */\n    private static trimRight(text: string): string\n    {\n        if (typeof text !== 'string')\n        {\n            return '';\n        }\n\n        for (let i = text.length - 1; i >= 0; i--)\n        {\n            const char = text[i];\n\n            if (!TextMetrics.isBreakingSpace(char))\n            {\n                break;\n            }\n\n            text = text.slice(0, -1);\n        }\n\n        return text;\n    }\n\n    /**\n     * Determines if char is a newline.\n     * @param char - The character\n     * @returns True if newline, False otherwise.\n     */\n    private static isNewline(char: string): boolean\n    {\n        if (typeof char !== 'string')\n        {\n            return false;\n        }\n\n        return TextMetrics._newlines.includes(char.charCodeAt(0));\n    }\n\n    /**\n     * Determines if char is a breaking whitespace.\n     *\n     * It allows one to determine whether char should be a breaking whitespace\n     * For example certain characters in CJK langs or numbers.\n     * It must return a boolean.\n     * @param char - The character\n     * @param [_nextChar] - The next character\n     * @returns True if whitespace, False otherwise.\n     */\n    static isBreakingSpace(char: string, _nextChar?: string): boolean\n    {\n        if (typeof char !== 'string')\n        {\n            return false;\n        }\n\n        return TextMetrics._breakingSpaces.includes(char.charCodeAt(0));\n    }\n\n    /**\n     * Splits a string into words, breaking-spaces and newLine characters\n     * @param text - The text\n     * @returns A tokenized array\n     */\n    private static tokenize(text: string): string[]\n    {\n        const tokens: string[] = [];\n        let token = '';\n\n        if (typeof text !== 'string')\n        {\n            return tokens;\n        }\n\n        for (let i = 0; i < text.length; i++)\n        {\n            const char = text[i];\n            const nextChar = text[i + 1];\n\n            if (TextMetrics.isBreakingSpace(char, nextChar) || TextMetrics.isNewline(char))\n            {\n                if (token !== '')\n                {\n                    tokens.push(token);\n                    token = '';\n                }\n\n                tokens.push(char);\n\n                continue;\n            }\n\n            token += char;\n        }\n\n        if (token !== '')\n        {\n            tokens.push(token);\n        }\n\n        return tokens;\n    }\n\n    /**\n     * Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior.\n     *\n     * It allows one to customise which words should break\n     * Examples are if the token is CJK or numbers.\n     * It must return a boolean.\n     * @param _token - The token\n     * @param breakWords - The style attr break words\n     * @returns Whether to break word or not\n     */\n    static canBreakWords(_token: string, breakWords: boolean): boolean\n    {\n        return breakWords;\n    }\n\n    /**\n     * Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior.\n     *\n     * It allows one to determine whether a pair of characters\n     * should be broken by newlines\n     * For example certain characters in CJK langs or numbers.\n     * It must return a boolean.\n     * @param _char - The character\n     * @param _nextChar - The next character\n     * @param _token - The token/word the characters are from\n     * @param _index - The index in the token of the char\n     * @param _breakWords - The style attr break words\n     * @returns whether to break word or not\n     */\n    static canBreakChars(_char: string, _nextChar: string, _token: string, _index: number,\n        _breakWords: boolean): boolean\n    {\n        return true;\n    }\n\n    /**\n     * Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior.\n     *\n     * It is called when a token (usually a word) has to be split into separate pieces\n     * in order to determine the point to break a word.\n     * It must return an array of characters.\n     * @param token - The token to split\n     * @returns The characters of the token\n     * @see TextMetrics.graphemeSegmenter\n     */\n    static wordWrapSplit(token: string): string[]\n    {\n        return TextMetrics.graphemeSegmenter(token);\n    }\n\n    /**\n     * Calculates the ascent, descent and fontSize of a given font-style\n     * @param font - String representing the style of the font\n     * @returns Font properties object\n     */\n    public static measureFont(font: string): IFontMetrics\n    {\n        // as this method is used for preparing assets, don't recalculate things if we don't need to\n        if (TextMetrics._fonts[font])\n        {\n            return TextMetrics._fonts[font];\n        }\n\n        const properties: IFontMetrics = {\n            ascent: 0,\n            descent: 0,\n            fontSize: 0,\n        };\n\n        const canvas = TextMetrics._canvas;\n        const context = TextMetrics._context;\n\n        context.font = font;\n\n        const metricsString = TextMetrics.METRICS_STRING + TextMetrics.BASELINE_SYMBOL;\n        const width = Math.ceil(context.measureText(metricsString).width);\n        let baseline = Math.ceil(context.measureText(TextMetrics.BASELINE_SYMBOL).width);\n        const height = Math.ceil(TextMetrics.HEIGHT_MULTIPLIER * baseline);\n\n        baseline = baseline * TextMetrics.BASELINE_MULTIPLIER | 0;\n\n        if (width === 0 || height === 0)\n        {\n            TextMetrics._fonts[font] = properties;\n\n            return properties;\n        }\n\n        canvas.width = width;\n        canvas.height = height;\n\n        context.fillStyle = '#f00';\n        context.fillRect(0, 0, width, height);\n\n        context.font = font;\n\n        context.textBaseline = 'alphabetic';\n        context.fillStyle = '#000';\n        context.fillText(metricsString, 0, baseline);\n\n        const imagedata = context.getImageData(0, 0, width, height).data;\n        const pixels = imagedata.length;\n        const line = width * 4;\n\n        let i = 0;\n        let idx = 0;\n        let stop = false;\n\n        // ascent. scan from top to bottom until we find a non red pixel\n        for (i = 0; i < baseline; ++i)\n        {\n            for (let j = 0; j < line; j += 4)\n            {\n                if (imagedata[idx + j] !== 255)\n                {\n                    stop = true;\n                    break;\n                }\n            }\n            if (!stop)\n            {\n                idx += line;\n            }\n            else\n            {\n                break;\n            }\n        }\n\n        properties.ascent = baseline - i;\n\n        idx = pixels - line;\n        stop = false;\n\n        // descent. scan from bottom to top until we find a non red pixel\n        for (i = height; i > baseline; --i)\n        {\n            for (let j = 0; j < line; j += 4)\n            {\n                if (imagedata[idx + j] !== 255)\n                {\n                    stop = true;\n                    break;\n                }\n            }\n\n            if (!stop)\n            {\n                idx -= line;\n            }\n            else\n            {\n                break;\n            }\n        }\n\n        properties.descent = i - baseline;\n        properties.fontSize = properties.ascent + properties.descent;\n\n        TextMetrics._fonts[font] = properties;\n\n        return properties;\n    }\n\n    /**\n     * Clear font metrics in metrics cache.\n     * @param {string} [font] - font name. If font name not set then clear cache for all fonts.\n     */\n    public static clearMetrics(font = ''): void\n    {\n        if (font)\n        {\n            delete TextMetrics._fonts[font];\n        }\n        else\n        {\n            TextMetrics._fonts = {};\n        }\n    }\n\n    /**\n     * Cached canvas element for measuring text\n     * TODO: this should be private, but isn't because of backward compat, will fix later.\n     * @ignore\n     */\n    public static get _canvas(): ICanvas\n    {\n        if (!TextMetrics.__canvas)\n        {\n            let canvas: ICanvas;\n\n            try\n            {\n                // OffscreenCanvas2D measureText can be up to 40% faster.\n                const c = new OffscreenCanvas(0, 0);\n                const context = c.getContext('2d', contextSettings);\n\n                if (context?.measureText)\n                {\n                    TextMetrics.__canvas = c;\n\n                    return c;\n                }\n\n                canvas = settings.ADAPTER.createCanvas();\n            }\n            catch (ex)\n            {\n                canvas = settings.ADAPTER.createCanvas();\n            }\n            canvas.width = canvas.height = 10;\n            TextMetrics.__canvas = canvas;\n        }\n\n        return TextMetrics.__canvas;\n    }\n\n    /**\n     * TODO: this should be private, but isn't because of backward compat, will fix later.\n     * @ignore\n     */\n    public static get _context(): ICanvasRenderingContext2D\n    {\n        if (!TextMetrics.__context)\n        {\n            TextMetrics.__context = TextMetrics._canvas.getContext('2d', contextSettings);\n        }\n\n        return TextMetrics.__context;\n    }\n}\n"],"names":["_TextMetrics"],"mappings":"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"}