Fork me on GitHub

core/fontMetrics.js

            
            /* eslint no-unused-vars:0 */
/**
 * This module contains metrics regarding fonts and individual symbols. The sigma
 * and xi variables, as well as the metricMap map contain data extracted from
 * TeX, TeX font metrics, and the TTF files. These data are then exposed via the
 * `metrics` variable and the getCharacterMetrics function.
 * @module fontMetrics
 * @private
 */
define(['mathlive/core/fontMetricsData'], function(metricMap) {

// This metricMap contains a mapping from font name and character code to character
// metrics, including height, depth, italic correction, and skew (kern from the
// character to the corresponding \skewchar)
// This map is generated via `make metrics`. It should not be changed manually.

const hangulRegex = /[\uAC00-\uD7AF]/;

// This regex combines
// - Hiragana: [\u3040-\u309F]
// - Katakana: [\u30A0-\u30FF]
// - CJK ideograms: [\u4E00-\u9FAF]
// - Hangul syllables: [\uAC00-\uD7AF]
// Notably missing are half width Katakana and Romaji glyphs.
const cjkRegex =
    /[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FAF]|[\uAC00-\uD7AF]/;


/**
 * 
 * In TeX, there are actually three sets of dimensions, one for each of
 * textstyle, scriptstyle, and scriptscriptstyle.  These are provided in the
 * the arrays below, in that order.
 *
 * The font metrics are stored in fonts cmsy10, cmsy7, and cmsy5 respectively.
 * This was determined by running the following script:
 *``` bash
      latex -interaction=nonstopmode \
      '\documentclass{article}\usepackage{amsmath}\begin{document}' \
      '$a$ \expandafter\show\the\textfont2' \
      '\expandafter\show\the\scriptfont2' \
      '\expandafter\show\the\scriptscriptfont2' \
      '\stop'
  ```
 * The metrics themselves were retrieved using the following commands:
 * ``` bash
      tftopl cmsy10
      tftopl cmsy7
      tftopl cmsy5
    ```
 *
 * The output of each of these commands is quite lengthy.  The only part we
 * care about is the FONTDIMEN section. Each value is measured in EMs.
 * @memberof module:fontMetrics
 */
const SIGMAS = {
    slant: [0.250, 0.250, 0.250],       // sigma1
    space: [0.000, 0.000, 0.000],       // sigma2
    stretch: [0.000, 0.000, 0.000],     // sigma3
    shrink: [0.000, 0.000, 0.000],      // sigma4
    xHeight: [0.431, 0.431, 0.431],     // sigma5
    quad: [1.000, 1.171, 1.472],        // sigma6
    extraSpace: [0.000, 0.000, 0.000],  // sigma7
    num1: [0.677, 0.732, 0.925],        // sigma8
    num2: [0.394, 0.384, 0.387],        // sigma9
    num3: [0.444, 0.471, 0.504],        // sigma10
    denom1: [0.686, 0.752, 1.025],      // sigma11
    denom2: [0.345, 0.344, 0.532],      // sigma12
    sup1: [0.413, 0.503, 0.504],        // sigma13
    sup2: [0.363, 0.431, 0.404],        // sigma14
    sup3: [0.289, 0.286, 0.294],        // sigma15
    sub1: [0.150, 0.143, 0.200],        // sigma16
    sub2: [0.247, 0.286, 0.400],        // sigma17
    supDrop: [0.386, 0.353, 0.494],     // sigma18
    subDrop: [0.050, 0.071, 0.100],     // sigma19
    delim1: [2.390, 1.700, 1.980],      // sigma20
    delim2: [1.010, 1.157, 1.420],      // sigma21
    axisHeight: [0.250, 0.250, 0.250],  // sigma22
};

// These font metrics are extracted from TeX by using
// \font\a=cmex10
// \showthe\fontdimenX\a
// where X is the corresponding variable number. These correspond to the font
// parameters of the extension fonts (family 3). See the TeXbook, page 441.
const xi1 = 0;
const xi2 = 0;
const xi3 = 0;
const xi4 = 0;
const xi5 = 0.431;
const xi6 = 1;
const xi7 = 0;
const xi8 = 0.04;
const xi9 = 0.111;
const xi10 = 0.166;
const xi11 = 0.2;
const xi12 = 0.6;
const xi13 = 0.1;

// This value determines how large a pt is, for metrics which are defined in
// terms of pts.
// This value is also used in katex.less; if you change it make sure the values
// match.
const ptPerEm = 10.0;

// The space between adjacent `|` columns in an array definition. From
// article.cls.txt:455
const doubleRuleSep = 2.0 / ptPerEm;

/**
 * This is just a mapping from common names to real metrics
 */
const metrics = {
    defaultRuleThickness: xi8,
    bigOpSpacing1: xi9,
    bigOpSpacing2: xi10,
    bigOpSpacing3: xi11,
    bigOpSpacing4: xi12,
    bigOpSpacing5: xi13,
    ptPerEm: ptPerEm,
    pxPerEm: ptPerEm * 4.0 / 3.0,   // A CSS pt is fixed at 1.333px
    doubleRuleSep: 2.0 / ptPerEm,
    arraycolsep: 5.0 / ptPerEm,
    baselineskip: 12.0 / ptPerEm,
    arrayrulewidth: 0.4 / ptPerEm,
    fboxsep: 3 / ptPerEm,               // From letter.dtx:1626
    fboxrule: 0.4 / ptPerEm,               // From letter.dtx:1627
};


// These are very rough approximations.  We default to Times New Roman which
// should have Latin-1 and Cyrillic characters, but may not depending on the
// operating system.  The metrics do not account for extra height from the
// accents.  In the case of Cyrillic characters which have both ascenders and
// descenders we prefer approximations with ascenders, primarily to prevent
// the fraction bar or root line from intersecting the glyph.
// TODO(kevinb) allow union of multiple glyph metrics for better accuracy.
const extraCharacterMap = {
    '\u00a0': '\u0020',     // NON-BREAKING SPACE is like space
    '\u200b': '\u0020',     // ZERO WIDTH SPACE is like space
    // Latin-1
    'Å': 'A',
    'Ç': 'C',
    'Ð': 'D',
    'Þ': 'o',
    'å': 'a',
    'ç': 'c',
    'ð': 'd',
    'þ': 'o',


    // Cyrillic
    'А': 'A',
    'Б': 'B',
    'В': 'B',
    'Г': 'F',
    'Д': 'A',
    'Е': 'E',
    'Ж': 'K',
    'З': '3',
    'И': 'N',
    'Й': 'N',
    'К': 'K',
    'Л': 'N',
    'М': 'M',
    'Н': 'H',
    'О': 'O',
    'П': 'N',
    'Р': 'P',
    'С': 'C',
    'Т': 'T',
    'У': 'y',
    'Ф': 'O',
    'Х': 'X',
    'Ц': 'U',
    'Ч': 'h',
    'Ш': 'W',
    'Щ': 'W',
    'Ъ': 'B',
    'Ы': 'X',
    'Ь': 'B',
    'Э': '3',
    'Ю': 'X',
    'Я': 'R',
    'а': 'a',
    'б': 'b',
    'в': 'a',
    'г': 'r',
    'д': 'y',
    'е': 'e',
    'ж': 'm',
    'з': 'e',
    'и': 'n',
    'й': 'n',
    'к': 'n',
    'л': 'n',
    'м': 'm',
    'н': 'n',
    'о': 'o',
    'п': 'n',
    'р': 'p',
    'с': 'c',
    'т': 'o',
    'у': 'y',
    'ф': 'b',
    'х': 'x',
    'ц': 'n',
    'ч': 'n',
    'ш': 'w',
    'щ': 'w',
    'ъ': 'a',
    'ы': 'm',
    'ь': 'a',
    'э': 'e',
    'ю': 'm',
    'я': 'r',

};

/**
 * This function is a convenience function for looking up information in the
 * metricMap table. It takes a character as a string, and a font name.
 *
 * Note: the `width` property may be undefined if fontMetricsData.js wasn't
 * built using `Make extended_metrics`.
 * @memberof module:fontMetrics
 * @private
 */
const getCharacterMetrics = function(character, fontName) {
    if (fontName === 'mathbb') fontName = 'AMS-Regular';
    console.assert(character.length === 1);
    console.assert(metricMap[fontName], 'Unknown font "' + fontName + '"');

    let ch = character.charCodeAt(0);

    if (character[0] in extraCharacterMap) {
        ch = extraCharacterMap[character[0]].charCodeAt(0);
    } else if (cjkRegex.test(character[0])) {
        ch = 'M'.charCodeAt(0);
    }
    const metrics = metricMap[fontName][ch];

    if (!metrics) {
        // console.warn( 
        //     'No metrics for ' +
        //     '"' + character + '" (U+' + ('000000' + ch.toString(16)).substr(-6) + ')' +
        //     ' in font "' + fontName + '"');
        // Assume default values.
        // depth + height should be less than 1.0 em
        return {
            depth: .20,
            height: .70,
            italic: 0,
            skew: 0
        }
    }

    if (metrics) {
        return {
            depth: metrics[0],
            height: metrics[1],
            italic: metrics[2],
            skew: metrics[3]
        }
    }

    return null;
}


/**
 * 
 * @param {number|string} value If value is a string, it may be suffixed
 * with a unit, which will override the `unit` paramter
 * @param {string} unit 
 * @param {number} precision
 */
function convertDimenToEm(value, unit, precision) {
    if (typeof value === 'string') {
        const m = value.match(/([0-9.]*)\s*([a-z]*)/);
        if (!m) {
            value = parseFloat(value);
        } else {
            value = parseFloat(m[1]);
            unit = m[2].toLowerCase();
        }
    }

    // If the units are missing, TeX assumes 'pt'
    let f = 1;
    if (unit === 'pt') {
        f = 1;
    } else if (unit === 'mm') {
        f = 7227 / 2540;
    } else if (unit === 'cm') {
        f = 7227 / 254;
    } else if (unit === 'ex') {
        f = 35271 / 8192;
    } else if (unit === 'px') {
        f = 3.0 / 4.0;
    } else if (unit === 'em') {
        f = metrics.ptPerEm;
    } else if (unit === 'bp') {
        f = 803 / 800;
    } else if (unit === 'dd') {
        f = 1238 / 1157;
    } else if (unit === 'pc') {
        f = 12;
    } else if (unit === 'in') {
        f = 72.27;
    } else if (unit === 'mu') {
        f = 10 / 18;
    }

    if (precision) {
        const factor = Math.pow(10, precision);
        return Math.round((value / metrics.ptPerEm) * f * factor) / factor;
    }

    return (value / metrics.ptPerEm) * f;
}

function convertDimenToPx(value, unit) {
    // if (unit === 'px') return value;
    return convertDimenToEm(value, unit) * (4.0 / 3.0) * metrics.ptPerEm;
}

return {
    toEm : convertDimenToEm,
    toPx : convertDimenToPx,
    metrics: metrics,
    sigmas: SIGMAS,
    getCharacterMetrics: getCharacterMetrics,
    cjkRegex: cjkRegex,
    hangulRegex: hangulRegex
}
})