StreamPDFStreamPDF
Back to blog

PDF.js Source Walkthrough: fonts_utils

October 27, 2026 · StreamPDF Team

PDF.js Source Walkthrough: fonts_utils

Introduction

The previous articles have already walked the "document" line to the end: xref retrieves objects by object number, parser turns bytes into objects, document holds the page model, evaluator translates content streams into operator lists, stream supplies chunked bytes, primitives defines the object primitives, and catalog organises the root dictionary into document semantics. This article opens the "font layer", the place where the text operators Tj and TJ in a content stream actually land.

Font parsing is divided into three parts in PDF.js: evaluator.js reads the font dictionary and resources, fonts.js decides the font type, constructs the Font subclasses and builds the character-to-glyph mapping, and Type1, CFF, TrueType and Type3 each have their own parser. src/core/fonts_utils.js (387 lines) sits between them; it is not the implementation of any single font type, but the drawer of constants and algorithms shared by all those branches.

In one sentence: it is the common toolbox of the font layer. It holds no font state and reads no PDF object; it only provides reusable pure functions and constant tables for flags, the standard glyph order, glyph-name recovery, character mapping, font-name normalisation, vertical mapping and Type3 bitmap compilation.

Module Overview

fonts_utils.js splits into eight blocks, each reused by at least two font branches:

BlockKey membersResponsibility
Policy switchSEAC_ANALYSIS_ENABLEDDecides whether a seac composite glyph is split into a base glyph and an accent drawn separately
FlagsFontFlagsMaps the bits in /Flags to numeric masks
Glyph orderMacStandardGlyphOrdering258 standard Mac glyph names arranged by glyph id
Glyph-name utilityrecoverGlyphNameRecovers non-standard names such as uniXXXX into standard names
Character mappingtype1FontGlyphMappingChar code to glyph id mapping for Type1 and simple CFF
Font-name utilitynormalizeFontNameUnifies commas, underscores and whitespace for standard-font table matching
Vertical utilitygetVerticalPresentationFormLazily built table from horizontal characters to vertical presentation code points
Type3 compilationcompileType3GlyphCompiles a bitmap glyph into the operator arguments of a vector path

Its place in the font chain can be drawn like this:

// Page content stream
//   → evaluator.js            reads /Font, /Encoding, /Widths, /FontDescriptor
//   → fonts.js                Font subclasses: TrueType / Type1 / CFF / Type3
//   → fonts_utils.js          this article: shared flags, glyph names, character mapping, Type3 compilation
//   → type1_font.js           built-in encoding and seac analysis
//   → cff_font.js             built-in encoding and charset
//   → font_renderer.js        generates glyph outlines
//   → glyph.js                width, unicode and outline of a single character

Version note: This article follows exactly the version of fonts_utils.js in the current repository, which exports only eight members: compileType3Glyph, FontFlags, getVerticalPresentationForm, MacStandardGlyphOrdering, normalizeFontName, recoverGlyphName, SEAC_ANALYSIS_ENABLED and type1FontGlyphMapping. The getFontNameToFileMap, getStdFontMap, getNonStdFontMap, getSerifFonts, getSymbolsFonts and getGlyphMapForStandardFonts that older material often mentions are not in this file in this version but in standard_fonts.js; the only things shared by both sides are normalizeFontName and MacStandardGlyphOrdering. Do not look for these functions according to the old file layout you remember when reading the source.

Dependency Direction

The file header has only five imports, all pointing to lower-level shared modules:

import { DrawOPS, info, OPS } from '../shared/util.js';
import { getEncoding, StandardEncoding } from './encodings.js';
import { getGlyphsUnicode } from './glyphlist.js';
import { getLookupTableFactory } from './core_utils.js';
import { getUnicodeForGlyph } from './unicode.js';
DependencyCapability providedUse site in this file
shared/utilOPS, DrawOPS, infoType3 compilation result, recovery-failure log
encodingsgetEncoding, StandardEncodingBase encodings of type1FontGlyphMapping
glyphlistgetGlyphsUnicodeReverse glyph-name lookup and /Differences recovery
core_utilsgetLookupTableFactoryLazy loading of the vertical presentation form table
unicodegetUnicodeForGlyphParsing code points out of uniXXXX

It has no reverse dependency on fonts.js, type1_font.js or cff_font.js, and does not depend on evaluator.js either. This one-way dependency is the precondition for every font branch to reference it safely: the shared layer does not know that font formats exist, while font formats are free to reference the shared layer.

Constant Layer: SEAC Switch and FontFlags

SEAC Analysis Switch

The file starts with a long comment and a boolean constant:

// Accented characters have issues on Windows and Linux. When this flag is
// enabled glyphs that use seac and seac style endchar operators are truncated
// and we instead just store the glyph id's of the base glyph and its accent to
// be drawn individually.
// Linux (freetype) requires that when a seac style endchar is used
// that the charset must be a predefined one, however we build a
// custom one. Windows just refuses to draw glyphs with seac operators.
const SEAC_ANALYSIS_ENABLED = true;

seac is the composite accent operator in Type1 fonts: a character is assembled from two glyphs, a "base glyph + accent", specified by the four extra parameters of endchar. The browser-side font renderer does not take this: freetype on Linux requires that when seac is used the charset must be a predefined one, while PDF.js builds a custom charset in order to handle subset fonts correctly; Windows simply refuses to draw glyphs with seac.

So PDF.js chooses to split the composite at parse time: when it meets seac, it no longer keeps that endchar but records the glyph ids of the base glyph and its accent and hands them to the upper layer to draw separately. That way the renderer only has to deal with ordinary paths and no longer depends on platform support for seac.

SEAC_ANALYSIS_ENABLED is a module-level constant and cannot be configured at runtime. It is referenced in four places:

Use sitePassed toEffect
FontRendererFactory in fonts.jsGlyph rendererWhether to draw the split base glyph and accent
Type1Parser in type1_font.jsHeader and eexec sectionRecords the component glyphs of seac
CFFParser in cff_font.jsCFF parserHandles the seac-style endchar in CFF
Glyph mapping in fonts.jsComposite accent mappingSplits seac into two glyph ids

All four must see the same value, otherwise "split accents" and "characters interpreted through seac" would be mixed inside one font.

FontFlags Bit Masks

The second constant freezes the font flag bits of the PDF specification into numbers:

const FontFlags = {
  FixedPitch: 1,
  Serif: 2,
  Symbolic: 4,
  Script: 8,
  Nonsymbolic: 32,
  Italic: 64,
  AllCap: 65536,
  SmallCap: 131072,
  ForceBold: 262144,
};

These values correspond to the bit positions of /Flags in /FontDescriptor, not to consecutive numbering:

MemberDecimalBit positionSpecification meaning
FixedPitch11Monospaced font
Serif22Has serifs
Symbolic43Symbolic font using a built-in encoding
Script84Script face
Nonsymbolic326Non-symbolic font using the standard encoding
Italic647Italic
AllCap6553617All capitals
SmallCap13107218Small capitals
ForceBold26214419Forced bold

In this version only four are actually read on the core path: FixedPitch, Serif, Symbolic and Nonsymbolic. Italic, Script, AllCap, SmallCap and ForceBold are defined after the specification table, but the current code does not branch on them. Note that Symbolic and Nonsymbolic should be mutually exclusive in the specification, yet in practice many fonts set both; evaluator.js has a dedicated fallback for this: when a TrueType font sets both and has /Differences, it clears Symbolic and treats the font as non-symbolic.

BitConsuming locationPurpose
FixedPitchisMonospace in fonts.js, width inference in evaluator.jsMonospace detection
SerifisSerifFont in fonts.js, evaluator.jsSerif font fallback
Symbolictype1FontGlyphMapping, fonts.jsWhether to choose the built-in or the standard encoding
NonsymbolicDefault-encoding fallback in evaluator.jsUse StandardEncoding by default

fonts_utils.js only defines the masks and does not interpret them. The decision logic stays in evaluator.js and fonts.js, because "what this bit means for the current font type" depends on whether the font is Type1, TrueType or CFF, and cannot live in the shared constant layer.

MacStandardGlyphOrdering: A Name Table Ordered by Glyph Id

MacStandardGlyphOrdering is a 258-entry array whose index is the glyph id and whose element is a glyph name:

// prettier-ignore
const MacStandardGlyphOrdering = [
  ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl",
  "numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft",
  "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash",
  // ...
  "Lslash", "lslash", "Scaron", "scaron", "Zcaron", "zcaron", "brokenbar",
  "Eth", "eth", "Yacute", "yacute", "Thorn", "thorn", "minus", "multiply",
  // ...
  "scedilla", "Cacute", "cacute", "Ccaron", "ccaron", "dcroat"];

The first 258 names come from the Macintosh standard glyph order; the first three are always .notdef, .null and nonmarkingreturn, and printable space starts at the fourth. This table is easily mistaken for "yet another encoding table", but its direction is exactly the opposite of the tables in encodings.js:

TableMeaning of the indexMeaning of the elementTypical consumer
StandardEncoding and friendschar code, 0 to 255glyph nameevaluator.js, fonts.js building the default encoding
MacStandardGlyphOrderingglyph id, 0 to 257glyph nameTrueType post table, standard-font glyph id mapping

It has two direct consumers. The first is the TrueType post table: format 1 is exactly this table, while format 2 appends custom names starting at index 258, and the source condition is that when j < 258 this table is used, otherwise customNames[j - 258]. The second is getGlyphMapForMacOrderedFonts in standard_fonts.js, which walks from index 3 and converts every glyph name through getGlyphsUnicode, producing a glyph id to Unicode mapping.

This table is a module-level constant and does not go through getLookupTableFactory. Because it is only a fixed array it has no construction cost; every access returns the same reference and it is never dropped by cleanup. What really needs lazy loading is the Unicode mapping derived from it, and those mappings live in the factories of standard_fonts.js.

recoverGlyphName: Recovering Standard Names from Non-Standard Ones

Some generators write glyph names in the form uni0041 instead of A. recoverGlyphName is responsible for restoring such names:

function recoverGlyphName(name, glyphsUnicodeMap) {
  if (glyphsUnicodeMap[name] !== undefined) {
    return name;
  }
  // The glyph name is non-standard, trying to recover.
  const unicode = getUnicodeForGlyph(name, glyphsUnicodeMap);
  if (unicode !== -1) {
    for (const key in glyphsUnicodeMap) {
      if (glyphsUnicodeMap[key] === unicode) {
        return key;
      }
    }
  }
  info('Unable to recover a standard glyph name for: ' + name);
  return name;
}

Three clear steps:

  1. The name is already in the standard glyph table, so return it as is. This is the overwhelming majority of cases and costs a single property lookup.
  2. If it is not in the table, hand it to getUnicodeForGlyph, which tries to parse a code point out of uniXXXX or uXXXX{XX}.
  3. Once a code point is parsed, walk glyphsUnicodeMap in reverse to find the first key with the same Unicode value and return it as the standard name.

The third step is a linear scan and looks expensive, but its trigger is narrow: it is reached only when a non-standard name appears in /Differences. glyphsUnicodeMap comes from getGlyphsUnicode in glyphlist.js, also a product of getLookupTableFactory, built once per worker, so the reverse scan works on that same shared data.

If all three steps fail, the function returns the original name rather than an empty value. Callers keep using that name for table lookups: type1FontGlyphMapping uses glyphNames.indexOf, and the cmap branch of fonts.js uses glyphsUnicodeMap; the former falls back to .notdef when the lookup misses, the latter simply skips the glyph. Returning the original name lets both paths share one degradation mode without an extra error type.

Design note: Glyph-name recovery happens only when "the mapping has already decided to do a table lookup", not when the font dictionary is parsed and all names are cleaned in bulk. Bulk cleaning would make every font pay for a full-table scan, while real bad names usually appear in only a few /Differences entries; moving recovery to the one lookup that fails keeps the cost proportional to the size of the problem.

type1FontGlyphMapping: From Char Code to Glyph Id

This is the longest and most reused function in the file: Type1 fonts and simple CFF fonts both use it to build the character mapping. It takes three inputs: the font properties, the built-in encoding carried by the font program, and an array of glyph names ordered by glyph id; it outputs a table from char code to glyph id.

function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) {
  const charCodeToGlyphId = Object.create(null);
  let glyphId, charCode, baseEncoding;
  const isSymbolicFont = !!(properties.flags & FontFlags.Symbolic);
 
  if (properties.isInternalFont) {
    baseEncoding = builtInEncoding;
    for (charCode = 0; charCode < baseEncoding.length; charCode++) {
      glyphId = glyphNames.indexOf(baseEncoding[charCode]);
      charCodeToGlyphId[charCode] = glyphId >= 0 ? glyphId : /* notdef = */ 0;
    }
  } else if (properties.baseEncodingName) {
    // If a valid base encoding name was used, the mapping is initialized with
    // that.
    baseEncoding = getEncoding(properties.baseEncodingName);
    for (charCode = 0; charCode < baseEncoding.length; charCode++) {
      glyphId = glyphNames.indexOf(baseEncoding[charCode]);
      charCodeToGlyphId[charCode] = glyphId >= 0 ? glyphId : /* notdef = */ 0;
    }
  } else if (isSymbolicFont) {
    // For a symbolic font the encoding should be the fonts built-in encoding.
    for (charCode in builtInEncoding) {
      charCodeToGlyphId[charCode] = builtInEncoding[charCode];
    }
  } else {
    // For non-symbolic fonts that don't have a base encoding the standard
    // encoding should be used.
    baseEncoding = StandardEncoding;
    // ...
  }
  // ...
  return charCodeToGlyphId;
}

The order in which the four branches are selected is itself a priority rule:

OrderConditionBase encodingWhy it sits at this level
1properties.isInternalFontThe encoding built into the font programThe internal encoding of the standard 14 fonts is the most trustworthy
2properties.baseEncodingNameThe table found by getEncodingAn explicit /Encoding takes precedence over the built-in encoding
3FontFlags.SymbolicThe built-in encoding, with glyph ids copied directlyFor a symbolic font the code itself is the glyph id
4Anything elseStandardEncodingThe specification requires non-symbolic fonts to use the standard encoding by default

The first, second and fourth branches do the same thing: take the glyph name from the base encoding and look up its index in glyphNames. When it is not found they write 0, that is .notdef. The third branch is different: it does not use indexOf, because builtInEncoding of a symbolic font is already a char code to glyph id mapping, so copying is enough.

After a branch has chosen the base encoding, /Differences is merged in at the end:

// Lastly, merge in the differences.
const differences = properties.differences;
let glyphsUnicodeMap;
if (differences) {
  for (charCode in differences) {
    const glyphName = differences[charCode];
    glyphId = glyphNames.indexOf(glyphName);
 
    if (glyphId === -1) {
      glyphsUnicodeMap ??= getGlyphsUnicode();
 
      const standardGlyphName = recoverGlyphName(glyphName, glyphsUnicodeMap);
      if (standardGlyphName !== glyphName) {
        glyphId = glyphNames.indexOf(standardGlyphName);
      }
    }
    charCodeToGlyphId[charCode] = glyphId >= 0 ? glyphId : /* notdef = */ 0;
  }
}

glyphsUnicodeMap ??= getGlyphsUnicode() uses a "build only on first need" form: if all /Differences names hit glyphNames directly, the font never touches the Unicode table at all. This is common for embedded subset fonts and saves the initialisation of a table with roughly four thousand entries.

Differences overrides the base encoding: the same char code first gets a name from the base encoding and is then rewritten by the differences array. This is exactly the "base encoding + differences" model described in section 9.6.6.2 of the PDF specification.

Design note: That the mapping function receives glyphNames instead of parsing the font program itself is the most important decoupling in this layer. The /CharStrings of Type1, the charset of CFF and the post table of TrueType can all produce "an array of names whose index is the glyph id", so a single set of char-code-to-glyph-id rules can serve three font formats. Font-format differences stay in their own parsers, and the shared layer only knows about name arrays.

normalizeFontName: Normalising Font Names

function normalizeFontName(name) {
  return name.replaceAll(/[,_]/g, '-').replaceAll(/\s/g, '');
}

The two regular expressions handle two kinds of noise:

InputOutputProblem handled
Arial,BoldArial-BoldSome generators separate weight and style with a comma
Arial_Bold_ItalicArial-Bold-ItalicUnderscores used as hyphens
Times New RomanTimesNewRomanWhitespace mixed into the font name
ArialMT,ItalicArialMT-ItalicComma and style suffix appearing together

It only normalises separators; it does not fold case and does not replace aliases. The normalised name is used directly as the key of the tables in standard_fonts.js: getStandardFontName and isKnownFontName, for example, both call it before the lookup. font_substitutions.js and xfa_fonts.js also use it to build keys for the substitution table, so the same font name has the same shape on both the "standard-font recognition" and "font substitution" paths.

Why not normalise once while parsing the font dictionary and write the result back into /BaseFont? Because /BaseFont has other uses: error messages, font cache keys and debug output all want to see the original name. Normalisation is a poorly reversible transformation (once spaces are removed they cannot be restored), so it should be done temporarily before a lookup, not by rewriting the original data.

getVerticalPresentationForm: Vertical Presentation Forms

Vertical text cannot simply be horizontal text rotated ninety degrees; punctuation, brackets and ellipses have dedicated presentation code points in vertical writing. This table is wrapped by getLookupTableFactory and is built on first call:

const getVerticalPresentationForm = getLookupTableFactory(t => {
  t[0x2013] = 0xfe32; // EN DASH
  t[0x2014] = 0xfe31; // EM DASH
  t[0x2025] = 0xfe30; // TWO DOT LEADER
  t[0x2026] = 0xfe19; // HORIZONTAL ELLIPSIS
  t[0x3001] = 0xfe11; // IDEOGRAPHIC COMMA
  t[0x3002] = 0xfe12; // IDEOGRAPHIC FULL STOP
  // ...
  t[0xff5b] = 0xfe37; // FULLWIDTH LEFT CURLY BRACKET
  t[0xff5d] = 0xfe38; // FULLWIDTH RIGHT CURLY BRACKET
});

The table has 33 entries in total, covering dashes, ellipses, CJK punctuation and full-width brackets. It is used in exactly one place:

if (this.missingFile && this.vertical && fontChar.length === 1) {
  const vertical = getVerticalPresentationForm()[fontChar.charCodeAt(0)];
  if (vertical) {
    fontChar = unicode = String.fromCharCode(vertical);
  }
}

The conditions are strict: the substitution happens only when the font file is missing, the text is vertical and the character is a single code point all hold at the same time. An embedded font carries its own vertical glyphs and does not need this table; multi-code-point characters (a combining sequence, for example) have no one-to-one presentation form either. The substitution rewrites fontChar and unicode together, ensuring that glyph lookup and text extraction see the same code point.

The behaviour of getLookupTableFactory also deserves a separate note: on the first call it creates a prototype-less object, runs the initialiser and then clears the initialiser; later calls return the same object directly. This table is not part of cleanup, because clearGlobalCaches only clears the primitive cache, the pattern cache and the Unicode category cache; the vertical mapping is pure static data unrelated to any document, and clearing it would only make the next access rebuild it. What really needs to be cleared with a document are font objects and the CMap cache, which live in the cleanup paths of Catalog and PartialEvaluator.

Design note: This table is "a patch for a missing platform", not a general-purpose vertical shaper. It is enabled only when missingFile is set, which means that when a font file exists everything is left to the font's own cmap; the table itself does not need to cover every vertical punctuation mark, only the ones most often met when no font is embedded.

compileType3Glyph: Compiling Bitmap Glyphs into Paths

Glyphs of a Type3 font are not outlines but a content stream, usually containing a bitmap mask. Drawing a bitmap mask per glyph on a canvas is expensive, so compileType3Glyph compiles the mask once into a vector path and afterwards draws it as an ordinary filled path.

The entry point applies two safeguards first:

// To disable Type3 compilation, set the value to `-1`.
const MAX_SIZE_TO_COMPILE = 1000;
 
function compileType3Glyph({ data: img, width, height }) {
  if (width > MAX_SIZE_TO_COMPILE || height > MAX_SIZE_TO_COMPILE) {
    return null;
  }
 
  const POINT_TO_PROCESS_LIMIT = 1000;
  // ...

If the width or the height exceeds 1000 the function gives up and returns null. This is not a correctness limit but a cost limit: the compilation algorithm traces boundaries on a point grid of (width + 1) x (height + 1), and a large image makes the number of points and the traversal time explode. Inside the function there is also POINT_TO_PROCESS_LIMIT: if the boundary has more than 1000 points it also returns null, separating "too complex" from "too large".

The first step expands the bit-packed mask into one byte per pixel:

// decodes bit-packed mask data
const lineSize = (width + 7) & ~7;
const data = new Uint8Array(lineSize * height);
let pos = 0;
for (const elem of img) {
  let mask = 128;
  while (mask > 0) {
    data[pos++] = elem & mask ? 0 : 255;
    mask >>= 1;
  }
}

lineSize aligns every row to 8 bits; each byte is split into 8 pixels starting from the most significant bit, a mask bit of 1 meaning "do not fill" and written as 0, otherwise 255. The expanded data is the only input of the later boundary decisions.

The second step finds the "points of interest" between pixels. The point coordinate system has one more row and one more column than the pixels, and each point gets a flag from the fill state of the four surrounding pixels:

FlagMeaningHandling
1Top-right differs from bottom-rightUsed as an outline start or a turn
2Top-left differs from bottom-leftUsed as an outline start or a turn
4Bottom-left differs from bottom-rightUsed as an outline start or a turn
8Top-left differs from top-rightUsed as an outline start or a turn
5A crossingSpecial turning rule
10The other kind of crossingSpecial turning rule

The source uses a POINT_TYPES lookup table that maps the "combination of the four neighbouring pixels" straight to the point flag, avoiding per-point conditional branches. Then the steps array selects the direction of movement by flag, walks once around the boundary and writes every corner into pathBuf:

const steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]);
const pathBuf = [];
 
// the path shall be painted in [0..1]x[0..1] space
const { a, b, c, d, e, f } = new DOMMatrix()
  .scaleSelf(1 / width, -1 / height)
  .translateSelf(0, -height);
 
// ...
const p0 = p;
let type = points[p];
do {
  const step = steps[type];
  do {
    p += step;
  } while (!points[p]);
 
  const pp = points[p];
  if (pp !== 5 && pp !== 10) {
    // set new direction
    type = pp;
    // delete mark
    points[p] = 0;
  } else {
    // type is 5 or 10, ie, a crossing
    type = pp & ((0x33 * type) >> 4);
    points[p] &= (type >> 2) | (type << 2);
  }
  x = p % width1;
  y = (p / width1) | 0;
  pathBuf.push(DrawOPS.lineTo, a * x + c * y + e, b * x + d * y + f);
 
  if (!points[p]) {
    --count;
  }
} while (p0 !== p);
--i;

The inner do-while keeps moving in the current direction until it reaches a position that has a point; when pp is not a crossing it is taken directly as the new direction and the point is cleared to mark it as visited. On 5 or 10 a simple turn is not possible: a crossing has two possible exits, and the source uses bit operations to choose one according to the incoming direction while rewriting the flag of that point so that a later entry from the other side takes the other edge. count is decremented only when a point is cleared; the loop ends when it returns to the start p0, and --i makes the outer loop rescan the current row to handle the remaining outlines on the same row.

The coordinate transformation is done with DOMMatrix: scaleSelf scales x to 1 / width and y to -1 / height to flip the y axis, and the immediately following translateSelf(0, -height) writes the translation into the already scaled coordinate system. In the end y equal to height maps to 0 and y equal to 0 maps to 1, so pixel coordinates land in the [0, 1] glyph space. This step must agree with the FontMatrix convention of Type3 fonts, otherwise the glyph would be upside down or wrongly scaled.

The return value is exactly the operator arguments:

return [OPS.rawFillPath, [new Float32Array(pathBuf)], new Float32Array([0, 0, width, height])];

The three items are "fill path operation", "path buffer" and "bounding box". The caller in evaluator.js feeds them into OPS.constructPath:

args = compileType3Glyph(imgData);
 
if (args) {
  operatorList.addImageOps(OPS.constructPath, args, optionalContent);
  return;
}
warn('Cannot compile Type3 glyph.');

On failure it does not throw but falls back to OPS.paintImageMaskXObject, drawing with the original bitmap mask. This fallback path guarantees that compilation is only an optimisation, not a precondition for correctness; any glyph whose size or complexity exceeds the limit can still be displayed, just more slowly.

Design note: compileType3Glyph encapsulates both "whether it can be optimised" and "how to optimise it" in one pure function: the input is the mask with its width and height, the output is operator arguments or null. The caller only has to test whether the return value is empty and needs to know nothing about point tracing, crossing turns or coordinate transformation; a failed optimisation does not pollute the rest of the operator list either.

Conclusion

fonts_utils.js is the shared infrastructure of the font layer: it does not parse font programs and does not decide the font type, it only provides the flags, name tables and algorithms every branch needs. fonts.js decides "what font this is", type1_font.js and cff_font.js parse font programs, font_renderer.js generates outlines, and all of them depend on the definitions here.

Where It Sits in the Overall Chain

StageModuleInputOutput
Resource readingevaluator.js/Font, /Encoding, /FlagsFont property object
Format dispatchfonts.jsFont properties, font fileFont subclass
Mapping constructionfonts_utils.jsProperties, built-in encoding, glyph name arrayTable from char code to glyph id
Outline generationtype1_font.js, cff_font.jsFont programGlyph outlines and widths
Drawingfont_renderer.js, evaluatorGlyphs, SEAC_ANALYSIS_ENABLEDPath or bitmap operator

Chained in one sentence: evaluator reads the font properties, fonts.js picks the font branch, fonts_utils.js provides the mapping rules and compilation algorithms shared between branches, and finally font_renderer turns glyph ids into drawable outlines.

Five Design Principles

  1. Only cross-branch shared material lives here: FontFlags, the glyph order and the character mapping are used by Type1, CFF and TrueType alike, so they live in fonts_utils.js; parsing logic unique to one font type stays in its own file.
  2. Tables are built on demand, constants are shared directly: MacStandardGlyphOrdering is a fixed array available as soon as the module loads; getVerticalPresentationForm and getGlyphsUnicode use factories to build lazily, avoiding a full-table initialisation cost just for opening a document.
  3. The mapping direction must be explicit: the tables in encodings.js are "char code to name", while MacStandardGlyphOrdering is "glyph id to name". The same kind of array means completely different things in different directions, so function signatures write the direction into the calling contract with names such as glyphNames.
  4. A failed optimisation must fall back to the original path: compileType3Glyph gives up proactively at size and point limits and returns null, and the caller falls back to bitmap mask drawing. Optimisation only affects speed, never whether something can be displayed.
  5. Platform differences are absorbed at parse time: SEAC_ANALYSIS_ENABLED splits the seac composite into a base glyph and an accent, keeping the renderer differences between Linux and Windows out of the drawing stage; the price is that the parser has to understand glyph composition, the benefit is a unified rendering path.

Once fonts_utils.js is understood, looking again at how font.js generates glyphNames for Type1 and CFF, and how fonts.js combines /Differences and the base encoding into the final mapping, makes it clear that they share one set of character-mapping semantics rather than each assembling a table on the fly. The next article moves on to encodings.js, to see how the "char code to glyph name" base tables themselves are organised.

StreamPDF design principle: anything that can be handled ahead of time is absolutely not deferred to parse time; the server solves all performance problems once. The browser only fetches and renders the data it must.

Related articles