PDF.js Source Walkthrough: standard_fonts
October 27, 2026 · StreamPDF Team

Introduction
The previous articles already laid out the shared foundation of the font layer: fonts_utils.js provides FontFlags, MacStandardGlyphOrdering and normalizeFontName, encodings.js defines several built-in encodings, glyphlist.js gives the glyph-name to Unicode table, and cmap.js translates character codes of composite fonts into CIDs or Unicode. Those modules answer "how to encode", "how to look up a glyph name" and "how to parse a CMap". But one class of question is still unanswered: when a font has no embedded file at all, how should the name be recognised, which substitute font should be chosen, and how should glyph numbers be guessed.
src/core/standard_fonts.js (1092 lines) is the data layer prepared for exactly this class of question. It contains almost no algorithms; its body is nine lookup tables lazily built by factory functions, plus two query functions. It does not parse font files and does not construct Font objects; it only gathers domain knowledge such as "which names belong to standard fonts", "who a given name should degrade to" and "which Unicode a given CID most likely corresponds to".
In one sentence: it is the standard font dictionary of the font layer, handing the result of a normalised font-name lookup to evaluator.js and fonts.js, so that a PDF without embedded files can still choose a usable substitute font and preserve as many glyphs as possible.
Calling it a "dictionary" rather than a "module" is deliberate: a dictionary's job is to collect and retrieve, not to use. What actually decides which font is used and which glyph is drawn is the parser and the font constructor that call it.
Module Overview
standard_fonts.js can be cut into nine blocks, each either an independent table or a very thin query function:
| Block | Key constants or functions | Responsibility |
|---|---|---|
| Standard font table | getStdFontMap | The standard 14 fonts, plus common aliases such as Arial and Times New Roman |
| File name map | getFontNameToFileMap | Mapping from standard font names to built-in font files |
| Non-standard font table | getNonStdFontMap | Degradation targets for non-embedded fonts such as Calibri |
| Serif name set | getSerifFonts | Judging serif tendency from the name, compensating for an untrustworthy /Flags |
| Symbol name set | getSymbolsFonts | Names in the Symbol, Dingbats and Wingdings family |
| Mac-order glyph map | getGlyphMapForMacOrderedFonts | Builds the base CID-to-Unicode table from the standard Macintosh glyph ordering |
| Standard glyph map | getGlyphMapForStandardFonts | Common CID-to-Unicode for fonts such as Ghostscript's, which carry no CID mapping |
| Supplemental glyph maps | the three getSupplementalGlyphMapFor... | Sparse overrides for Trebuchet, ArialBlack and Calibri |
| Queries and exports | getStandardFontName, isKnownFontName | Look up after normalising the name, and answer whether a font name is known |
Looked at by entry count, the division of labour is even clearer:
| Table | Entries | Key type | Value type | Generated? |
|---|---|---|---|---|
getStdFontMap | 72 | Font name string | Standard font name or alias | No |
getFontNameToFileMap | 18 | Standard font name | Font file name | No |
getNonStdFontMap | 50 | Non-standard font name | Substitute font name | No |
getSerifFonts | 134 | Family name | Boolean | No |
getSymbolsFonts | 6 | Family name | Boolean | No |
getGlyphMapForMacOrderedFonts | at most 256 | glyph id number | Unicode code point | Yes |
getGlyphMapForStandardFonts | 496 | CID number | Unicode code point | No |
getSupplementalGlyphMapForTrebuchetMS | 75 | CID number | Unicode code point | No |
getSupplementalGlyphMapForArialBlack | 3 | CID number | Unicode code point | No |
getSupplementalGlyphMapForCalibri | 117 | CID number | Unicode code point | No |
The size difference between the name tables and the glyph tables tells us two things. The keys of the name tables are a limited set of vendor names, so a few dozen to a hundred plus entries cover the mainstream cases; the keys of the glyph tables are slots inside a font and could theoretically run to thousands, so PDF.js records only correspondences verified against real samples. The generated Mac-order table is the only exception: it derives Unicode from 258 standard glyph names, trading a loop for size.
Its place in the whole chain can be drawn like this:
// Font dictionary /BaseFont, /Subtype, /FontDescriptor
// → evaluator.js reads the font dictionary, decides whether to use a system font
// → standard_fonts.js this article: name tables, substitution tables, glyph maps
// → fonts.js the Font family: fallbackToSystemFont, toFontChar
// → font_substitutions.js actually resolves names to system or built-in fonts
// → canvas / text layer draws glyphs, extracts UnicodeVersion note: This article follows exactly the version of
standard_fonts.jsin the current repository. This version exports 12 members, among whichgetGlyphMapForMacOrderedFontsandgetSupplementalGlyphMapForTrebuchetMSare two tables split out specifically around the TrebuchetMS glyph ordering; the Trebuchet branch infonts.jsapplies the Mac-order base map first and then layers the supplemental table on top, and the source comment cites issue 21713. Older material that covers onlygetGlyphMapForStandardFontsmisses this branch. Also, all tables are lazily built bygetLookupTableFactory, but the source does not callObject.freeze; read-only is a module contract, not a runtime guarantee.
Dependency Direction
The file header imports only three things, all pointing at low-level shared modules:
import { MacStandardGlyphOrdering, normalizeFontName } from './fonts_utils.js';
import { getGlyphsUnicode } from './glyphlist.js';
import { getLookupTableFactory } from './core_utils.js';| Dependency | Capability provided | Used in this file at |
|---|---|---|
fonts_utils | MacStandardGlyphOrdering, normalisation | the generation loop of the Mac-order table, both query functions |
glyphlist | getGlyphsUnicode | translating Mac glyph names into Unicode code points |
core_utils | getLookupTableFactory | the lazy-loading wrapper for the nine tables |
It does not import fonts.js, evaluator.js or cmap.js, and it imports no stream or parser. This one-way dependency lets several modules inside the Worker reference it safely without dragging font implementation details into the data layer. Conversely, both evaluator.js and fonts.js import from it, while font_substitutions.js receives an already computed standardFontName as a parameter and does not depend on this file directly.
The Lazy-Loading Factory and Data Shapes
All nine tables are created through getLookupTableFactory. That factory does not live in this file but in core_utils.js, and its implementation is only about ten lines:
function getLookupTableFactory(initializer, useArray = false) {
let lookup;
return function () {
if (initializer) {
lookup = useArray ? [] : Object.create(null);
initializer(lookup);
initializer = null;
}
return lookup;
};
}What it returns is not a table but a table-getter function. On the first call it creates the container, runs the initialiser and then sets initializer to null; every later call returns the same lookup. This is a "construct on first access, reuse forever" singleton, with the same purpose as the shadow target in Catalog, only scoped at module level.
A few details are easy to overlook:
| Detail | Concrete behaviour | Impact |
|---|---|---|
| Container type | Object.create(null) by default, [] only in array mode | The table has no prototype chain, so names such as toString cannot produce false hits |
| Construction time | On the first getter call | Not built when a document is opened, but when a font is actually encountered |
| Construction count | The initialiser runs once and is then nulled | Exactly one table per module instance |
| Mutability | The original object is returned, without wrapper or freeze | Callers could in theory modify the table; in practice they only read |
| Numeric keys | t[2] = 10 ends up as the string key "2" | Both numbers and strings work for lookup, but iteration must convert back |
The choice of Object.create(null) matches NameCache in primitives.js: font names come from PDF content and may be strings such as constructor or __proto__. A plain object literal inherits Object.prototype, so an in check or a direct subscript could return a non-entry; a prototype-less object reduces that risk to zero. Note that the getSerifFonts test in evaluator.js uses the in operator, which is exactly the pattern a prototype-less container can support safely.
The behaviour of numeric keys deserves its own note. getGlyphMapForStandardFonts writes t[2] = 10 and t[2424] = 7821, and JavaScript turns numeric keys into string properties. Later, for (const charCode in glyphMap) inside applyStandardFontGlyphMap yields strings, so downstream code must write map[+charCode] = glyphMap[charCode], using unary plus to convert back to a numeric index. This is not an incidental conversion but the necessary bridge between an object table and an array table.
Table Lifetime
These tables have nothing to do with any document. They register their initialisers when the module is first evaluated, are constructed on the first lookup, and then stay in the Worker's module scope. Opening a second PDF does not rebuild them, and closing the first PDF does not release them. This contrasts with fontCache and standardFontDataCache in Catalog:
| Cache or table | Scope | Constructed when | Cleared when |
|---|---|---|---|
The nine tables such as getStdFontMap | Module level | First getter call | Never; lives as long as the Worker |
Catalog.fontCache | Document level | First font encountered | Document cleanup |
Catalog.standardFontDataCache | Document level | First font file fetched | Document cleanup |
The system cache of getFontSubstitution | Document level | First need for a system substitute | Document cleanup |
The distinction is reasonable: the nine tables are immutable domain knowledge, do not change with the document and hold no large binary blob; font objects and fetched file bytes are document-level resources and must be reclaimed per document. Making domain knowledge a module-level singleton avoids rebuilding the entire mapping set for every document opened.
The Tables Are Not Frozen
getLookupTableFactory returns the original initialised object, with no Object.freeze and no Object.seal. In other words, any caller that obtains a table can write new keys into it. The source relies on two layers of constraint to keep it read-only:
- The initialiser is nulled once it has run, so the factory never reconstructs the table and never rewrites it after construction;
- None of the table constants is exported directly; outsiders can only obtain a reference through a getter, and the module exposes no write interface.
This is contractual read-only, not runtime read-only. The benefit is an extremely short construction path with no freezing cost; the price is that an accidental write is not caught immediately. For a 496-entry table such as getGlyphMapForStandardFonts, freezing once means walking every key, while the table is read by only a few paths inside the Worker, so PDF.js chose the former.
Design note: The lazy-loading factory separates data definition from data instantiation. Loading the module only registers initialisers and allocates no memory for the nine tables; a table is constructed only when it is actually needed. Most documents use only two or three of them, so building on demand avoids meaningless allocation at startup and keeps unused tables out of the heap forever.
The Standard 14 Fonts and the Alias Table
getStdFontMap is the most central table in the whole file. It first lists the standard 14 fonts from the PDF specification, then appends a large set of commercial font aliases:
const getStdFontMap = getLookupTableFactory(function (t) {
// The standard 14 fonts:
t["Times-Roman"] = "Times-Roman";
t.Helvetica = "Helvetica";
t.Courier = "Courier";
t.Symbol = "Symbol";
t["Times-Bold"] = "Times-Bold";
t["Helvetica-Bold"] = "Helvetica-Bold";
t["Courier-Bold"] = "Courier-Bold";
t.ZapfDingbats = "ZapfDingbats";
t["Times-Italic"] = "Times-Italic";
t["Helvetica-Oblique"] = "Helvetica-Oblique";
t["Courier-Oblique"] = "Courier-Oblique";
t["Times-BoldItalic"] = "Times-BoldItalic";
t["Helvetica-BoldOblique"] = "Helvetica-BoldOblique";
t["Courier-BoldOblique"] = "Courier-BoldOblique";The standard 14 fonts are the set that a viewer must support without an embedded file according to the PDF specification: three families, Times, Helvetica and Courier, with four styles each, plus the two symbol fonts Symbol and ZapfDingbats, 14 in total. Most values in the table equal their keys, which is not redundant: it gives standard font lookup a uniform return shape, so a hit always yields a usable standard name.
| Family | Regular | Bold | Italic | Bold italic |
|---|---|---|---|---|
| Times | Times-Roman | Times-Bold | Times-Italic | Times-BoldItalic |
| Helvetica | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| Courier | Courier | Courier-Bold | Courier-Oblique | Courier-BoldOblique |
| Symbol | Symbol | none | none | none |
| Symbol | ZapfDingbats | none | none | none |
The naming is not fully symmetric: Helvetica and Courier use Oblique for italic while Times uses Italic; Courier's bold italic is called BoldOblique, yet among the Arial aliases both BoldItalic and BoldOblique appear. These asymmetries are not typos but the historical naming of different font vendors, and the table must register real names rather than force them into one form.
The real value lies in the "Extra mappings" that follow. The table has 72 entries in total, and apart from the 14 standard names, everything else normalises aliases:
// Extra mappings
t.ArialNarrow = 'Helvetica';
t['ArialNarrow-Bold'] = 'Helvetica-Bold';
t['ArialNarrow-BoldItalic'] = 'Helvetica-BoldOblique';
t['ArialNarrow-Italic'] = 'Helvetica-Oblique';
t.ArialBlack = 'Helvetica';
t['ArialBlack-Bold'] = 'Helvetica-Bold';
t['ArialBlack-BoldItalic'] = 'Helvetica-BoldOblique';
t['ArialBlack-Italic'] = 'Helvetica-Oblique';
t['Arial-Black'] = 'Helvetica';
t['Arial-Black-Bold'] = 'Helvetica-Bold';
t['Arial-Black-BoldItalic'] = 'Helvetica-BoldOblique';
t['Arial-Black-Italic'] = 'Helvetica-Oblique';
t.Arial = 'Helvetica';
t['Arial-Bold'] = 'Helvetica-Bold';
t['Arial-BoldItalic'] = 'Helvetica-BoldOblique';
t['Arial-Italic'] = 'Helvetica-Oblique';
t.ArialMT = 'Helvetica';
t['Arial-BoldItalicMT'] = 'Helvetica-BoldOblique';
t['Arial-BoldMT'] = 'Helvetica-Bold';
t['Arial-ItalicMT'] = 'Helvetica-Oblique';These aliases are not a simple claim that "Arial equals Helvetica". They handle four kinds of difference at once:
| Kind of difference | Example | Handling |
|---|---|---|
| Different family name | Arial, ArialMT, ArialUnicodeMS | All land on Helvetica |
| Different weight suffix | Bold, BoldMT, Black | All land on Helvetica-Bold |
| Different italic naming | Italic, Oblique, BoldItalic | All land on the Helvetica-Oblique series |
| Hyphenation difference | Arial-Black and ArialBlack | Both spellings are registered |
These aliases cover common commercial fonts such as Arial, ArialNarrow, ArialBlack, ArialUnicodeMS, CourierNew, CourierNewPS, TimesNewRoman, TimesNewRomanPS and HelveticaLTStd. What they share is that the PDF names them but embeds no glyph data, so the viewer has to draw them as some standard font.
| Original family | Regular target | Bold target | Italic target | Bold italic target |
|---|---|---|---|---|
| Arial | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| ArialNarrow | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| ArialBlack | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| ArialUnicodeMS | Helvetica | Helvetica-Bold | Helvetica-Oblique | Helvetica-BoldOblique |
| TimesNewRoman | Times-Roman | Times-Bold | Times-Italic | Times-BoldItalic |
| TimesNewRomanPS | Times-Roman | Times-Bold | Times-Italic | Times-BoldItalic |
| CourierNew | Courier | Courier-Bold | Courier-Oblique | Courier-BoldOblique |
| CourierNewPS | Courier | Courier-Bold | Courier-Oblique | Courier-BoldOblique |
| HelveticaLTStd | Helvetica | Helvetica-Bold | none | none |
This table also explains why getStdFontMap has 72 entries instead of 14: the same family can appear in a PDF with suffixes such as MT, PS, PSMT, UnicodeMS, Narrow and Black, and each must be registered separately. Miss one spelling and the font falls back to its original name, obtaining neither a built-in file nor standard metrics.
The Symbol family gets special treatment: Symbol-Bold, Symbol-BoldItalic and Symbol-Italic all map back to Symbol. Symbol fonts have no real weight or style variation, and preserving style names would only leave downstream code unable to find the matching file.
The Alias Table Is Not Exhaustive
This table is accumulated from PDFs that were actually encountered, not enumerated from a vendor catalogue. Of the 72 entries, 14 are standard fonts and 58 are aliases; the aliases concentrate in four families: Arial, Times New Roman, Courier New and Helvetica. It covers the names most common in desktop publishing and Office exports, and makes no claim to cover every commercial font.
| Group | Entries | Examples |
|---|---|---|
| Standard 14 | 14 | Times-Roman, Helvetica, Symbol |
| Arial family aliases | 27 | Arial, ArialMT, ArialUnicodeMS |
| Times family aliases | 15 | TimesNewRoman, TimesNewRomanPSMT |
| Courier family aliases | 10 | CourierNew, CourierNewPSMT |
| Helvetica family aliases | 3 | Helvetica-Italic, HelveticaLTStd-Bold |
| Symbol style aliases | 3 | Symbol-Bold, Symbol-Italic |
When a name is not in the table, getStandardFontName returns undefined and the font goes to system substitution or the default fallback. This is not a defect but a deliberate boundary: guessing a substitute for every font name would make mistakes harder to find; registering only verified names means an unknown font at least keeps its original name and is handed to the platform's font matching.
normalizeFontName performs one clean-up pass before the lookup:
function normalizeFontName(name) {
return name.replaceAll(/[,_]/g, '-').replaceAll(/\s/g, '');
}Commas and underscores both become hyphens, and all whitespace is removed. So Times New Roman normalises to TimesNewRoman, which hits a table entry exactly; Times,New_Roman normalises to Times-New-Roman, which is not the same key, and the table has no such entry. Note that it does not lower-case anything, so table keys must match the actual casing in the PDF. That is also why ArialMT, Arial-BoldMT and TimesNewRomanPSMT-Bold all appear: normalisation can flatten whitespace and two separators, but it cannot flatten vendor naming habits.
Design note:
getStdFontMapcarries two meanings at once. For the standard 14 fonts it says "the specification guarantees this font is available"; for the Arial family it says "this is not part of the standard 14, but its glyphs and metrics can be borrowed from Helvetica". Sharing one table gives a single lookup entry point; the cost is thatisStandardFontalso counts Arial as standard, so downstream must use extra tests such asisNarrowandisMappedToStandardFontto correct the width recomputation.
Standard Font File Name Mapping
getFontNameToFileMap has only 18 entries and maps standard font names to the font files shipped with PDF.js:
// Contains mapping for standard fonts and xfa fonts.
const getFontNameToFileMap = getLookupTableFactory(function (t) {
t.Courier = 'FoxitFixed.pfb';
t['Courier-Bold'] = 'FoxitFixedBold.pfb';
t['Courier-BoldOblique'] = 'FoxitFixedBoldItalic.pfb';
t['Courier-Oblique'] = 'FoxitFixedItalic.pfb';
t.Helvetica = 'LiberationSans-Regular.ttf';
t['Helvetica-Bold'] = 'LiberationSans-Bold.ttf';
t['Helvetica-BoldOblique'] = 'LiberationSans-BoldItalic.ttf';
t['Helvetica-Oblique'] = 'LiberationSans-Italic.ttf';
t['Times-Roman'] = 'FoxitSerif.pfb';
t['Times-Bold'] = 'FoxitSerifBold.pfb';
t['Times-BoldItalic'] = 'FoxitSerifBoldItalic.pfb';
t['Times-Italic'] = 'FoxitSerifItalic.pfb';
t.Symbol = 'FoxitSymbol.pfb';
t.ZapfDingbats = 'FoxitDingbats.pfb';
t['LiberationSans-Regular'] = 'LiberationSans-Regular.ttf';
t['LiberationSans-Bold'] = 'LiberationSans-Bold.ttf';
t['LiberationSans-Italic'] = 'LiberationSans-Italic.ttf';
t['LiberationSans-BoldItalic'] = 'LiberationSans-BoldItalic.ttf';
});This table has two obvious characteristics. First, Courier, Times, Symbol and ZapfDingbats use Foxit .pfb files while the Helvetica family uses Liberation Sans .ttf files. Second, four LiberationSans-* names are registered at the end, corresponding to font names that XFA forms may write directly. The comment "standard fonts and xfa fonts" refers to exactly this.
Its consumer is fetchStandardFontData in evaluator.js:
const standardFontNameToFileName = getFontNameToFileMap(),
filename = standardFontNameToFileName[name];
let data;
try {
if (this.options.useWorkerFetch) {
data = await fetchBinaryData(
`${this.options.standardFontDataUrl}${filename}`
);
} else {Upstream reaches this path only when useSystemFonts is off and the name is neither Symbol nor ZapfDingbats. In other words, the table serves the path that renders standard fonts with its own font files; with system fonts enabled, names go to font_substitutions.js to be matched against local fonts, and no built-in file is needed. The file name is not a path: the actual directory comes from standardFontDataUrl, and the Worker only concatenates the full URL and fetches the bytes.
| Entry source | Example | File type | Typical use |
|---|---|---|---|
| Serif family of the standard 14 | Times-Roman | .pfb | built-in Type1 outlines |
| Sans-serif of the standard 14 | Helvetica | .ttf | built-in TrueType outlines |
| Symbol fonts | Symbol, ZapfDingbats | .pfb | special character sets |
| XFA fallback names | LiberationSans-Regular | .ttf | direct hits from XFA font names |
Fetching and Caching
The full entry point of fetchStandardFontData shows the table's role in the chain more clearly:
async fetchStandardFontData(name) {
const cachedData = this.standardFontDataCache.get(name);
if (cachedData) {
return new Stream(cachedData);
}
if (
this.options.useSystemFonts &&
name !== "Symbol" &&
name !== "ZapfDingbats"
) {
return null;
}
const standardFontNameToFileName = getFontNameToFileMap(),
filename = standardFontNameToFileName[name];
let data;The cache key is the standard font name, not the file name. The same Helvetica referenced by many pages is fetched only once; the bytes are then stored in Catalog.standardFontDataCache and released together when the document is cleaned up. When name is not in the table, filename is undefined, the request fails and returns null, and the font then falls through to a system font or the default substitute path.
There is also an easily missed condition: with useSystemFonts enabled, only Symbol and ZapfDingbats still use built-in files. The reason is that system fonts usually lack these two symbol fonts, and their glyphs cannot be replaced by an ordinary text font; the remaining standard fonts go to system font matching and save a network request.
| Condition | Fetch the built-in file? | Reason |
|---|---|---|
useSystemFonts is false | yes | no system font is available, so the built-in file is required |
useSystemFonts is true, name is Symbol | yes | system fonts usually have no symbol glyphs |
useSystemFonts is true, name is ZapfDingbats | yes | same as above |
useSystemFonts is true, other standard names | no | handed to font_substitutions.js to match a local font |
The Four LiberationSans Names for XFA
The four LiberationSans-* entries at the end of the table are not standard 14 fonts but fallback names for XFA forms:
t['LiberationSans-Regular'] = 'LiberationSans-Regular.ttf';
t['LiberationSans-Bold'] = 'LiberationSans-Bold.ttf';
t['LiberationSans-Italic'] = 'LiberationSans-Italic.ttf';
t['LiberationSans-BoldItalic'] = 'LiberationSans-BoldItalic.ttf';XFA forms are described in XML, and a font name may be written directly as LiberationSans-Regular instead of a PDF /BaseFont name. xfa_fonts.js brings such names in when constructing the font dictionary, and fetchStandardFontData then uses this table to fetch the corresponding file. Without these four entries, the default sans-serif font in an XFA form could only fall back to a system font or the default fallback, and the layout would drift from the design.
These four names point at the same .ttf files as Helvetica, but the keys must be registered separately because getFontNameToFileMap matches exactly and performs no alias inference. The Helvetica substitution lives in getStdFontMap; the file map only handles the name-to-file layer, not the name-to-name layer.
Non-Standard Fonts and the Substitution Chain
getNonStdFontMap holds 50 entries and handles names that are not part of the standard 14 yet are frequently written in PDFs as if they were standard fonts:
/**
* Holds the map of the non-standard fonts that might be included as
* a standard fonts without glyph data.
*/
const getNonStdFontMap = getLookupTableFactory(function (t) {
t.Calibri = "Helvetica";
t["Calibri-Bold"] = "Helvetica-Bold";
t["Calibri-BoldItalic"] = "Helvetica-BoldOblique";
t["Calibri-Italic"] = "Helvetica-Oblique";
t.CenturyGothic = "Helvetica";
t["CenturyGothic-Bold"] = "Helvetica-Bold";
t["CenturyGothic-BoldItalic"] = "Helvetica-BoldOblique";
t["CenturyGothic-Italic"] = "Helvetica-Oblique";
t.ComicSansMS = "Comic Sans MS";
t["ComicSansMS-Bold"] = "Comic Sans MS-Bold";
t["ComicSansMS-BoldItalic"] = "Comic Sans MS-BoldItalic";
t["ComicSansMS-Italic"] = "Comic Sans MS-Italic";
t.GillSansMT = "Helvetica";
t["GillSansMT-Bold"] = "Helvetica-Bold";
t["GillSansMT-BoldItalic"] = "Helvetica-BoldOblique";
t["GillSansMT-Italic"] = "Helvetica-Oblique";
t.Impact = "Helvetica";The difference from getStdFontMap is not one of form but of whether the result is still a standard font. Calibri, CenturyGothic, GillSansMT, Impact and TrebuchetMS all degrade to the Helvetica family; ComicSansMS degrades to the Comic Sans MS family with spaces; LucidaConsole degrades to the Courier family; MS Gothic, MS Mincho, MS PGothic and MS PMincho degrade to Japanese font families of the same name. The latter two groups do not necessarily hit getStdFontMap, so downstream treats them as "mapped to a standard font" rather than "being a standard font".
| Original family | Substitute target | Target is standard? | Note |
|---|---|---|---|
| Calibri | Helvetica family | yes | Office default font, often referenced without embedding |
| CenturyGothic | Helvetica family | yes | geometric sans, shapes close to Helvetica |
| GillSansMT | Helvetica family | yes | British sans, Helvetica as the safety net |
| Impact | Helvetica | yes | regular weight only, no italic or bold variants |
| TrebuchetMS | Helvetica family | yes | only a name replacement here; glyph mapping has its own branch |
| ComicSansMS | Comic Sans MS family | no | keeps the original name, left to system font matching |
| LucidaConsole | Courier family | yes | monospaced console font, lands on Courier |
| MS Gothic series | same-named families such as MS Gothic | no | Japanese Windows fonts, family name kept |
| MS Mincho series | same-named families such as MS Mincho | no | same as above |
| NuptialScript | Times-Italic | yes | a script face cannot be substituted, so it degrades to italic serif |
Of the 50 entries, the MS Gothic and MS Mincho series account for 16, the largest group; the four four-style families Calibri, CenturyGothic, GillSansMT and TrebuchetMS account for 16; ItcSymbol has 6, LucidaConsole 4 and ComicSansMS 4; the rest are single entries such as Impact, LucidaSans-Demi, NuptialScript and SegoeUISymbol. Registering all four weights keeps the weight of the substitute as well, instead of drawing bold body text as regular.
fallbackToSystemFont in fonts.js writes this chain very plainly:
const { name, type } = this;
let fontName = normalizeFontName(name);
const stdFontMap = getStdFontMap(),
nonStdFontMap = getNonStdFontMap();
const isStandardFont = !!stdFontMap[fontName];
const isMappedToStandardFont = !!(nonStdFontMap[fontName] && stdFontMap[nonStdFontMap[fontName]]);
fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;The lookup order is standard table first, non-standard table second, and finally the original name. isStandardFont looks only at the standard table; isMappedToStandardFont requires that the non-standard result itself hits the standard table once more. This double condition is necessary: Calibri maps to Helvetica, which is in the standard table, so it is true; ComicSansMS maps to Comic Sans MS, which is not in the standard table, so it is false. The two take different paths for width recomputation and for constructing toFontChar.
| Name | Standard table hit | Non-standard result | Name actually used | Treated as standard |
|---|---|---|---|---|
Helvetica | yes | none | Helvetica | yes |
Arial | yes | none | Helvetica | yes |
Calibri | no | Helvetica | Helvetica | yes (indirectly) |
ComicSansMS | no | Comic Sans MS | Comic Sans MS | no |
MS-Gothic | no | MS Gothic | MS Gothic | no |
SomeUnknownFont | no | none | SomeUnknownFont | no |
Design note: The substitution chain splits recognition from replacement.
getStdFontMapanswers "is this name a standard font" andgetNonStdFontMapanswers "what should this name become". With the two separated,isMappedToStandardFontcan return true only for substitutions that really land on a standard font, instead of treating Comic Sans MS as standard and applying metric corrections to it.
The Serif Name Set and the Symbol Name Set
The two boolean set tables do not substitute anything; they only tag fonts.
The Serif Name Set
getSerifFonts has 134 entries, all mapped to true:
const getSerifFonts = getLookupTableFactory(function (t) {
t["Adobe Jenson"] = true;
t["Adobe Text"] = true;
t.Albertus = true;
t.Aldus = true;
t.Alexandria = true;
t.Algerian = true;
t["American Typewriter"] = true;
t.Antiqua = true;
t.Apex = true;
t.Arno = true;
t.Aster = true;
t.Aurora = true;
t.Baskerville = true;
t.Bell = true;
t.Bembo = true;
t["Bembo Schoolbook"] = true;
t.Benguiat = true;
t["Berkeley Old Style"] = true;
t["Bernhard Modern"] = true;
t["Berthold City"] = true;
t.Bodoni = true;
t["Bauer Bodoni"] = true;
t["Book Antiqua"] = true;
t.Bookman = true;
t["Bordeaux Roman"] = true;
t["Californian FB"] = true;
t.Calisto = true;
t.Calvert = true;
t.Capitals = true;
t.Cambria = true;
t.Cartier = true;
t.Caslon = true;The middle and the end are just as dense:
t.Garamond = true;
t.Gentium = true;
t.Georgia = true;
t.Gloucester = true;
t['Goudy Old Style'] = true;
t['Goudy Schoolbook'] = true;
t['Goudy Pro Font'] = true;
t.Granjon = true;
t['Guardian Egyptian'] = true;
t.Heather = true;
t.Hercules = true;
t['High Tower Text'] = true;
t.Hiroshige = true;
t['Hoefler Text'] = true;
t['Humana Serif'] = true;
t.Imprint = true;
t['Ionic No. 5'] = true;
t.Janson = true;
t.Joanna = true;
t.Korinna = true;
t.Lexicon = true;
t.LiberationSerif = true;
t['Liberation Serif'] = true;
t['Linux Libertine'] = true;
t.Literaturnaya = true;
t.Lucida = true;
t['Lucida Bright'] = true;
t.Melior = true;The table contains traditional serifs such as Garamond, Baskerville, Bodoni and Caslon, modern or free fonts such as Cambria, Constantia, Georgia, Times, LiberationSerif and FreeSerif, and towards the end technical typesetting fonts such as Bitstream Vera, Vera Serif and XITS. Its unit of judgement is the family name with the style suffix removed, not the full font name.
| Category | Examples | Characteristic |
|---|---|---|
| Old-style serif | Garamond, Caslon, Janson, Bembo | low stroke contrast, oblique axis, traditional book setting |
| Transitional and modern | Baskerville, Bodoni, Bauer Bodoni | high stroke contrast, vertical axis, common in headlines |
| Egyptian | Rockwell, Guardian Egyptian, Clarendon | heavy serifs, close to slab serif |
| Modern system fonts | Georgia, Cambria, Constantia | optimised for screen reading, common on Windows |
| Free fonts | LiberationSerif, FreeSerif, Gentium | open licences, often used as Times substitutes |
| Technical typesetting | Computer Modern, STIX, XITS | mathematics and academic typesetting |
Because the set stores only family names, full names such as Times-BoldItalic and TimesNewRomanPSMT never hit directly; they must first pass through normalisation and the substitution tables, and only then have the style suffix split off. This is also why the loop in fonts.js is longer than isSerifFont in evaluator.js: it has to handle subset prefixes, vendor aliases and style suffixes all at once.
isSerifFont in evaluator.js shows the usage:
isSerifFont(baseFontName) {
// Simulating descriptor flags attribute
const fontNameWoStyle = baseFontName.split("-", 1)[0];
return fontNameWoStyle in getSerifFonts() || /serif/i.test(fontNameWoStyle);
}split("-", 1)[0] takes only the first segment before the hyphen, so Times-BoldItalic becomes Times; the regular expression afterwards catches names containing serif. The test in fonts.js is more lenient: it first splits off the subset prefix by +, then normalises, then passes through the standard and non-standard tables, and only at the end splits and queries the set:
let isSerifFont = !!(properties.flags & FontFlags.Serif);
// Fallback to checking the font name, in order to improve text-selection,
// since the /Flags-entry is often wrong (fixes issue13845.pdf).
if (!isSerifFont && !properties.isSimulatedFlags) {
const stdFontMap = getStdFontMap(),
nonStdFontMap = getNonStdFontMap(),
serifFonts = getSerifFonts();
for (const namePart of name.split('+')) {
let fontName = normalizeFontName(namePart);
fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;
fontName = fontName.split('-', 1)[0];
if (serifFonts[fontName]) {
isSerifFont = true;
break;
}
}
}The comment in this code explains the motivation: the Serif bit in /Flags is often wrong, and looking only at the flag would make the text layer choose the wrong family, affecting both selection and copying. A name lookup is a very cheap second opinion, executed only when the flag does not declare a serif.
| Decision path | Input | Basis | Use of the result |
|---|---|---|---|
The Serif bit in /Flags | font descriptor | the bit written by the generator | primary decision |
getSerifFonts name lookup | family name without style | the 134-entry set in this file | correcting a wrong flag |
serif regular expression | family name | whether the name contains serif | last-resort recognition |
getBaseFontMetrics fallback | no metric data | Times if serif, otherwise Helvetica | choosing the default width table |
The Symbol Name Set
getSymbolsFonts has only 6 entries, yet covers the most common symbol font families in PDFs:
const getSymbolsFonts = getLookupTableFactory(function (t) {
t.Dingbats = true;
t.Symbol = true;
t.ZapfDingbats = true;
t.Wingdings = true;
t['Wingdings-Bold'] = true;
t['Wingdings-Regular'] = true;
});It has two usage sites. The first is in evaluator.js when constructing a simulated font descriptor, where getSymbolsFonts()[fontNameWoStyle] decides between FontFlags.Symbolic and FontFlags.Nonsymbolic:
const flags =
(this.isSerifFont(fontNameWoStyle) ? FontFlags.Serif : 0) |
(metrics.monospace ? FontFlags.FixedPitch : 0) |
(getSymbolsFonts()[fontNameWoStyle] ? FontFlags.Symbolic : FontFlags.Nonsymbolic);The second is the glyph-mapping split in fonts.js: when the name matches Symbol or Dingbats, ordinary encodings are skipped in favour of the dedicated symbol encoding tables:
} else if (/Symbol/i.test(fontName)) {
const isCidKeyed =
this.composite && this.cidEncoding.startsWith("Identity-");
this.toFontChar = buildToFontChar(
isCidKeyed ? getSymbolGlyphIdEncoding() : SymbolSetEncoding,
getGlyphsUnicode(),
this.differences
);
} else if (/Dingbats/i.test(fontName)) {
this.toFontChar = buildToFontChar(
ZapfDingbatsEncoding,
getDingbatsGlyphsUnicode(),
this.differences
);
}The regular expressions here match the substituted fontName, not the original name. Symbol-Bold is first normalised to Symbol by the standard table and then enters this branch; Wingdings is not in the regular expression, but because it is in the symbol set it receives the Symbolic flag and later follows the system font or symbol path. The set table and the regular expressions each cover one part and do not overlap.
Note that the set does not contain names such as Wingdings 2 or Webdings. Its role is a hint of known symbol fonts, not an exhaustive list of symbol fonts; a symbol font that misses still gets recognised through the Symbolic bit in /Flags and through /Encoding, only without the name-based fallback.
Design note: The boolean set tables isolate name-to-category knowledge and take no part in substitution. Substitution tables change the font name; category tables only answer yes or no. Mixing the two would make it hard to distinguish
Calibri(which becomes sans-serif after substitution) fromTimes(which is serif to begin with), and later metric and text-layer decisions would lose a stable input.
Glyph Mapping: Mac Ordering and Standard Fonts
When a font has no embedded file, the character codes in the PDF are often just CIDs or glyph ids, without a trustworthy CIDToGIDMap. PDF.js then has to guess from an empirical table which Unicode glyph a given number probably corresponds to. This file provides two base maps and three supplemental maps.
The Mac Standard Glyph Ordering Base Map
getGlyphMapForMacOrderedFonts is not a handwritten table; it is generated from MacStandardGlyphOrdering:
// Base glyph map for fonts whose glyph ids follow the standard Macintosh
// ordering implied by a `post` table of format 1. Fonts such as TrebuchetMS can
// use part of this ordering with supplemental overrides.
const getGlyphMapForMacOrderedFonts = getLookupTableFactory(function (t) {
const glyphsUnicode = getGlyphsUnicode();
t[2] = 10;
for (let gid = 3; gid < MacStandardGlyphOrdering.length; gid++) {
const unicode = glyphsUnicode[MacStandardGlyphOrdering[gid]];
if (unicode !== undefined) {
t[gid] = unicode;
}
}
});MacStandardGlyphOrdering is a list of 258 standard Mac glyph names ordered by glyph id. The first few entries are:
const MacStandardGlyphOrdering = [
".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl",
"numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft",
"parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash",
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
"nine", "colon", "semicolon", "less", "equal", "greater", "question", "at",Entries 0 to 2 are .notdef, .null and nonmarkingreturn. The loop starts at 3 because the first three have no usable Unicode; t[2] = 10 maps nonmarkingreturn alone to a line feed, a historical convention that keeps the text layer from treating a carriage-return slot as a missing glyph. space is at gid 3, corresponding to Unicode 32; exclam at gid 4 corresponds to 33; after that the order basically follows ASCII up to asciitilde at gid 97.
The handling of the first three entries is worth remembering separately: .notdef is the placeholder for a missing glyph, .null is an empty glyph, and nonmarkingreturn is a carriage return that leaves no ink. None of them should appear in visible text, but nonmarkingreturn needs to preserve line-break semantics, so the source gives it code point 10 instead of skipping it like the first two. Skipping it as well would make some fonts generated in Mac order lose paragraph breaks during text extraction.
The loop writes at most 255 entries, plus the separate t[2], for no more than 256 in total; the real number depends on whether glyphsUnicode contains the corresponding names. It produces a complete base map, suited to fonts whose glyph ids follow a format 1 post table on the Mac. fonts.js uses it only when the name matches Trebuchet, because most non-embedded standard fonts do not follow this ordering, and applying it blindly would map characters to the wrong glyphs.
The Standard Font Glyph Map
getGlyphMapForStandardFonts is the longest table in the file, with 496 entries whose keys are CIDs and values are Unicode:
// Glyph map for well-known standard fonts. Sometimes Ghostscript uses CID
// fonts, but does not embed the CID to GID mapping. The mapping is incomplete
// for all glyphs, but common for some set of the standard fonts.
const getGlyphMapForStandardFonts = getLookupTableFactory(function (t) {
t[2] = 10;
t[3] = 32;
t[4] = 33;
t[5] = 34;
t[6] = 35;
t[7] = 36;
t[8] = 37;
t[9] = 38;
t[10] = 39;
t[11] = 40;
t[12] = 41;
t[13] = 42;
t[14] = 43;
t[15] = 44;
t[16] = 45;
t[17] = 46;
t[18] = 47;
t[19] = 48;
t[20] = 49;
t[21] = 50;
t[22] = 51;
t[23] = 52;
t[24] = 53;
t[25] = 54;
t[26] = 55;
t[27] = 56;
t[28] = 57;
t[29] = 58;
t[30] = 894;
t[31] = 60;The first 30 entries are almost a continuous ASCII range: CID 3 to 29 correspond to space through colon, and CID 30 jumps to 894, the "therefore" sign, after which 31 to 97 continue from the less-than sign to the tilde. The gap shows that this is not a table laid out directly by character code but one compiled from the actual glyph slots of some CID font.
The middle covers Latin-1 supplement characters and common punctuation, while the later part reaches into a broader character set:
t[130] = 8224;
t[131] = 176;
t[132] = 162;
t[133] = 163;
t[134] = 167;
t[135] = 8226;
t[136] = 182;
t[137] = 223;
t[138] = 174;
t[139] = 169;
t[140] = 8482;
t[141] = 180;
t[142] = 168;
t[143] = 8800;
t[144] = 198;
t[145] = 216;
t[146] = 8734;These values are Unicode code points: for instance 8224 is a dagger, 8226 a bullet, 8482 a trade mark sign, 8800 not equal to, and 8734 infinity. The table also contains ranges for Greek, Cyrillic, Hebrew and Arabic:
t[493] = 1039;
t[494] = 1040;
t[570] = 1040;
t[571] = 1041;
t[672] = 1488;
t[673] = 1489;
t[759] = 1617;
t[761] = 1776;
t[2424] = 7821;
t[2425] = 7822;
t[3282] = 11799;
t[3379] = 42785;
t[3416] = 8377;t[570] through t[633] is the Cyrillic uppercase range, t[672] onward is Hebrew, t[759] and t[761] sit in Arabic characters, and t[2424] lies in a much later supplemental range. The keys are not continuous; the end even contains a high CID such as 3416, which shows the table was accumulated from real font samples rather than generated completely from the specification.
| CID range | Typical Unicode content | Note |
|---|---|---|
| 2 to 97 | control characters, basic Latin, ASCII punctuation | the most used segment, covering space through tilde |
| 98 to 171 | Latin-1 supplement and common punctuation | includes accented letters, currency, copyright and trade mark |
| 179 to 229 | quotes, dashes, mathematical symbols | curly quotes, ellipsis, not equal to, infinity |
| 305 to 337 | Greek letters and box drawing | includes sum, product, integral and box drawing |
| 493 to 633 | Cyrillic | uppercase and lowercase ranges |
| 672 to 711 | Hebrew | consecutive Alef through Tav |
| 759 to 805 | Arabic diacritics and presentation forms | includes diacritics such as shadda and ligature forms |
| 2424 to 3416 | Latin Extended, mathematical and currency supplements | high CIDs seen in real samples, sparsely distributed |
This distribution table shows it is not a linear mapping of the form "CID minus some constant equals Unicode" but a series of separate blocks with gaps between them. PDF.js made no attempt to fill the gaps, because the glyphs in the gaps have no reliable evidence in real samples; a single wrong key would make the text layer emit wrong characters, which is harder to diagnose than a missing one.
The Layering Order of the Four Mapping Layers
What fallbackToSystemFont in fonts.js finally constructs is not one table but the result of four layers stacked together:
| Layer | Source | Effect | Overrides the previous layer |
|---|---|---|---|
| 1 | getGlyphMapForStandardFonts or the Mac-order base map | gives the initial CID-to-Unicode guess | initial layer |
| 2 | the Trebuchet, ArialBlack and Calibri supplemental tables | corrects known misaligned keys | yes |
| 3 | /CIDToGIDMap | corrects indices with the font's own mapping | yes |
| 4 | /ToUnicode | corrects with the Unicode mapping carried by the document | yes |
The third layer appears directly in the code:
if (cidToGidMap) {
for (const charCode in map) {
const cid = map[charCode];
if (cidToGidMap[cid] !== undefined) {
map[+charCode] = cidToGidMap[cid];
}
}
}It iterates the map that already has the base and supplemental tables applied, and translates the value for each CID once more through cidToGidMap. The fourth layer writes the charCode to unicodeCharCode mapping directly through toUnicode.forEach. The more layers, the closer the result is to the real font; but only the first two layers come from this file, and the last two come from the PDF itself.
Design note: This table is deliberately kept incomplete. A complete mapping would require finding the right glyph for every possible CID, while real PDFs use only a small part of them; collecting the known sample correspondences into a sparse table covers common fonts while keeping the size under five hundred entries.
fallbackToSystemFontthen overrides the table withcidToGidMapandToUnicode, which shows that it is only a fallback starting point, not the final truth.
How Downstream Turns It Into an Array
applyStandardFontGlyphMap in fonts.js flattens the object table into an array:
function applyStandardFontGlyphMap(map, glyphMap) {
for (const charCode in glyphMap) {
map[+charCode] = glyphMap[charCode];
}
}map is an array indexed by CID, and glyphMap is an object with string keys. +charCode turns "2424" back into 2424 and writes it at the matching index. When the function is called several times, later writes overwrite earlier ones, which is exactly the mechanism that lets supplemental tables correct the base table.
Inside fallbackToSystemFont, the way base and supplemental tables combine is split by font name:
if (/Trebuchet/i.test(name)) {
applyStandardFontGlyphMap(map, getGlyphMapForMacOrderedFonts());
applyStandardFontGlyphMap(map, getSupplementalGlyphMapForTrebuchetMS());
} else {
applyStandardFontGlyphMap(map, getGlyphMapForStandardFonts());
if (/Arial-?Black/i.test(name)) {
applyStandardFontGlyphMap(map, getSupplementalGlyphMapForArialBlack());
} else if (/Calibri/i.test(name)) {
applyStandardFontGlyphMap(map, getSupplementalGlyphMapForCalibri());
}
}Trebuchet takes a completely different base table, while ArialBlack and Calibri patch the standard base table. The order of this split cannot be reversed: lay the large table first and stack the small table on top, and only then does the small table have a corrective effect; putting the supplemental table first would let the base table overwrite the same keys.
Supplemental Glyph Maps: Trebuchet, ArialBlack, Calibri
The three supplemental tables differ greatly in size but are structurally identical: they register only the keys that need correcting.
The TrebuchetMS Supplemental Map
The comment on TrebuchetMS explains its strategy:
// TrebuchetMS largely follows the standard Macintosh glyph ordering through
// glyph id 235, with the exceptions below. Additional mappings for the affected
// Latin glyph repertoire follow.
const getSupplementalGlyphMapForTrebuchetMS = getLookupTableFactory(
function (t) {
// A few of the standard Macintosh glyph slots hold a different, but
// similar looking, glyph in TrebuchetMS.
t[151] = 956;
t[159] = 937;
t[168] = 916;
t[189] = 8364;
t[195] = 8729;
t[218] = 713;
t[236] = 222;
t[237] = 254;
t[238] = 8722;
t[239] = 185;
t[240] = 178;
t[241] = 179;The first six entries are cases where "the same slot holds a similar-looking but different glyph": 956 is the Greek letter mu, 937 is Omega, 916 is Delta, 8364 is the euro sign, 8729 is the dot operator, and 713 is a macron. From 236 onward the table covers an extended range outside the Mac ordering, spanning Latin characters such as thorn, minus, superscript digits, fractions and hook forms.
The table has 75 entries and is used as "Mac-order base map first, then this map overrides". The "largely follows" in the comment is the key: Trebuchet agrees with the Mac ordering for the most part, so the base map can be reused, and only the places where it disagrees need per-item correction. The source comment in fonts.js gives a real consequence: if the standard font base map is used by mistake, š gets mapped to ž (issue 21713).
The ArialBlack Supplemental Map
getSupplementalGlyphMapForArialBlack has only 3 entries:
// The glyph map for ArialBlack differs slightly from the glyph map used for
// other well-known standard fonts. Hence we use this (incomplete) CID to GID
// mapping to adjust the glyph map for non-embedded ArialBlack fonts.
const getSupplementalGlyphMapForArialBlack = getLookupTableFactory(function (t) {
t[227] = 322;
t[264] = 261;
t[291] = 346;
});The three entries correct CIDs 227, 264 and 291: the value 322 is ł at U+0142, 261 is ą at U+0105, and 346 is Ś at U+015A. The table is small enough to read at a glance, yet enough to place a few key Latin Extended glyphs of ArialBlack correctly. It shows that supplemental tables need no system; they only need to cover known misalignments.
The Calibri Supplemental Map
getSupplementalGlyphMapForCalibri has 117 entries and is the largest supplemental table:
// The glyph map for Calibri (a Windows font) differs from the glyph map used
// in the standard fonts. Hence we use this (incomplete) CID to GID mapping to
// adjust the glyph map for non-embedded Calibri fonts.
const getSupplementalGlyphMapForCalibri = getLookupTableFactory(function (t) {
t[1] = 32;
t[4] = 65;
t[5] = 192;
t[6] = 193;
t[9] = 196;
t[17] = 66;
t[18] = 67;
t[21] = 268;
t[24] = 68;
t[28] = 69;
t[29] = 200;
t[30] = 201;
t[32] = 282;
t[38] = 70;
t[39] = 71;
t[44] = 72;
t[47] = 73;
t[48] = 204;
t[49] = 205;
t[58] = 74;
t[60] = 75;
t[62] = 76;
t[68] = 77;
t[69] = 78;
t[75] = 79;
t[76] = 210;
t[80] = 214;
t[87] = 80;
t[89] = 81;
t[90] = 82;
t[92] = 344;Its CID numbering starts at 1 rather than at 2 or 3 like the standard table; the letter A lands on 4, B on 17 and C on 18. This shows Calibri has its own CID ordering that cannot be solved by a few patches on the standard table; a larger table covering the whole basic Latin range is required. The second half also covers lowercase letters, digits and punctuation:
t[258] = 97;
t[259] = 224;
t[260] = 225;
t[263] = 228;
t[268] = 261;
t[271] = 98;
t[272] = 99;
t[273] = 263;
t[275] = 269;
t[282] = 100;
t[853] = 44;
t[855] = 58;
t[856] = 46;
t[876] = 47;
t[878] = 45;
t[882] = 45;
t[894] = 40;
t[895] = 41;
t[896] = 91;
t[897] = 93;
t[923] = 64;
t[940] = 163;
t[1004] = 48;
t[1005] = 49;
t[1006] = 50;Lowercase a is at 258, comma at 853 and the digit 0 at 1004. The key positions span a huge range, which shows the later Calibri slots also mix in punctuation and digits; the table registers only the 117 entries it is confident about, and leaves the rest to cidToGidMap and ToUnicode.
Put together, the three supplemental tables reveal a common design thread:
| Font | Base table | Supplemental entries | Trigger condition | Correction scope |
|---|---|---|---|---|
| TrebuchetMS | Mac standard glyph ordering | 75 | name matches Trebuchet | similar-looking characters and the extended Latin range |
| ArialBlack | standard font glyph map | 3 | name matches Arial-?Black | three misaligned letters |
| Calibri | standard font glyph map | 117 | name matches Calibri | basic Latin, digits and punctuation |
Why the Branch Conditions Use Regular Expressions
All three branches match the original name, not the already substituted fontName. This matters: TrebuchetMS is replaced by Helvetica in getNonStdFontMap, so if the branch looked at the substituted name it would never enter the Trebuchet Mac-order base map. The original name preserves vendor information; the substituted name only selects a usable font, and the two have different purposes.
| Object inspected | Source | Keeps vendor information? | Purpose |
|---|---|---|---|
name | original name in the font dictionary | yes | choosing the glyph-map base and supplemental tables |
fontName | after normalisation and substitution | partly | choosing a built-in file or a system font |
loadedName | the family name after the final split | no | CSS font name and internal identifier |
The regular expressions in the branches are written loosely. /Trebuchet/i only requires Trebuchet to appear in the name and covers TrebuchetMS, Trebuchet MS-Bold and forms with a subset prefix; /Arial-?Black/i accepts both ArialBlack and Arial-Black, matching the two spellings registered in the standard table; /Calibri/i covers the four weights. The price of loose matching is that another font sharing the prefix could be caught, but commercial fonts beginning with these strings are extremely rare in the font-name space, so the practical risk is acceptable.
The priority of the three branches is also worth noting. ArialBlack and Calibri are mutually exclusive if / else if, so only one supplemental table is applied at a time; Trebuchet sits in its own outer if and never fires together with them. This order does not conflict with the aliases in getStdFontMap, because aliases only change fontName and never affect the regular-expression test on the original name.
Design note: Supplemental tables are not a patch collection but an overlay that can be stacked. The base table covers most keys and the supplemental table only the known differences; the stacking order is fixed as base table first, supplemental table after. This structure means adding a new font special case requires only a small table, with no need to copy or modify the whole base map, and it keeps the origin and scope of each table traceable.
Query Functions, Exports and the Call Surface
The end of the file has only two functions, both very thin:
function getStandardFontName(name) {
const fontName = normalizeFontName(name);
const stdFontMap = getStdFontMap();
return stdFontMap[fontName];
}
function isKnownFontName(name) {
const fontName = normalizeFontName(name);
return !!(
getStdFontMap()[fontName] ||
getNonStdFontMap()[fontName] ||
getSerifFonts()[fontName] ||
getSymbolsFonts()[fontName]
);
}getStandardFontName queries only the standard table and returns a standard name or undefined. It does not query the non-standard table, because the caller wants to know whether this name maps directly to a built-in standard font. evaluator.js uses it to obtain standardFontName and then hands it to fetchStandardFontData or getFontSubstitution.
isKnownFontName is the union of four tables: the standard table, the non-standard table, the serif set and the symbol set; a hit in any of them returns true. It does not judge the substitution result, only whether the name is in the knowledge base. evaluator.js uses it to decide whether to trust the descriptor when the font name and the descriptor name disagree:
if (
fontNameStr &&
baseFontStr &&
(baseFontStr.startsWith(fontNameStr) ||
(!isKnownFontName(fontNameStr) && isKnownFontName(baseFontStr)))
) {
fontName = null;
}
fontName ||= baseFont;This check handles two real situations. The first is fontNameStr of Arial with baseFontStr of Arial,Bold: the descriptor name is a prefix of the base name, so the two are the same font and either can be used. The second is fontNameStr of wg09np with baseFontStr of Wingdings-Regular: the descriptor name is an internal name written casually by the generator, and the base name is the readable family name; here isKnownFontName concludes that the internal name is unknown and the base name is known, so the base name is used. If neither name is known, no substitution is made and the original is kept, avoiding the misclassification of an unknown font as a known one.
The Boundaries of Normalization
normalizeFontName performs only three replacements, so lookups have a clear line between hits and misses:
| Input | Normalised result | Hits the standard table | Reason |
|---|---|---|---|
Times New Roman | TimesNewRoman | yes | whitespace removed, exactly equal to the entry |
Times,New_Roman | Times-New-Roman | no | comma and underscore become hyphens, no such entry |
Arial-BoldMT | Arial-BoldMT | yes | no whitespace, hyphens preserved |
Arial Bold MT | ArialBoldMT | no | whitespace removed, no longer matches the entry |
Symbol-Bold | Symbol-Bold | yes | the style alias is registered separately |
ABCDEF+Times-Roman | ABCDEF+Times-Roman | no | the subset prefix is not in the table and must be stripped first |
The last row explains why fonts.js calls name.split("+") before looking up. The subset prefix is six uppercase letters followed by a plus sign, and PDF.js does not treat it as part of the font name; querying the full name directly would never hit. The query functions do not strip the prefix themselves because they do not know which parsing path the caller is on.
Why the Two Query Functions Are Not Merged
getStandardFontName and isKnownFontName look like they could be merged into one function: query four tables, then return a standard name or a boolean. The source does not do that, because the two return completely different semantics.
getStandardFontName returns a standard identity that can be passed on. After receiving it, the caller fetches the built-in file, looks up metrics and hands it to the system substitution module; the return value must be a name from the standard table, never true or the original name. isKnownFontName returns a trust decision. The caller only uses it in a conditional and needs neither the table that hit nor a standard name.
| Dimension | getStandardFontName | isKnownFontName |
|---|---|---|
| Query scope | standard table only | standard table, non-standard table, serif set, symbol set |
| Return value | a standard name string or undefined | a boolean |
| Typical use | fetching built-in files, metrics, system substitution | judging whether a descriptor name is trustworthy |
| Meaning of a miss | not a standard font that can be used directly | completely unfamiliar; none of the four knowledge categories records it |
After merging, a caller would either have to test whether the return value is a string, or query the standard table again when it needs a standard name, adding a step. Splitting into two small functions costs one duplicated normalisation and gives every call site a result it can use directly.
The export list divides the twelve members into three groups:
export {
getFontNameToFileMap,
getGlyphMapForMacOrderedFonts,
getGlyphMapForStandardFonts,
getNonStdFontMap,
getSerifFonts,
getStandardFontName,
getStdFontMap,
getSupplementalGlyphMapForArialBlack,
getSupplementalGlyphMapForCalibri,
getSupplementalGlyphMapForTrebuchetMS,
getSymbolsFonts,
isKnownFontName,
};| Category | Members | Callers |
|---|---|---|
| Name tables | getStdFontMap, getNonStdFontMap, getSerifFonts, getSymbolsFonts, getFontNameToFileMap | evaluator.js, fonts.js |
| Glyph tables | getGlyphMapForMacOrderedFonts, getGlyphMapForStandardFonts, the three supplemental tables | fonts.js |
| Query functions | getStandardFontName, isKnownFontName | evaluator.js |
Looking at the call surface, the file holds no state and receives no PDF objects. Every function is either pass a name, query a table, return a result, or get a table and let the caller stack it. That lets several font paths inside the Worker share it, and makes it easy to test: no Dict, no stream, only strings.
Design note: The query functions only normalise and look up; they never mix in the substitution chain, width metrics or glyph mapping.
getStandardFontNamedoes not know that Calibri degrades to Helvetica, andisKnownFontNamedoes not know that Trebuchet needs the Mac-order base map. Clear responsibility boundaries are what let callers choose different tables at different stages instead of being tied to one universal query.
The Contract with Neighbouring Modules
The file holds no state and starts no asynchronous work, so its contract with neighbouring modules can be described completely as what goes in and what comes out.
Dependencies on Upstream Modules
| Upstream module | Import | Contract |
|---|---|---|
fonts_utils | MacStandardGlyphOrdering | provides the 258-entry standard Mac glyph name array |
fonts_utils | normalizeFontName | provides a pure function that removes whitespace and unifies separators |
glyphlist | getGlyphsUnicode | provides the lazily loaded glyph-name to Unicode code point table |
core_utils | getLookupTableFactory | provides the construct-once, reuse-after wrapper |
All four dependencies are pure data or pure functions; none reads a PDF, touches the network or mutates global state. That lets standard_fonts.js be called on any thread at any time, independent of the lifetime of XRef, PDFManager or PartialEvaluator.
Outputs to Downstream Modules
| Downstream module | Members used | How it is used |
|---|---|---|
evaluator.js font name resolution | getStandardFontName, isKnownFontName | decides the standard name, judges whether a descriptor name is trustworthy |
evaluator.js metrics | getStdFontMap, getSerifFonts | chooses the standard metric table, falls back to Times when serif |
evaluator.js file fetching | getFontNameToFileMap | standard name to a .pfb or .ttf file name |
evaluator.js flags | getSymbolsFonts | decides the Symbolic flag |
fonts.js fallback | getStdFontMap, getNonStdFontMap, getSerifFonts | computes isStandardFont, fontName and the serif tendency |
fonts.js glyph mapping | the five glyph tables | constructs toFontChar and ToUnicodeMap |
getStandardFontName appears in several places in evaluator.js, covering the simple font, composite font and CFF font branches; the excerpt below is the simple font branch:
const standardFontName = getStandardFontName(baseFontName);
let file = null;
if (standardFontName) {
file = await this.fetchStandardFontData(standardFontName);
properties.isInternalFont = !!file;
}
if (!properties.isInternalFont && this.options.useSystemFonts) {
properties.systemFontInfo = getFontSubstitution(
this.systemFontCache,
this.idFactory,
this.options.standardFontDataUrl,
baseFontName,
standardFontName,
type,
);
}This code links three paths: first obtain the standard name, then use it to fetch the built-in file; if that fails and system fonts are allowed, hand both the original name and the standard name to the substitution module. getFontSubstitution needs both: the original name to match a concrete family in the substitution table, and the standard name as a second fallback to a standard font. This interface shows that the output of standard_fonts.js is not a final decision but one input to a downstream decision.
getBaseFontMetrics shows the metric path:
let fontName = normalizeFontName(name);
const stdFontMap = getStdFontMap();
fontName = stdFontMap[fontName] || fontName;
const Metrics = getMetrics();
const glyphWidths =
Metrics[fontName] ?? Metrics[this.isSerifFont(name) ? 'Times-Roman' : 'Helvetica'];It normalises first, queries the standard table for the standard name, then fetches the width table from metrics.js; if that fails, it falls back to Times-Roman or Helvetica depending on the serif test. getNonStdFontMap is not used here, because the metric tables recognise only standard fonts; a non-standard name keeps its original form and then hits the default fallback. This detail differs from fallbackToSystemFont in fonts.js, which does use the non-standard table because it needs a drawable font name rather than standard metrics.
What It Does Not Do
To keep its responsibility from growing, here are the things it explicitly does not do:
| What it does not do | Module actually responsible | Reason |
|---|---|---|
reading /BaseFont and /Flags | evaluator.js | this file never touches PDF objects |
| parsing font files | type1_font.js, cff_font.js, fonts.js | this file only provides mapping data |
| deciding system fonts | font_substitutions.js | needs platform and font-list information |
| caching fetched font bytes | Catalog.standardFontDataCache | document-level resources must be released per document |
| generating glyph outlines | font_renderer.js, glyph.js | this file does no drawing |
This boundary lets the file stay in the shape of pure data plus two query functions. Anything requiring PDF context, asynchronous I/O or platform information is pushed to neighbouring modules; the data layer answers only domain questions.
How Missing Values Flow
The file throws no exceptions and performs no null checks. Every "do not know" appears as undefined or false, and the caller decides the next step:
| Call | Return value on a miss | What the caller does |
|---|---|---|
getStdFontMap()[name] | undefined | continue with the non-standard table or keep the original name |
getNonStdFontMap()[name] | undefined | keep the original name |
getSerifFonts()[name] | undefined | treat as non-serif |
getSymbolsFonts()[name] | undefined | treat as non-symbol |
getFontNameToFileMap()[name] | undefined | the built URL lacks a file name, the fetch fails and returns null |
getStandardFontName(name) | undefined | skip the built-in file and go straight to the system font branch |
isKnownFontName(name) | false | do not trust the name, keep the original |
This convention matches the fault-tolerance approach of Catalog: the data layer does not judge whether something is an error, it only returns whether something exists. undefined is kept strictly apart from an empty string and from null; a value missing from a table is never filled with a default, because a default would hide a real spelling problem in the name.
The miss of getFontNameToFileMap deserves special attention. When filename is undefined, fetchBinaryData receives a URL ending in undefined, the request fails, and fetchStandardFontData catches the exception and returns null. The font does not raise an error; it simply follows the path where isInternalFont is false. This is a deliberate degradation: a missing file name for an unknown font should not make the whole page fail to render.
Conclusion
standard_fonts.js is the data dictionary of the PDF.js font layer: downward it depends on normalisation and the Mac glyph ordering from fonts_utils, and on the glyph name table from glyphlist; upward it provides name recognition and built-in font file names to evaluator.js, and glyph-map base and supplemental tables for non-embedded fonts to fonts.js. It parses no font file and draws no glyph; it only organises domain knowledge into lazily loaded lookup tables.
Where It Sits in the Overall Chain
| Stage | Module | Input | Output |
|---|---|---|---|
| Dictionary reading | evaluator.js | /BaseFont, /Flags, /Encoding | normalised font name, font type |
| Name recognition | standard_fonts.js | font name | standard name, substitute name, serif or symbol tag |
| File selection | getFontNameToFileMap | standard name | .pfb or .ttf file name |
| System substitution | font_substitutions.js | standard and non-standard names | local font information or built-in font information |
| Glyph mapping | fonts.js | CIDs, cidToGidMap, base and supplemental tables | toFontChar, ToUnicodeMap |
| Drawing and extraction | canvas and text layer | glyphs and Unicode | page image, selectable text |
Strung together in one sentence: evaluator.js turns the font dictionary into a name, standard_fonts.js turns the name into a standard identity and a substitution target, and fonts.js turns the identity into a drawable glyph mapping.
What the Data Volume Means
Adding the nine tables together, the number of entries resident in memory is roughly twelve hundred: 280 name entries, 691 glyph entries, plus at most 256 from the generated Mac-order table. That is small for a Worker, yet enough to cover the great majority of documents with non-embedded fonts.
| Data category | Entry count | Growth pattern | Maintenance cost |
|---|---|---|---|
| Font names | about 280 | append when a new vendor name appears | low, a single line is enough |
| Glyph mappings | about 690 | add keys from real samples | medium, Unicode correctness must be verified |
| Generated Mac order | at most 256 | generated automatically from the ordering and the glyph name table | very low, follows upstream table changes |
The data volume also explains why these tables can be fully static. They need no database, no runtime download and no per-document update; once packaged into the Worker script they can be queried synchronously at any time. For a viewer that must open documents quickly, that is far more stable than building mapping tables on the fly during parsing.
The Full Path of a Typical Query
Suppose a PDF writes a non-embedded Arial,Bold and system fonts are disabled. The chain looks roughly like this:
// /BaseFont = "Arial,Bold", with no embedded font file
const baseFontName = 'Arial,Bold';
const standardFontName = getStandardFontName(baseFontName);
// normalizeFontName("Arial,Bold") → "Arial-Bold"
// getStdFontMap()["Arial-Bold"] → "Helvetica-Bold"
const fileName = getFontNameToFileMap()[standardFontName];
// → "LiberationSans-Bold.ttf"
// after the evaluator fetches the file, fonts.js builds the Font by name
// if it is CIDFontType2 + Identity encoding without a complete CIDToGIDMap:
applyStandardFontGlyphMap(map, getGlyphMapForStandardFonts());There are three table lookups in this path, answering three questions: is this name a standard font, what is the corresponding built-in file called, and which Unicode values does a CID probably correspond to when there is no glyph mapping. None of the three depends on PDF objects, only on strings and a few large tables; the only truly asynchronous step is fetching the font file.
If baseFontName were Calibri, the first step would fail because Calibri is not in the standard table; evaluator.js would then hand the original name to getFontSubstitution, and font_substitutions.js would decide on a system substitute; with system fonts disabled the font finally goes through fallbackToSystemFont, where the non-standard table turns the name into Helvetica and the standard glyph map is applied. The path differs, but every step still queries only one table from this file.
Which Table to Check When Something Goes Wrong
| Question | Table to check | Return shape |
|---|---|---|
| Is this name a standard font | getStdFontMap | a standard name or undefined |
| What should a non-embedded font become | getNonStdFontMap | a substitute name or undefined |
| Does this font name tend to be serif | getSerifFonts | boolean |
| Is this font name a symbol font | getSymbolsFonts | boolean |
| Which built-in file does a standard font use | getFontNameToFileMap | a file name or undefined |
| Which Unicode does a CID probably correspond to | getGlyphMapForStandardFonts | object table |
| Do the glyphs follow the Mac standard ordering | getGlyphMapForMacOrderedFonts | object table |
| Is it Trebuchet, ArialBlack or Calibri | the three getSupplementalGlyphMapFor... | overlay table |
This index also works as a navigation aid while reading the source: first decide which kind of question you are answering, then go to the corresponding factory function, instead of looking in one table for another table's answer.
One more remark: there is no implicit priority between the tables. The order of the standard and non-standard tables is decided by the caller, the order of base and supplemental tables is decided by fonts.js, and the serif and symbol sets only provide boolean judgements. Keeping the order in the caller instead of in the data means the same table can be combined differently on different paths.
Six Design Principles
- Data and algorithm are separated: of the 1092 lines, the vast majority are table entries, and the real logic is only the factory wrapper, the normalised lookup and two query functions. Font knowledge can be reviewed and extended independently, without reading the parser.
- Lazy loading instead of startup construction: all nine tables are built by
getLookupTableFactoryon the first call and reused afterwards. Opening a document pays no memory or construction cost for unused fonts. - Normalisation only flattens separators:
normalizeFontNamemakes commas, underscores and whitespace uniform hyphens but preserves case. Table entries must be registered with the vendor's actual spelling for lookups to be stable. - Standard identity and substitution target are separated:
getStdFontMapanswers whether a font is standard, andgetNonStdFontMapanswers what it should become. Keeping them apart is what letsisMappedToStandardFontjudge accurately whether the substitution still lands on a standard font. - Glyph mapping uses a base map plus overlays: the Mac ordering or the standard table provides most keys, and the supplemental tables for Trebuchet, ArialBlack and Calibri correct only the known differences, with the stacking order fixed as base first, supplemental after.
- The mapping tables are deliberately incomplete:
getGlyphMapForStandardFontshas only 496 entries and the Calibri supplemental table only 117. They are the fallback starting point for non-embedded fonts, and the final result is still corrected bycidToGidMap,ToUnicodeand the real font file.
Once you understand standard_fonts.js, looking again at fallbackToSystemFont in fonts.js, getBaseFontMetrics in evaluator.js and the font matching in font_substitutions.js makes it clear that they share the same names and mappings instead of each maintaining its own font knowledge. The next article stays in the font layer and moves into fonts.js, which actually constructs the Font family, to see how these tables become toFontChar, toUnicode and glyph widths.
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
- PDF.js Deep Dive from Demo 10: CanvasGraphics and operatorList RenderingOctober 27, 2026
- PDF.js Deep Dive from Demo 9: Network Streams and Range RequestsOctober 27, 2026
- PDF.js Deep Dive from Demo 8: PDFWorker.create and the Real/Fake Worker HandshakeOctober 27, 2026
- PDF.js Deep Dive from Demo 7: WorkerTransport InitializationOctober 27, 2026
- PDF.js Deep Dive from Demo 6: MessageHandler Cross-Thread CommunicationOctober 27, 2026
- PDF.js Deep Dive from Demo 5: renderOctober 27, 2026
- PDF.js Deep Dive from Demo 4: getViewportOctober 27, 2026
- PDF.js Deep Dive from Demo 3: getPageOctober 27, 2026
- PDF.js Deep Dive from Demo 2: getDocumentOctober 27, 2026
- PDF.js Source Walkthrough: TextLayerOctober 27, 2026
- PDF.js Source Walkthrough: font_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_displayOctober 27, 2026
- PDF.js Source Walkthrough: PDFObjectsOctober 27, 2026
- PDF.js Source Walkthrough: Network StreamsOctober 27, 2026
- PDF.js Source Walkthrough: internal_evt.jsOctober 27, 2026
- PDF.js Source Walkthrough: math_clamp.jsOctober 27, 2026
- PDF.js Source Walkthrough: css_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: scripting_utilsOctober 27, 2026
- PDF.js Source Walkthrough: MurmurHash3_64October 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_utilsOctober 27, 2026
- PDF.js Source Walkthrough: image_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: BasePDFStreamOctober 27, 2026
- PDF.js Source Walkthrough: Shared UtilitiesOctober 27, 2026
- PDF.js Source Walkthrough: MessageHandlerOctober 27, 2026
- PDF.js Source Walkthrough: apiOctober 27, 2026
- PDF.js Source Walkthrough: fontsOctober 27, 2026
- PDF.js Source Walkthrough: glyphlistOctober 27, 2026
- PDF.js Source Walkthrough: binary_cmapOctober 27, 2026
- PDF.js Source Walkthrough: cmapOctober 27, 2026
- PDF.js Source Walkthrough: encodingsOctober 27, 2026
- PDF.js Source Walkthrough: metricsOctober 27, 2026
- PDF.js Source Walkthrough: fonts_utilsOctober 27, 2026
- PDF.js Source Walkthrough: default_appearanceOctober 27, 2026
- PDF.js Source Walkthrough: metadata_parserOctober 27, 2026
- PDF.js Source Walkthrough: name_number_treeOctober 27, 2026
- PDF.js Source Walkthrough: struct_treeOctober 27, 2026
- PDF.js Source Walkthrough: catalogOctober 27, 2026
- PDF.js Source Walkthrough: pdf_managerOctober 27, 2026
- PDF.js Source Walkthrough: object_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_coreOctober 27, 2026
- PDF.js Source Walkthrough: cryptoOctober 27, 2026
- PDF.js Source Walkthrough: core_utilsOctober 27, 2026
- PDF.js Source Walkthrough: primitivesOctober 27, 2026
- PDF.js Source Walkthrough: worker_streamOctober 27, 2026
- PDF.js Source Walkthrough: ccitt_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpx_streamOctober 27, 2026
- PDF.js Source Walkthrough: jbig2_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpeg_streamOctober 27, 2026
- PDF.js Source Walkthrough: decrypt_streamOctober 27, 2026
- PDF.js Source Walkthrough: brotli_streamOctober 27, 2026
- PDF.js Source Walkthrough: run_length_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_hex_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_85_streamOctober 27, 2026
- PDF.js Source Walkthrough: lzw_streamOctober 27, 2026
- PDF.js Source Walkthrough: predictor_streamOctober 27, 2026
- PDF.js Source Walkthrough: flate_streamOctober 27, 2026
- PDF.js Source Walkthrough: decode_streamOctober 27, 2026
- PDF.js Source Walkthrough: chunked_streamOctober 27, 2026
- PDF.js Source Walkthrough: streamOctober 27, 2026
- PDF.js Source Walkthrough: base_streamOctober 27, 2026
- PDF.js Deep Dive 11: ParserOctober 26, 2026
- PDF.js Deep Dive 10: XRefOctober 26, 2026
- PDF.js Deep Dive 9: CanvasGraphicsOctober 26, 2026
- PDF.js Deep Dive 8: PDFDocumentOctober 26, 2026
- The AI Era: What Should We Do to Survive? (Reflections on Founding StreamPDF)October 26, 2026
- PDF.js Deep Dive 7: PartialEvaluatorOctober 25, 2026
- PDF.js Deep Dive 6: PDFManager.initDocumentOctober 25, 2026
- PDF.js Deep Dive 5: PDFFetchStream and PDFWorkerStreamOctober 25, 2026
- PDF.js Deep Dive 4: MessageHandlerOctober 22, 2026
- PDF.js Deep Dive 3: getViewport & renderOctober 22, 2026
- PDF.js Deep Dive from Demo 1: Rendering a PDF with a Custom ViewerOctober 19, 2026
- PDF.js Deep Dive 2: getDocument & getPageOctober 19, 2026
- PDF.js Deep Dive 1: The Architecture of PDF.jsOctober 16, 2026