StreamPDFStreamPDF
Back to blog

PDF.js Source Walkthrough: glyphlist

October 27, 2026 · StreamPDF Team

PDF.js Source Walkthrough: glyphlist

Introduction

The font layer has reached this point, and the previous articles have already covered most of "how bytes become glyphs": encodings.js maps single-byte character codes to glyph names (seven 256-entry tables such as StandardEncoding, WinAnsiEncoding, MacRomanEncoding, SymbolSetEncoding and ZapfDingbatsEncoding); cmap.js handles character code to CID for composite fonts, and character code to Unicode from the /ToUnicode stream; the Font class in fonts.js assembles the font dictionary, glyph data, widths, encoding and Unicode mapping into a renderable object; fonts_utils.js, type1_font.js and cff_parser.js handle glyph name lists and glyph programs.

One link has been missing in the middle all along: how a glyph name itself is read aloud. encodings.js only answers "which name does this byte correspond to", and /ToUnicode only answers "which Unicode does the author claim this byte corresponds to". When a PDF has no /ToUnicode, or its /ToUnicode is incomplete, misaligned or covers only some characters, the only semantic clue left to the viewer is the glyph name. The module that turns names such as Aacute, afii10017, a1 and f_f into code points is src/core/glyphlist.js.

The file is 4557 lines, of which 4531 are data assignments; the logic is just two import lines and one export line:

import { getLookupTableFactory } from './core_utils.js';
export { getDingbatsGlyphsUnicode, getGlyphsUnicode };

It exports two zero-argument factories: getGlyphsUnicode corresponds to the Adobe Glyph List (hereafter AGL), and getDingbatsGlyphsUnicode corresponds to ZapfDingbats glyph names. Both return plain objects mapping glyph name strings to code point integers.

In one sentence: it is the font layer's name dictionary, taking glyph names produced by encodings.js and /Differences from below, and providing code points upward to fonts.js, evaluator.js, font_renderer.js and standard_fonts.js, ultimately deciding which characters the text layer can copy and which character is used when falling back to a system font.

Version note: This article follows this version of glyphlist.js in the current repository. This version has no compact string or flat array form of the data: both tables assign t.Name = 0xNNNN entry by entry inside the initializer of getLookupTableFactory. Historically there was indeed a flat-array version (October 2020 introduced getArrayLookupTableFactory, reverted that November), but the current source has neither getArrayLookupTableFactory nor any code that splits a string on a separator. In addition, this version appends 116 TeX-specific names at the end of the AGL table and inserts 5 underscore-style ligatures into the f section; older versions or other branches may not have these entries. Do not apply the implementation you remember when reading the source.

Module Overview

glyphlist.js has a very simple structure, only four blocks:

BlockKey members or tablesResponsibility
DependencygetLookupTableFactoryProvides the lazy-loading shell that builds the table on the first call and returns it afterwards
General glyph tablegetGlyphsUnicode, 4328 entriesAGL glyph name to code point, covering Latin, Greek, Cyrillic, Armenian, Hebrew, Arabic, Indic scripts, Thai, kana, bopomofo, Hangul compatibility letters, the CJK compatibility area and TeX math names
Symbol glyph tablegetDingbatsGlyphsUnicode, 203 entriesZapfDingbats numbered names such as a1 to a206 to code points, mostly in the U+2700 block
Exportexport { getDingbatsGlyphsUnicode, getGlyphsUnicode }Exposes only the two factories, never the tables themselves

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

// PDF font dictionary and content stream
//   → encodings.js          character code → glyph name (seven 256-entry encoding tables)
//   → font built-in encoding / Differences   character code → more glyph names
//   → glyphlist.js          glyph name → code point (this article)
//   → cmap.js ToUnicode     character code → Unicode (author-provided, may be missing)
//   → fonts.js Font         toUnicode / toFontChar / fontChar
//   → canvas drawing and text layer    visible glyphs and copyable text

Note that in the diagram glyphlist.js and cmap.js are two parallel sources, not a series: /ToUnicode takes priority when it exists and is trustworthy; only when it is missing or untrustworthy does the code fall back to "look up the glyph name". This is also why fonts.js has both "after-the-fact repairs" such as adjustTrueTypeToUnicode and adjustType1ToUnicode, and a branch in fallbackToSystemFont that builds toFontChar directly from the font name.

glyphlist.js is not a parser and does not manage caches. It does not read font data, does not understand PDF syntax, and only moves static constants into an object the first time it is called. The sections below proceed through "why it is needed, what the two tables look like, how a table is built, what patterns the data shows, who looks things up, and what happens on failure".

The Calling Contract of the Two Factories

The external contract of both factories is identical and very narrow:

TermBehaviour
Call formZero arguments, getGlyphsUnicode() or getDingbatsGlyphsUnicode()
Return typeNull-prototype object, keys are glyph name strings, values are integer code points
Object identityAlways returns the same object within one Worker, never a new one
Build timeRuns the initializer on the first call, returns the reference afterwards
LifetimeSame as the Worker, does not take part in any document-level cleanup
MissReturns undefined, throws nothing, logs nothing

The caller's code is therefore very simple:

const glyphsUnicodeMap = getGlyphsUnicode(); // build the table or take the existing reference
const unicode = glyphsUnicodeMap['A']; // 0x0041
const missing = glyphsUnicodeMap['A.sc']; // undefined

The narrow contract has a direct benefit: "no context is needed", since a lookup does not care which document, font or page is current. The downside is equally clear — the table cannot be customised per document. If a PDF needs extra name mappings (for example a vendor's private glyph names), the consumer can only build an overlay, not modify this global table. This version has no overlay mechanism at all; every extension is added directly to the source, which also explains why a TeX section appears at the end of the file and why underscore ligatures show up in the f section.

Design note: Once a globally shared object is exposed, it cannot be isolated per document. glyphlist.js therefore reduces "extensibility" to a minimum: no write interface, no registration function, no parameters. Customisation is left to the mapping tables above, so that a malicious PDF cannot pollute the whole Worker's name resolution.

Three Parallel Code-Point Interpretation Paths

A simple font (Type1, TrueType, Type3) runs several paths in parallel during rendering and text extraction, and glyphlist.js serves only one of them:

PathInputIntermediate resultOutputConsequence if missing
Renderingcharacter codeglyph name or glyph idfontChar / glyph programa missing glyph on screen, or a fallback to .notdef
Textcharacter code/ToUnicode or glyph nameunicode stringgarbled copy, search and read-aloud
Fallbackfont name + character codestandard font name tablesystem font and toFontCharwhole stretches of text misplaced when no font is embedded

The rendering path does not necessarily need Unicode: as long as the glyph data is in the file, Font can draw directly by glyph id. What truly depends on "glyph name to Unicode" is the text path and the system-font fallback path. The Glyph class in fonts.js keeps these two outputs side by side, which illustrates the division of labour:

class Glyph {
  constructor(
    originalCharCode,
    fontChar,
    unicode,
    accent,
    width,
    vmetric,
    operatorListId,
    isSpace,
    isInFont
  ) {
    this.originalCharCode = originalCharCode;
    this.fontChar = fontChar;
    this.unicode = unicode;
    // ...
  }

fontChar is for the canvas, unicode is for the text layer. The two can disagree: a font that only carries Type1 glyph programs may draw Aacute very accurately while leaving unicode empty when there is no /ToUnicode.

Where the Names Come From: The PostScript Inheritance

PDF did not invent "glyph names" out of thin air. It inherited them from PostScript, and PostScript font formats indexed glyphs by name from the start:

FormatWhere the names liveRole
Type1keys of the /CharStrings dictionarythe key of each glyph program is the name; programs are fetched by name
CFFcharsetglyph id to SID or custom string, all ultimately restored to names
TrueTypepost table 2.0glyph id to name; the first 258 are the standard Mac ordering names
PDFencoding tables and /Differencescharacter code to name

The Type1 parser reads names out as ordinary tokens:

            if (token !== "/") {
              continue;
            }
            const glyph = this.getToken();
            length = this.readInt();
            this.getToken(); // read in 'RD' or '-|'
            data = length > 0 ? stream.getBytes(length) : new Uint8Array(0);

The CFF charset, on the other hand, maps glyph ids back to names one by one:

const charset = [cid ? 0 : '.notdef'];
// ...
charset.push(cid ? id : strings.get(id));

strings.get(id) returns the name in the standard strings or the custom strings. In other words, the glyph order of a CFF font is entirely determined by the name sequence; the glyph id is only an index into that sequence.

Names are not just internal identifiers; they are the common language across font formats. PDF encoding tables, Type1 CharStrings and the CFF charset all use the same naming conventions, so a single AGL can serve all three sources at once. This is the fundamental reason glyphlist.js can be a "global table independent of the document".

However, this naming is not always reliable in real files: TrueType's post table is often mangled by generators, or even filled with 0 to mean "no names". PDF.js therefore avoids names in most cases and instead takes code points directly from cmap; only when a standard font is embedded as a CID font without a glyph mapping does it reconstruct names using MacStandardGlyphOrdering plus AGL. A concrete example is in the consumer section later.

One Character's Full Journey

The same character code 0x41 ends at completely different names and code points in different fonts:

FontEncoding tableCharacter codeName foundGlyph tableCode point
Ordinary text fontStandardEncoding0x41AgetGlyphsUnicode0x0041
SymbolSymbolSetEncoding0x41AlphagetGlyphsUnicode0x0391
ZapfDingbatsZapfDingbatsEncoding0x21a1getDingbatsGlyphsUnicode0x2701

0x41 itself has no meaning; it is just a byte. Meaning is decided by the pair "encoding table + glyph table": StandardEncoding says 0x41 is called A, SymbolSetEncoding says 0x41 is called Alpha. If the two are mismatched, A is not found in the Dingbats table and the text layer loses the character.

This chain can go one step further: 0x2701 passes through String.fromCharCode to become a string, written into Font.toUnicode; the text layer reads that string back, and that is what the user sees when selecting. Throughout, glyphlist.js contributes only the middle "name to number" conversion, but it decides whether the correct character is obtained in the end.

The Specification Delegates This to AGL

_simpleFontToUnicode in evaluator.js writes the steps required by the specification as numbered comments:

    const glyphsUnicodeMap = getGlyphsUnicode();
    for (const charcode in encoding) {
      // a) Map the character code to a character name.
      let glyphName = encoding[charcode];
      if (glyphName === "") {
        continue;
      }
      // b) Look up the character name in the Adobe Glyph List (see the
      //    Bibliography) to obtain the corresponding Unicode value.
      let unicode = glyphsUnicodeMap[glyphName];
      if (unicode !== undefined) {
        toUnicode[charcode] = String.fromCharCode(unicode);
        continue;
      }

This code explains three things at once:

  1. The official basis for glyph name to Unicode is the Adobe Glyph List, not any table inside the font;
  2. The lookup result goes straight into String.fromCharCode, which shows that the table stores code point integers, not strings;
  3. On a miss it returns undefined, and the flow continues into the heuristics below instead of failing on the spot.

The third point is crucial. glyphlist.js does not define the concept of a "bad name"; it only answers "is there one". Whether a name is valid, whether it should be repaired and how, is entirely up to the caller. This boundary lets the table stay pure data and lets different callers apply their own fault-tolerance strategies to the same miss.

The Table Is a Protocol Constant, Not Document State

The name sets of AGL and ZapfDingbats come from Adobe's public data, identical for every PDF and independent of the document. The table can therefore be shared by the whole Worker, built once, and never cleaned up:

PropertyBehaviourImpact
Globalnot attached to Catalog or Fontcleanup does not need to handle it
Read-onlyno write path at allsafe for concurrent lookups across fonts and pages
Idempotentthe factory returns the same object every timecallers may call it freely without passing references around
Silenta miss returns undefinederror handling is the caller's responsibility

Design note: glyphlist.js separates "data" from "policy" completely. The table only handles literal hits, and on a miss it hands back a blank; name normalisation, reverse lookup and heuristic code-point recovery live in unicode.js, fonts_utils.js and evaluator.js respectively. That way the table can be checked entry by entry against Adobe's original data, while the fault-tolerance policy can evolve and be tested independently.

The Two Tables: AGL and ZapfDingbats

AGL: One Name, One Code Point

getGlyphsUnicode starts with a run of names in alphabetical order, uppercase names in the first half and lowercase names in the second:

const getGlyphsUnicode = getLookupTableFactory(function (t) {
  t.A = 0x0041;
  t.AE = 0x00c6;
  t.AEacute = 0x01fc;
  t.AEmacron = 0x01e2;
  t.AEsmall = 0xf7e6;
  t.Aacute = 0x00c1;
  t.Aacutesmall = 0xf7e1;
  t.Abreve = 0x0102;
  t.Abreveacute = 0x1eae;
  t.Abrevecyrillic = 0x04d0;
  t.Abrevedotbelow = 0x1eb6;
  // ...
  t.Alpha = 0x0391;
  t.Alphatonos = 0x0386;
  t.Amacron = 0x0100;
  t.Amonospace = 0xff21;

This excerpt already reveals several name sources in AGL:

Name shapeExamplesMeaning
Single letters and Latin extensionsA, AE, Abrevebasic Latin and letters with diacritics
Greek namesAlpha, AlphatonosGreek letters and their accented forms
Monospace formsAmonospacefullwidth or halfwidth monospaced variants, landing at U+FF21
small variantsAEsmall, Aacutesmallsmall capitals from the Expert character set, landing in the private use area
afii numbersafii10017numbered names from the early font interchange standard, whose code point cannot be derived from the name

Naming: A Base Name Plus a Suffix, but Every Name Registered Individually

AGL is not a generative system that "combines infinite names by rules"; it is a closed list. Several suffix families are visible in the names, but every combination must appear in the table on its own:

Suffix familyCountRepresentative entryWhere the code point goes
cyrillic222AcyrillicU+0400 block
arabic244ainarabicthe U+0600 block and the U+FE70 presentation forms block
hebrew169alefhebrewthe U+0590 block and the U+FB1D presentation forms
katakana93akatakanaU+30A0 block
hiragana86ahiraganaU+3040 block
halfwidth62akatakanahalfwidthU+FF00 block
monospace99AmonospaceU+FF00 block
thai109Thai letter names such as kothaiU+0E00 block
bopomofo37aibopomofoU+3100 block
small100AEsmallprivate use area U+F600 to U+F8FF
oldstyle12oneoldstyleprivate use area
superior42superscript digit and letter variantsthe U+2070 block and the private use area
inferior17subscript digit variantsthe U+2080 block and the private use area
display14summationdisplaymathematical symbols in the U+2200 block

This count reveals an important fact: suffixes do not form a derivable grammar. Acyrillic exists, Acyrillicsmall does not; ainarabic and ainfinalarabic are two separate entries, and the latter's code point is not in the main Arabic block but in the Arabic presentation forms after U+FE70. You cannot write a "base name plus suffix, then compute an offset" function to replace this table, because the offset changes with letter shape, and the shape names themselves come from different standards of different historical periods.

The Arabic line best illustrates the problem. The same letter ain has isolated, initial, medial and final shapes, corresponding to four names and four code points:

NameShapeCode point
ainarabicisolatedU+0639
aininitialarabicinitialaround U+FEBB
ainmedialarabicmedialaround U+FECC
ainfinalarabicfinalaround U+FECA

The four code points are spread across two non-adjacent blocks, with other scripts in between. Any attempt to "compress this into a formula" fails in similar places, which is why the table must exist in its raw-data form.

A quick count of the table's actual size:

MetricValueNote
Entries43284327 dotted assignments plus one .notdef
Source sizeabout 115.7 KBtable body only, excluding comments and exports
Code point rangeU+0000 to U+FFE6all inside the Basic Multilingual Plane
Private use values192 entriesfalling in U+F600 to U+F8FF, that is, PUA
ASCII values159 entriesvalues no greater than U+007F, including .notdef at 0
Supplementary plane values0 entriesno code point above U+FFFF in the table

The last two rows deserve separate thought. The table has no supplementary-plane code points, which shows that the AGL data itself is a product of the BMP era; yet the uXXXX{XX} branch of getUnicodeForGlyph accepts 5 or 6 hexadecimal digits and can resolve something like U+1D400, a mathematical letter. In other words, characters the table does not cover can only be recovered through the encoding rules in the name. This is the premise of the "name variants" section later.

Code-Point Distribution: The Data Explains Its Own Coverage

Counting the 4328 entries by code point range gives the main distribution below (these ranges do not overlap; ranges not listed are discussed separately after the table):

Code point rangeCountContents
U+0000 to U+007F159Basic Latin and control characters
U+0080 to U+00FF102Latin-1 Supplement
U+0100 to U+017F159Latin Extended-A
U+0180 to U+024F152Latin Extended-B
U+0250 to U+036F219IPA, modifier letters, combining marks
U+0370 to U+03FF111Greek and Coptic
U+0400 to U+052F327Cyrillic
U+0530 to U+058F85Armenian
U+0590 to U+05FF259Hebrew
U+0600 to U+06FF175Arabic main block
U+0900 to U+0DFF344Indic scripts
U+0E00 to U+0E7F87Thai
U+1E00 to U+1EFF246Latin Extended Additional
U+3000 to U+30FF224CJK punctuation, hiragana, katakana
U+3100 to U+312F37Bopomofo
U+3130 to U+318F94Hangul compatibility letters
U+3200 to U+33DD236enclosed CJK and CJK compatibility
U+FB00 to U+FB4F78Latin ligatures
U+F600 to U+F8FF192private use area
U+FF00 to U+FFEF162halfwidth and fullwidth forms

The table above sums to 3448 entries; the remaining 880 are concentrated in two stretches. The first is punctuation, currency, enclosed letters, arrows, mathematical operators, box drawing and geometric shapes from U+2000 to U+27BF, 674 entries; the second is Arabic presentation forms and CJK compatibility forms from U+FB50 to U+FEFF, 204 entries. Besides those there are only two isolated entries (U+1D40 and U+5344). Both stretches are "typographic variants or symbol extensions of the same character", not new letters, so despite the large counts they do not cover as many writing systems as they seem to.

The distribution difference between the two tables is also easy to see:

TableLatinGreek and CyrillicMiddle Eastern and IndicKana and CJKSymbols and private use
AGLabout 900 entriesabout 440 entriesabout 1150 entriesabout 600 entriesabout 1030 entries
Dingbats0000201 numbered symbols, 1 space, 1 .notdef

AGL's coverage is the result of "accretion by historical standard": first Latin and Greek for Western font interchange, then Indic and kana for CID fonts and Asian languages, finally symbols for mathematical typesetting. Dingbats is merely the private list of one class of font.

afii Numbered Names: A Category That Cannot Be Derived

There are 246 entries with the afii prefix, and in the table they are pure hand data: afii10017 is the Cyrillic letter A (U+0410), afii08941 is the lira sign (U+20A4), afii00208 is a horizontal bar; there is no derivable relation between the number and the code point. These names are the classic evidence that a table lookup is unavoidable.

Reading the Table, Part One: Latin and Diacritics

The first few hundred entries of AGL are almost all Latin letter and diacritic combinations. Centring on A (excerpt):

t.A = 0x0041;
t.AE = 0x00c6;
t.AEacute = 0x01fc;
t.AEmacron = 0x01e2;
t.AEsmall = 0xf7e6;
t.Aacute = 0x00c1;
t.Aacutesmall = 0xf7e1;
t.Abreve = 0x0102;
t.Abreveacute = 0x1eae;
t.Abrevecyrillic = 0x04d0;
t.Abrevedotbelow = 0x1eb6;
t.Abrevegrave = 0x1eb0;
t.Abrevehookabove = 0x1eb2;
t.Abrevetilde = 0x1eb4;
t.Acaron = 0x01cd;
t.Acircle = 0x24b6;

These lines show four combination dimensions:

DimensionExamplesBlock of the code point
Single diacriticAacute, Agrave, AtildeLatin-1 Supplement
Stacked diacriticsAbreveacute, AcircumflexhookaboveLatin Extended Additional
Non-Latin adaptationAbrevecyrillic, AdieresiscyrillicCyrillic block
Style or decorationAcircle, Asmall, Amonospaceenclosed letters block, private use area, fullwidth block

The last row is worth noting: Acircle = 0x24b6 is a circled A, and Amonospace = 0xff21 is a fullwidth A. They both look like "variants of A", but Unicode treats them as independent characters. Glyph names therefore have to distinguish down to this level; otherwise the text layer would copy a fullwidth A as a halfwidth A and the typographic information would be lost.

The Abreve family also explains why the table must have more than 4000 entries: five diacritic combinations (acute, grave, hookabove, tilde, dotbelow) times a dozen or so base letters, times upper and lower case, is a Cartesian product of several hundred entries. All these combinations have their own code points in Unicode, so every one must be registered separately; there is no shortcut.

Reading the Table, Part Two: Greek Letters Sharing Names with Math Symbols

The Greek section has an easy trap: several names point by default at mathematical symbols, not Greek letters (excerpt):

t.Alpha = 0x0391;
t.Alphatonos = 0x0386;
t.Beta = 0x0392;
t.Delta = 0x2206;
t.Deltagreek = 0x0394;
t.Gamma = 0x0393;
t.Gammaafrican = 0x0194;
t.Omega = 0x2126;
t.Omegacyrillic = 0x0460;
t.Omegagreek = 0x03a9;
t.Omegatonos = 0x038f;
t.Upsilon = 0x03a5;
t.Upsilon1 = 0x03d2;
t.Upsilondieresis = 0x03ab;

Delta is U+2206 (the increment sign), and only Deltagreek is U+0394 (Greek capital delta). Omega is U+2126 (the ohm sign), and only Omegagreek is U+03A9. This is AGL's historical baggage: early fonts used these two glyphs as mathematical symbols, the names stuck, and entries with a greek suffix were added later to mean the actual letters.

NameCode pointActual characterUse
Delta0x2206incrementmathematical typesetting
Deltagreek0x0394Greek capital deltaGreek text
Omega0x2126ohmphysics and engineering
Omegagreek0x03A9Greek capital omegaGreek text
Upsilon10x03D2upsilon with hookGreek text and the Symbol font

This kind of "same name, different meaning" is the most common source of lookup errors. A text layer that looks only at the name and not at the font context copies a mathematical increment as a Greek letter, or the other way round. PDF.js does not correct semantics: the table gives what it gives, and whether that is sensible is the font author's and the upper layer's responsibility.

Reading the Table, Part Three: Hebrew and Arabic Shape Families

The Hebrew and Arabic entries best express "one letter, many shapes" (excerpt):

t.alef = 0x05d0;
t.alefarabic = 0x0627;
t.alefdageshhebrew = 0xfb30;
t.aleffinalarabic = 0xfe8e;
t.alefhamzaabovearabic = 0x0623;
t.alefhamzaabovefinalarabic = 0xfe84;
t.alefhamzabelowarabic = 0x0625;
t.alefhebrew = 0x05d0;
t.aleflamedhebrew = 0xfb4f;
t.alefmaddaabovearabic = 0x0622;
t.alefmaksuraarabic = 0x0649;
t.alefpatahhebrew = 0xfb2e;
t.alefqamatshebrew = 0xfb2f;

Three things can be read from this:

PhenomenonExamplesNote
Same name across scriptsalef and alefarabicHebrew alef is U+05D0, Arabic alef is U+0627
Same name, same code pointalef and alefhebrewboth names point at the same Hebrew letter
Shape suffixes...finalarabic, ...medialarabicfinal and medial shapes, in the presentation forms block
Combinations and vocalisationalefdageshhebrew, alefpatahhebrewcombinations with dagesh or vowel marks, after U+FB1D

alef and alefhebrew sharing a value shows that AGL allows aliases. A forward lookup hits both names; a reverse lookup via for...in returns alef first because it comes earlier in the source. This is another instance of the "many names, one code point" pattern mentioned earlier, except that here the two names are semantically equivalent and it does not matter which one the reverse lookup picks.

The Arabic part writes "shape" into the name even more explicitly:

t.ainarabic = 0x0639;
t.ainfinalarabic = 0xfeca;
t.aininitialarabic = 0xfecb;
t.ainmedialarabic = 0xfecc;
t.alefmaksurafinalarabic = 0xfef0;
t.alefmaksurainitialarabic = 0xfef3;
t.alefmaksuramedialarabic = 0xfef4;

Four names correspond to four code points spread across two blocks. If a font provides only one shape, the other three names can still be looked up but cannot be drawn — the classic case of "the name table is complete, the glyphs need not be". The table can answer "which character this name should be"; it cannot answer "whether this font has this glyph".

Reading the Table, Part Four: Indic Scripts, Kana and Bopomofo

Indic scripts are ordered as "vowel letter plus script-name suffix" (excerpt):

t.abengali = 0x0985;
t.adeva = 0x0905;
t.agujarati = 0x0a85;
t.agurmukhi = 0x0a05;

All four names are "a", and the code points fall in the Bengali, Devanagari, Gujarati and Gurmukhi blocks respectively. The name structure is identical, the code points follow no pattern, and every entry has to be registered one by one. There are 344 entries of this kind, covering U+0900 to U+0DFF.

Kana and Bopomofo show four dimensions: fullwidth, halfwidth, hiragana and katakana (excerpt):

t.ahiragana = 0x3042;
t.akatakana = 0x30a2;
t.akatakanahalfwidth = 0xff71;
t.aibopomofo = 0x311e;
NameCode pointForm
ahiragana0x3042hiragana a
akatakana0x30a2katakana a
akatakanahalfwidth0xff71halfwidth katakana
aibopomofo0x311ebopomofo symbol

The same sound has four spellings, each an independent character. If a Japanese PDF has a font that embeds only halfwidth katakana, the text layer still needs to copy the halfwidth form; the table provides the "name to code point" correspondence, and which form is actually used is decided by the font's encoding.

Reading the Table, Part Five: Symbols, Arrows and Mathematics

Names in the symbol section usually describe the shape rather than a pronunciation (excerpt):

t.arrowboth = 0x2194;
t.arrowdblboth = 0x21d4;
t.arrowdblleft = 0x21d0;
t.arrowdown = 0x2193;
t.arrowdownwhite = 0x21e9;
t.arrowleft = 0x2190;
t.integral = 0x222b;
t.integralbottom = 0x2321;
t.integraltp = 0x2320;
t.circle = 0x25cb;
t.circleplus = 0x2295;
t.filledbox = 0x25a0;

What these names share is that they are named by visual features: arrowdblleft is a double-line left arrow, integralbottom is the lower half of an integral sign, circleplus is a circled plus. There is no semantic relation between name and character; only the table helps.

Local patches are also most concentrated in this section. There is one commented local extension:

t.circlecopyrt = 0x00a9; // Glyph is missing from Adobe's original list.

The comment states outright that "the glyph is missing from Adobe's original list". This is one of the few places where the authors deliberately deviate from upstream. It illustrates the maintenance principle: copy AGL by default, and only append when a real file requires a name that must be supported, leaving a comment to explain why.

ZapfDingbats: Numbered Names and Symbol Blocks

The second table starts in a completely different style (excerpt):

const getDingbatsGlyphsUnicode = getLookupTableFactory(function (t) {
  t.space = 0x0020;
  t.a1 = 0x2701;
  t.a2 = 0x2702;
  t.a202 = 0x2703;
  t.a3 = 0x2704;
  t.a4 = 0x260e;
  t.a5 = 0x2706;
  t.a119 = 0x2707;
  t.a118 = 0x2708;
  t.a117 = 0x2709;
  t.a11 = 0x261b;
  // ...
  t.a191 = 0x27be;
  // ...
  t.a89 = 0x2768; // 0xF8D7
  t.a90 = 0x2769; // 0xF8D8
  t.a93 = 0x276a; // 0xF8D9
  t.a94 = 0x276b; // 0xF8DA
  // ...
  t.a95 = 0x2774; // 0xF8E3
  t.a96 = 0x2775; // 0xF8E4
  t['.notdef'] = 0x0000;
});

The characteristics of this table:

CharacteristicBehaviourReason
Name shapea plus a number, numbered from a1 to a206, with 5 numbers missingnames come from the ZapfDingbats font's glyph list, not from Unicode names
Orderneither name-sorted nor code-point-sortedpreserves the order of Adobe's original list
Main code point block174 of the 203 entries fall in U+2700 to U+27BFUnicode's Dingbats block is encoded according to this symbol set
Outlying entries29 fall outside that blockincluding .notdef at 0, the space at 0x20, and U+2605 black star, U+25A0 square, U+2660 to U+2666 card suits, U+2460 to U+2469 circled digits, U+2192 and U+2194 arrows
Duplicate values0every name maps to a unique code point
Trailing notes14 entries carry comments such as // 0xF8D7preserve the old private-use value of the symbol in the original font for comparison

The trailing-note group best explains where this table comes from: the whole table has 14 entries with a trailing note, namely a85 to a96 at the end plus a205 and a206. They originally fell in the private use area U+F8D7 to U+F8E4, and Unicode later included them in U+2768 to U+2775, so the table writes the new code point and keeps the old one in a comment. This preserves the old-new comparison and avoids emitting a private-use value as the final result.

Non-Contiguous Numbers, and an Order That Ignores the Numbers

Names are numbered from a1 to a206, but there are actually only 201 entries; the missing numbers are a80, a113, a114, a115 and a116. Listing the order at the head of the table:

PositionNameCode point
2nd entrya10x2701
3rd entrya20x2702
4th entrya2020x2703
5th entrya30x2704
6th entrya40x260E
7th entrya50x2706
8th entrya1190x2707
9th entrya1180x2708
10th entrya1170x2709
11th entrya110x261B

Three counter-intuitive points:

  1. a202 sits between a2 and a3. Its code point 0x2703 is the lower blade scissors, and the number 202 has nothing to do with the position; the order comes from Adobe's original list.
  2. a4 has the code point 0x260E (telephone), jumping out of the U+2700 block. It is the 4th symbol in the original list, but Unicode files the symbol itself in the Misc Symbols block, so the name number and the block membership are unrelated.
  3. a117, a118 and a119 appear in the opposite order to their numbers, and their code points decrease as well. They are U+2709 (envelope), U+2708 (airplane) and U+2707 (paper tape) in that order, exactly as in the original list.

The conclusion: the number in the name is only a glyph index, not a code point and not a sort key. Any attempt to "generate code points by numeric order" will go wrong; the table must be registered entry by entry.

Comparing the Two Tables

DimensionAGL (getGlyphsUnicode)ZapfDingbats (getDingbatsGlyphsUnicode)
CoverageLatin, Greek, Cyrillic, Armenian, Hebrew, Arabic, Indic, Thai, kana, bopomofo, Hangul compatibility letters, CJK compatibility area, mathematical symbolsone symbol font, ZapfDingbats, with 201 a-numbered glyphs
Name sourcethe Adobe Glyph List, plus afii numbers and TeX additionsthe ZapfDingbats font's glyph list
Name shapemixed-case letter names, small suffix, afiiNNNNN, math namesa plus a number
Entries4328 (4211 alphabetical entries plus .notdef plus 116 TeX entries)203 (space plus 201 a-numbered names plus .notdef)
Source sizetable body about 113 KiB (115,699 bytes)table body about 3.9 KiB (3,972 bytes)
Code point rangeU+0000 to U+FFE6U+0020 to U+27BE
Main usegeneral text semantics, system-font fallback, Type1 and CFF glyph name resolutionused only by the ZapfDingbats font
Consumersfonts.js, evaluator.js, font_renderer.js, standard_fonts.js, fonts_utils.jsthe /Dingbats/i branch of fonts.js

Why Not Merge Them into One Table

Both tables contain space, both with the value 0x0020, so merging would not conflict. But merging would bring two problems:

  1. Polluted queries: AGL has no a4, while in Dingbats a4 is U+260E. After merging, any path that queries a4 with AGL would suddenly hit a telephone symbol. A font encoding containing a4 should not hit anything in the first place; hitting something actually hides the fact that "this font is not Dingbats".
  2. Loss of choice: which table to use is not decided by the name but by the font name. fonts.js routes explicitly by font name:
    } else if (/Symbol/i.test(fontName)) {
      // The non-embedded SymbolMT font in issue 21523 uses Identity encoding
      // and an Identity CIDToGIDMap, hence its CIDs are glyph ids.
      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
      );
    } else if (isStandardFont || isMappedToStandardFont) {

The choice of table and the choice of encoding table happen in the same branch: SymbolSetEncoding goes with getGlyphsUnicode(), and ZapfDingbatsEncoding goes with getDingbatsGlyphsUnicode(). The two must be paired, otherwise the name space given by the encoding table and that of the glyph table do not line up. Merging the tables would make "pairing" invisible and errors harder to spot.

The Symbol pairing also shows something else: all its names come from AGL, and no third table is needed:

SymbolSetEncoding nameCode point in AGLLater normalisation
universal0x2200unchanged
existential0x2203unchanged
suchthat0x220Bunchanged
theta10x03D1unchanged
Upsilon10x03D2unchanged
arrowvertex0xF8E60x23D0
copyrightserif0xF6D90x00A9
registerserif0xF6DA0x00AE

The private-use values in the table (arrowvertex at U+F8E6, copyrightserif at U+F6D9) are replaced with proper code points by mapSpecialUnicodeValues in unicode.js. The glyph table gives "the mapping Adobe had back then"; normalisation is another layer's job. If copyrightserif were written as 0x00A9 directly in glyphlist.js to "look more correct", the original correspondence would be lost and there would be nothing to check against when verifying upstream data later.

Design note: The name space is split by source, not merged because "they are all strings". a1 is a symbol in Dingbats and nothing in any other font; keeping the two tables separate encodes the notion of "font family" into the module structure.

Building the Table: Lazy Loading, a Null Prototype and Entry-by-Entry Assignment

The Factory Does Only Three Things

The factory in core_utils.js is only a dozen lines:

function getLookupTableFactory(initializer, useArray = false) {
  let lookup;
  return function () {
    if (initializer) {
      lookup = useArray ? [] : Object.create(null);
      initializer(lookup);
      initializer = null;
    }
    return lookup;
  };
}

Three things: create the container on the first call, run the initializer, and clear initializer. Every later call returns the same lookup directly. Setting initializer = null is not just a guard against running twice; it also cuts the closure's reference to the initializer so that function object can be collected. The data stays in lookup; the function shell is no longer needed.

The second parameter useArray is unused in this file, and the default false goes through Object.create(null). It exists because other places need an array form: getSymbolGlyphIdEncoding in fonts.js passes /* useArray = */ true to build an array indexed by glyph id.

const getSymbolGlyphIdEncoding = getLookupTableFactory(t => {
  let glyphId = 3;
  for (const [firstCharCode, lastCharCode] of [
    [0x20, 0x7e],
    [0xa1, 0xfe],
  ]) {
    for (let charCode = firstCharCode; charCode <= lastCharCode; charCode++) {
      t[glyphId++] = SymbolSetEncoding[charCode];
    }
  }
}, /* useArray = */ true);

One factory, two containers: the glyph table needs string keys, so it uses a null-prototype object; the Symbol glyph-id table needs consecutive integer keys, so it uses an array. This is exactly the value of parameterising the factory, and it shows that glyphlist.js choosing an object is not arbitrary but decided by the key type.

Why It Must Be a Null-Prototype Object

Object.create(null) has no Object.prototype, so inherited names such as constructor, toString, hasOwnProperty and __proto__ never show up in lookup results. On a glyph table this is a hard requirement, because the keys come from PDF files: names in a /Differences array are filled in freely by the generator and could perfectly well be constructor.

If the table were an ordinary object literal, glyphsUnicodeMap["constructor"] would return the Object constructor, a truthy value. The caller would then run String.fromCharCode(Object) and get garbage such as "function Object() { [native code] }", with no error at all. A null prototype turns this kind of false hit into undefined, keeping "not found" honest.

Likewise, writing t.__proto__ = 0x1234 on an ordinary object tries to change the prototype rather than add a property; on a null-prototype object __proto__ is just an ordinary data property and the assignment behaves as expected. Although this file's tables contain no glyph named __proto__, font data is untrusted, and the container has to fill this hole first.

The limitations of a null prototype should also be stated: the table has no inherited methods such as hasOwnProperty or toString, so callers can only use prototype-independent means such as in, for...in and Object.keys. All consumers in this file use direct subscript access; the only place that needs to walk the whole table is the reverse lookup in recoverGlyphName, which uses for...in and is unaffected.

Why Not a Map, and Why Not an Array

Five candidate structures compared side by side:

StructureLookup by nameHow it is builtMemoryKeys and order
Plain object literalproperty lookupallocated once at module evaluationmediumstrings, subject to prototype pollution
Object.create(null)property lookupentry-by-entry assignment in the initializermediumstrings, no prototype
Mapgetentry-by-entry sethigher, each entry has extra overheadany type, insertion order
Arraynot applicableneeds preallocation by indexdepends on sparsitynumeric indices
Flat arrayneeds conversion to an object or a linear scandoubles the peak during conversionhighmixed

There are three reasons for choosing a null-prototype object:

  1. The keys are strings, and mostly look like identifiers. Aacute, uni0041 and afii10017 can all be property names directly, and V8 has dedicated optimisations for this kind of access. Map.get needs a method call and a hash lookup, slightly slower on the hot path.
  2. Building can be one assignment at a time. t.A = 0x0041 is a property write; Map needs t.set("A", 0x0041), an extra call layer. 4328 entries mean 4328 call differences, small but with no reason to pick the slower option.
  3. None of Map's extra capabilities are needed. The table does not need "any-type keys", size, "iteration order preserved after deletion" or identity comparison by object reference. Unused features only add memory.

An array is entirely unsuitable: the table's primary key is a name, not a consecutive integer. Using an array means either hashing names into numbers or scanning linearly. The useArray option of getLookupTableFactory exists because getSymbolGlyphIdEncoding happens to have consecutive glyph ids as keys; that is another scenario, not this table.

This version also does not freeze the table: glyphlist.js contains no Object.freeze, and getLookupTableFactory does not freeze its result. The table is never exposed directly; callers can only obtain a reference through the factory, and there is no write path in the core code, so the protection freezing would offer is limited while costing an extra pass over 4000 keys during construction. This is the trade-off of "convention" instead of "runtime enforcement": read-only is guaranteed by usage, not by object state.

The Boundary with encodings.js: Arrays and Dictionaries

encodings.js stores "character code to name", glyphlist.js stores "name to code point". The directions are opposite, and so are the containers:

// prettier-ignore
const SymbolSetEncoding = [
  "", "", "", "", "", "", "", "", "", "", "", "", "", "",
  "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
  "space", "exclam", "universal", "numbersign", "existential", "percent",
  "ampersand", "suchthat", "parenleft", "parenright", "asteriskmath", "plus",
  "comma", "minus", "period", "slash", "zero", "one", "two", "three", "four",
  "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less",
Dimensionencodings.jsglyphlist.js
Keysintegers from 0 to 255arbitrary glyph name strings
Containerordinary array, the index is the character codenull-prototype object, the key is the name
Size256 entries per table, seven tables in total4328 entries and 203 entries
Loadingbuilt directly at module evaluationlazily loaded through getLookupTableFactory
Empty valuethe empty string ""undefined (the key simply does not exist)
Lookuparr[charCode]obj[glyphName]

All three differences have reasons. The keys are consecutive integers, so an array is most natural and there is no prototype pollution; 256 entries are small, so building at module evaluation is no noticeable burden; empty slots are filled with "", so the array has no holes and the behaviour of slice and for...in is predictable — the encoding table needs whole-table copies (defaultEncoding.slice()), and holes would make the copy contain non-enumerable positions.

glyphlist.js is the opposite: string keys, large size, and only needed on demand, so it uses a lazily built dictionary. Consumers must also handle empty values according to their own conventions: _simpleFontToUnicode checks glyphName === "" and skips, while an AGL lookup checks unicode === undefined. The two conventions cannot be mixed, and this is the most confusing part when reading these two modules.

Design note: The shape of the container is decided by the shape of the key. Consecutive small integers use an array, sparse strings use a null-prototype dictionary; this is not a style preference but makes "not found" explicit at the data-structure level, so callers need no extra check.

Entry-by-Entry Assignment Instead of an Object Literal

The table body is written as one assignment per line:

t.A = 0x0041;
t.AE = 0x00c6;
t.AEacute = 0x01fc;
t.AEmacron = 0x01e2;
t.AEsmall = 0xf7e6;

Why not one big object literal, such as const map = { A: 0x0041, ... }? There are three direct reasons:

ReasonExplanation
Lazy loadingan object literal is allocated and filled at module evaluation; entry-by-entry assignment inside the initializer defers the cost until the first lookup
Key validity.notdef contains a dot and cannot be written as t..notdef; only t[".notdef"] works. A literal would need quotes too, so both forms need special handling
Generation and checkingone name per line is the easiest way to compare line by line with Adobe's original list; a difference also affects only one line

The third is the most important for maintenance. This file is essentially a data list translated into JavaScript, and the readability target is "like a data file", not "like a program". One name per line with hexadecimal code points matching the AGL source (0041 rather than 65) lets people diff text directly against upstream updates.

Only two entries have a dotted name, both .notdef, written the same way:

t['.notdef'] = 0x0000;

It appears at the end of the AGL body (before the TeX section) and at the end of the Dingbats table. 0x0000 is not "missing" but an explicit mapping; the semantic ambiguity it creates is discussed separately later.

Startup Cost and Memory Footprint

The cost of this table is concentrated in the first call:

StageHow oftenCost
Module evaluationonce per Workeronly creates two closures, builds no table
First getGlyphsUnicode()once per Workerruns 4328 assignments, allocates an object with 4328 keys
Later callsevery lookupreturns the same reference, constant time
Single queryevery glyphone string-keyed property lookup

An object with 4328 string keys goes into dictionary mode in V8 once it exceeds a certain property count, and each entry carries hash-table overhead. The key strings themselves come from string constants in the module and are already interned, so building the table does not copy them again; values are small integers. The overall magnitude is "within a few hundred KB", negligible compared with font data (a CJK font easily runs to several MB), but it is a fixed cost per Worker.

This also explains why lazy loading is mandatory: opening a pure-image PDF or a pure-CMap document may never need AGL at all. Deferring the 4328 assignments until first use avoids paying for unused functionality. Conversely, if a document has dozens of fonts, after the first lookup it is pure reading, and the cost is amortised to nothing.

The cost of a lookup itself can be estimated as well:

OperationMagnitudeNote
Single lookup1 property accessstring-key lookup on a dictionary-mode object
One table build4328 assignmentshappens only once per Worker
Building toFontChar for one font256 lookups plus difference entriesonce per simple font
Text extraction for a whole documentone per characterone per character

A 100-page document with 2000 characters per page triggers roughly 200,000 lookups for a full copy. 200,000 hash lookups is milliseconds; the real cost is still the table build. This also explains why all optimisation focuses on "when to build the table" rather than "how to make lookups faster" — there is little room left in the latter.

The Table Is Bundled into the Worker

glyphlist.js lives in src/core/ and is bundled together with pdf.worker.js, so its size directly affects the download, parse and compile time of the Worker script:

ItemImpact
Bundling locationgoes into pdf.worker.js, not into the main-thread bundle
Syntax parsingover 4500 lines and 4531 assignment statements; parsing cost exists but is far below the first execution
Transfer compressionthe repeated t. prefix and hexadecimal literals are gzip-friendly, so the actual compression ratio is fairly high
Tree shakingboth factories are referenced by several core modules, so they cannot be shaken out
Splittingsplitting into a separate chunk needs extra load ordering and only adds complexity

Questions such as the compressed transfer size, parse time and "how much the TeX section could save" cannot be answered by feel; they need measurement on a real build output. The only certainty is that the size of this table is fixed and independent of the document: a 1-page invoice and a 1000-page manual pay exactly the same build cost.

Design note: The first principle of a global constant table is not "lookups are fast" but "pay nothing if you never look up". A lookup itself is O(1); what is truly expensive is moving more than 4000 entries onto the heap. Putting that move after the first access makes documents that never use the font layer completely immune to the cost.

Version Difference: The 2020 Flat-Array Experiment

The current version assigns entry by entry, but in October 2020 it was changed to a flat array. That change introduced a new factory:

import { getArrayLookupTableFactory } from './core_utils.js';
 
var getGlyphsUnicode = getArrayLookupTableFactory(function () {
  // prettier-ignore
  return [
    "A", 0x0041,
    "AE", 0x00c6,
    "AEacute", 0x01fc,
    "AEmacron", 0x01e2,
    "AEsmall", 0xf7e6,
    // ...
  ];
});

The factory converted the paired array into an object:

function getArrayLookupTableFactory(initializer) {
  let lookup;
  return function () {
    if (initializer) {
      let arr = initializer();
      initializer = null;
      lookup = Object.create(null);
      for (let i = 0, ii = arr.length; i < ii; i += 2) {
        lookup[arr[i]] = arr[i + 1];
      }
      arr = null;
    }
    // ...

The cost of that version is easy to see:

ItemEntry-by-entry assignmentFlat array
Source sizeone name plus one code point per lineeach name and code point takes one array element, halving the line count
First buildwrites directly into the target objectallocates an array first, then walks half its length writing into an object, and finally discards the array
Peak memoryone objectone array plus one object
Readabilityone record per linetwo aligned columns, like CSV
Formattingnormal formattingrequires // prettier-ignore, or the array gets wrapped

In November 2020 the change was reverted wholesale, and the commit message gave the reason that SystemJS had already been removed. In other words, the flat array mainly served the module loading method of the time and had nothing to do with lookup performance. After the revert getArrayLookupTableFactory disappeared from core_utils.js, and it can no longer be found in the current source.

The practical point of this history for reading the source is: do not assume that a data table must go through runtime parsing. The current version has no separators, no splitting and no traversal conversion, only one initializer call. All the imagination about "compact strings plus separator parsing" belongs to other versions or other projects.

Maintenance: How Many Places a New Name Touches

This table has no generator script and no validator; adding an entry is entirely manual:

StepActionReason
1First check whether Adobe's official AGL already has ita self-invented name cannot be aligned with other readers
2Decide whether it goes into AGL or Dingbatsdecides whether consumers can hit it
3Upstream entries are inserted alphabetically; local patches go nearby or are appended at the endkeep upstream comparable
4Values are written in lowercase four-digit hexadecimalminimal diff noise
5Call getUnicodeForGlyph manually to verifyconfirm the parse result directly
6Add an assertion in test/unit/unicode_spec.jsprevent a later regression

Step five can be tested by hand like this:

// in a unit test or debug environment
const map = getGlyphsUnicode();
getUnicodeForGlyph('f_f_i', map); // 0xfb03
getUnicodeForGlyph('circlecopyrt', map); // 0x00a9

The unit test for step six already exists and covers both tables and uniXXXX parsing:

it('should get Unicode values for valid glyph names', function () {
  expect(getUnicodeForGlyph('A', standardMap)).toEqual(0x0041);
  expect(getUnicodeForGlyph('a1', dingbatsMap)).toEqual(0x2701);
});
 
it('should recover Unicode values from uniXXXX/uXXXX{XX} glyph names', function () {
  expect(getUnicodeForGlyph('uni0041', standardMap)).toEqual(0x0041);
  expect(getUnicodeForGlyph('u0041', standardMap)).toEqual(0x0041);
 
  expect(getUnicodeForGlyph('uni2701', dingbatsMap)).toEqual(0x2701);
  expect(getUnicodeForGlyph('u2701', dingbatsMap)).toEqual(0x2701);
});
 
it('should not get Unicode values for invalid glyph names', function () {
  expect(getUnicodeForGlyph('Qwerty', standardMap)).toEqual(-1);
  expect(getUnicodeForGlyph('Qwerty', dingbatsMap)).toEqual(-1);
});

The third group of assertions pins down the -1 sentinel: Qwerty starts with an uppercase Q, does not enter the u branch, and is found in neither table, so it returns -1.

Note the second group: uni2701 and u2701 are not in the Dingbats table; they get 0x2701 from the prefix parsing in getUnicodeForGlyph. The test verifies both the "table" and the "parsing" layers at once.

The risks of manual maintenance are concentrated in four things:

RiskConsequenceCurrent defence
Duplicate keysthe later value silently overwrites the earlier oneno automatic check, only a global search for the name
Insertion positionchanges for...in order and therefore which entry a reverse lookup pickspatches are placed next to the relevant letter section, and the TeX section is kept at the end
Wrong casebecomes another glyph: found but semantically wrongstrictly copy the upstream naming
Inconsistent radix or digit countvalues still correct, but noisy diffsthe code style standardises on lowercase four-digit hex

The first is the most insidious: assigning the same property twice in a JavaScript object does not raise an error, and t.X = 1; t.X = 2 results in 2. This file relies on manual care, with no CI deduplication. If you suspect a name has two entries while reading the source, the most direct way is to search for t. followed by the name.

Patterns in the Data

Upper and Lower Case in Parallel, with No Normalisation

In AGL, upper and lower case are two independent sets of names with their own code points:

t.A = 0x0041;
// ...
t.a = 0x0061;
t.aacute = 0x00e1;
// ...
t.Aacute = 0x00c1;
// ...
t.Alpha = 0x0391;
t.alpha = 0x03b1;

Lookups are case-sensitive: A and a are two keys, and so are Aacute and aacute. PDF glyph names are themselves case-sensitive, which is specified behaviour, not an implementation oversight. If someone writes Aacute as aacute, that is a different glyph (lowercase a with an accent), and the table happens to contain it too, so the result is "found but the meaning changed". The table cannot detect this kind of error; only the upstream encoding table can guarantee the names are correct.

small Variants and the Private Use Area

There are 100 names with the small suffix, 99 of which have values in the private use area; the typical pattern maps small capitals into the PUA:

t.AEsmall = 0xf7e6;
t.Aacutesmall = 0xf7e1;
t.Acircumflexsmall = 0xf7e2;
t.Acutesmall = 0xf7b4;
t.Zsmall = 0xf77a;
t.Zcaronsmall = 0xf6ff;

These names come from Adobe's Expert character set. ExpertCharset in charsets.js lists names such as Asmall, Bsmall, Csmall, Dsmall, Esmall, Fsmall and Gsmall in order, matching the entries in the table exactly. In other words, the run of small names in the table is not an arbitrary set of aliases; it exists so that the built-in charsets of Expert fonts can be resolved.

Private-use values have to be read together with unicode.js:

function mapSpecialUnicodeValues(code) {
  if (code >= 0xfff0 && code <= 0xffff) {
    // Specials unicode block.
    return 0;
  } else if (code >= 0xf600 && code <= 0xf8ff) {
    return getSpecialPUASymbols()[code] || code;
  } else if (code === /* softhyphen = */ 0x00ad) {
    return 0x002d; // hyphen
  }
  return code;
}

Values in the private use area from U+F600 to U+F8FF first pass a 27-entry whitelist that replaces a few symbols with proper code points (for example parenlefttp at U+F8EB becomes U+239B); values outside the whitelist are returned unchanged. So the U+F7E1 found for Aacutesmall survives all the way to the text layer, neither cleared to 0 nor automatically turned into A. This is another example of "the table gives the original mapping, normalisation has its own table".

Design note: The data table faithfully preserves the upstream private-use mappings and does not perform "more sensible-looking" substitutions here. PUA semantics can only be judged with a dedicated whitelist; putting that in glyphlist.js would mix policy into a pure data table and make the whitelist hard to update independently.

Many Names, One Code Point

Within one table it is very common for several names to map to the same code point:

NamesCode pointRelationship
Dcroat0x0110exactly the same value as Dslash
Dslash0x0110same as above
Zdot0x017bsame value as Zdotaccent
Zdotaccent0x017bsame as above
radical0x221asame value as radicalbig and radicalBig in the TeX section
radicalbig0x221aTeX section, size variants share the radical code point
parenleftBig0x0028four sizes in the TeX section share the left parenthesis code point
parenleftbigg0x0028same as above
space0x0020same value as spacehackarabic

Forward lookups are unambiguous: each name hits independently. Ambiguity arises only in the reverse direction — given a code point and wanting a standard name, several candidates are valid. recoverGlyphName in fonts_utils.js uses "walk the object and return the first key whose value matches":

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;
}

Since for...in order is insertion order, and insertion order is source order, the reverse result is the first matching key in source order. 0x017b returns Zdot (line 737) rather than Zdotaccent (line 738); 0x221a returns radical (line 3183) rather than the TeX variants at the end of the file. This is not random, but it is not curated either; it is simply "whoever is written first wins".

With that understood, the behaviour of recoverGlyphName becomes predictable: the name it recovers is not guaranteed to be the most common typographic one, only to have the same code point. That is enough for callers, because the subsequent glyphNames.indexOf and cmap queries only care whether the code point and the standard name exist in the font.

The Ambiguity of .notdef and 0x0000

Both tables have this entry: in AGL it sits at line 4230, before the TeX section; in Dingbats it is the last line.

t['.notdef'] = 0x0000;

0x0000 is a legal Unicode code point (NUL), so this mapping makes .notdef a name that "is found". That creates an ambiguity the caller must handle:

QueryThe table's answerSemantics
A name that does not existundefinednot found
.notdef0found, but the code point is NUL
A real space0x0020found, the code point is a space

Looking only at the return value, 0 and "not found" cannot be confused (the former is the number 0, the latter is undefined); but if the caller treats 0 as "no character", it will misjudge. The choice in evaluator.js is to skip it explicitly before the lookup:

for (const charcode in differences) {
  const glyphName = differences[charcode];
  if (glyphName === '.notdef') {
    // Skip .notdef to prevent rendering errors, e.g. boxes appearing
    // where there should be spaces (fixes issue5256.pdf).
    continue;
  }
  encoding[charcode] = glyphName;
}

fonts.js also has a dedicated check for .notdef, used to avoid giving an empty glyph a visible width. These treatments live on the consumer side rather than removing .notdef from the table. The reason for keeping it: some fonts really do need to know "this character code corresponds to notdef", and 0 happens to be the position of .notdef in glyph ids (glyph 0).

Design note: A sentinel must be chosen so that it can never be a legal result. The lookup function uses undefined for "absent" and -1 for "parse failure", precisely because 0 is already a legal code point; the presence of .notdef nails down the fact that "0 is a legal result", so any scheme using 0 for failure is wrong.

The TeX Section: 116 Entries Appended at the End of AGL

The body of the AGL table does not end after .notdef; a commented group follows (excerpt):

// TeX-specific glyph names.
t.angbracketleftbig = 0x2329;
t.angbracketleftBig = 0x2329;
t.angbracketleftbigg = 0x2329;
t.angbracketleftBigg = 0x2329;
t.angbracketrightBig = 0x232a;
t.angbracketrightbig = 0x232a;
t.angbracketrightBigg = 0x232a;
t.angbracketrightbigg = 0x232a;
t.arrowhookleft = 0x21aa;
t.arrowhookright = 0x21a9;
t.arrowlefttophalf = 0x21bc;
t.arrowleftbothalf = 0x21bd;
t.backslashbig = 0x2216;
t.backslashBig = 0x2216;
t.backslashBigg = 0x2216;
t.backslashbigg = 0x2216;
t.radicalvertex = 0x221a;
t.summationdisplay = 0x2211;
t.summationtext = 0x2211;
t.uniondisplay = 0x22c3;
t.unionmultidisplay = 0x228e;
t.vextenddouble = 0x2225;
t.vextendsingle = 0x2223;

These 116 entries were added for mathematical typesetting fonts. Their shared feature is that size variants share one code point: angbracketleftbig and angbracketleftBigg are both U+2329, and the actual display size is decided by the glyph in the font; Unicode only expresses "this is a left angle bracket". This also explains why the table has many variants differing only in case: all four spellings big, Big, bigg and Bigg must be covered, because different generators use different naming habits.

This block is appended after .notdef in the AGL table rather than merged into alphabetical order. The benefit is that the upstream AGL body stays complete and comparable, and updates make it obvious at a glance which entries are not Adobe's original data.

Non-Standard Ligatures: The Underscore Variants

Unlike the TeX section, the ligature patches are not concentrated at the end but inserted in place in the f section, right after the corresponding standard names:

Excerpt:

t.ff = 0xfb00;
t.f_f = 0xfb00;
t.ffi = 0xfb03;
t.f_f_i = 0xfb03;
t.ffl = 0xfb04;
t.f_f_l = 0xfb04;
t.fi = 0xfb01;
t.f_i = 0xfb01;
// ...
t.fl = 0xfb02;
t.f_l = 0xfb02;

The first eight entries are indeed adjacent, while fl and f_l come several dozen lines later (with the fifteen series and other entries in between). This shows that the patches are "inserted right after each standard name" rather than gathered into one block.

The underscore entries have no upstream basis. The standard AGL has only five ligature names: ff, fi, fl, ffi and ffl. The underscored f_f, f_i, f_l, f_f_i and f_f_l are products of some PDF generators that split a ligature into its component letters joined by underscores. The table maps them to the same U+FB00 to U+FB04, so text extraction needs no special case for these names.

But such patches cover only "ligatures that really have a corresponding Unicode". Combinations without an official code point, such as f_h, f_t and T_h, have no entry in the table, and evaluator.js handles them by hand with a small switch:

        default:
          // Support (some) non-standard ligatures.
          switch (glyphName) {
            case "f_h":
            case "f_t":
            case "T_h":
              toUnicode[charcode] = glyphName.replaceAll("_", "");
              continue;
          }
          break;

The same phenomenon splits into two paths: those with Unicode go into the table, those without go into the heuristics. This dividing line shows the table's role: it only accepts definite "name to code point" mappings, never guesses at "name to string". The result for f_h is two characters fh, not a code point, so it does not fit a table keyed by code points.

Who Looks Up the Table

Font Construction: Deferring Initialisation One Layer Further

type1FontGlyphMapping in fonts_utils.js maps character codes to glyph ids. Its main path only uses the index of a name in the font's built-in list and does not need Unicode at all:

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() defers lazy loading one layer further: the global table is only built when "a difference name cannot be found in the font". Three cases compared:

ConditionIs AGL built
The font has no /Differencesno
There are differences, but every name is found in glyphNamesno
At least one difference name is not found and recoverGlyphName is neededyes

This is not excessive caution. The /Differences of a Type1 font mostly correspond directly to the font's built-in names, and indexOf hits on the first try; only when a generator has changed the names, or the names come from another standard, is a fallback to Unicode reverse lookup needed. Deferring the table build into that branch means fonts whose "names all match" never touch this 4000-key table at all.

buildToFontChar: From an Encoding Array to a Code-Point Array

The most direct way to consume the table is to "translate a whole 256-entry encoding table into a code-point table":

function buildToFontChar(encoding, glyphsUnicodeMap, differences) {
  const toFontChar = [];
  let unicode;
  for (let i = 0, ii = encoding.length; i < ii; i++) {
    unicode = getUnicodeForGlyph(encoding[i], glyphsUnicodeMap);
    if (unicode !== -1) {
      toFontChar[i] = unicode;
    }
  }
  for (const charCode in differences) {
    unicode = getUnicodeForGlyph(differences[charCode], glyphsUnicodeMap);
    if (unicode !== -1) {
      toFontChar[+charCode] = unicode;
    }
  }
  return toFontChar;
}

Its output is a sparse array whose indices are character codes and whose values may be holes (skipped when not found). This shape matters: Font.charToGlyph later reads toFontChar[charcode], so a hole is undefined and naturally falls into the "no Unicode" branch without an extra sentinel.

differences overwrites the result of the encoding table in the second half. In other words, /Differences takes priority over the base encoding: for the same character code, the base encoding fills it first, and then the difference array overwrites it. This matches the semantics of the PDF specification — /Differences is the author's local correction to the base encoding.

Two ToUnicode Repair Paths

The repair functions for TrueType and Type1 are written differently, and the difference reflects "whether the names are trustworthy":

function adjustTrueTypeToUnicode(properties, isSymbolicFont, nameRecords) {
  // ...
  const encoding = WinAnsiEncoding;
 
  const toUnicode = [],
    glyphsUnicodeMap = getGlyphsUnicode();
  for (const charCode in encoding) {
    const glyphName = encoding[charCode];
    if (glyphName === '') {
      continue;
    }
    const unicode = glyphsUnicodeMap[glyphName];
    if (unicode === undefined) {
      continue;
    }
    toUnicode[charCode] = String.fromCharCode(unicode);
  }
  if (toUnicode.length > 0) {
    properties.toUnicode.amend(toUnicode);
  }
}
function adjustType1ToUnicode(properties, builtInEncoding) {
  // ...
  const toUnicode = [],
    glyphsUnicodeMap = getGlyphsUnicode();
  for (const charCode in builtInEncoding) {
    // ...
    const glyphName = builtInEncoding[charCode];
    const unicode = getUnicodeForGlyph(glyphName, glyphsUnicodeMap);
    if (unicode !== -1) {
      toUnicode[charCode] = String.fromCharCode(unicode);
    }
  }
  if (toUnicode.length > 0) {
    properties.toUnicode.amend(toUnicode);
  }
}
FunctionName sourceLookup methodReason
adjustTrueTypeToUnicodethe fixed WinAnsiEncodingdirect subscript glyphsUnicodeMap[glyphName]the names in the encoding table are all standard names, so parsing uniXXXX is unnecessary
adjustType1ToUnicodethe font's built-in encodinggetUnicodeForGlyphbuilt-in encodings may contain non-standard names and uniXXXX forms

This is a subtle but very useful design decision: when the names are known to come from a standard table, the parsing cost need not be paid. A direct subscript does one property lookup; getUnicodeForGlyph additionally does a prefix test and parseInt after a miss. The names on the TrueType path come from the WinAnsiEncoding hard-coded in the source, so uni0041 cannot appear, and skipping parsing is safe.

System Font Fallback: Choosing the Table by Font Name

When a font is not embedded, Font.fallbackToSystemFont needs to assign a Unicode to each character code so the browser can draw the right character with a system font. The /Symbol/i and /Dingbats/i branches quoted earlier are this step. The third branch handles standard fonts:

    } else if (isStandardFont || isMappedToStandardFont) {
      const map = buildToFontChar(
        this.defaultEncoding,
        getGlyphsUnicode(),
        this.differences
      );

Finally there is a fallback branch that first resolves by difference array and default encoding, then overwrites with toUnicode:

    } else {
      const glyphsUnicodeMap = getGlyphsUnicode();
      const map = [];
      this.toUnicode.forEach((charCode, unicodeCharCode) => {
        if (!this.composite) {
          const glyphName =
            this.differences[charCode] || this.defaultEncoding[charCode];
          const unicode = getUnicodeForGlyph(glyphName, glyphsUnicodeMap);
          if (unicode !== -1) {
            unicodeCharCode = unicode;
          }
        }
        map[+charCode] = unicodeCharCode;
      });

Here the initial value of unicodeCharCode comes from toUnicode, but it is overwritten as soon as the glyph name resolves to a code point. The priority is the exact opposite of amendFallbackToUnicode: that function protects the author-declared /ToUnicode, while this branch lets the glyph name win. The reason is a difference in purpose — what is being chosen here is "what character the glyph drawn with a system font should display as", and the glyph name in the encoding table is closer to the glyph than /ToUnicode is; whereas amendFallbackToUnicode repairs text selection, where the author's /ToUnicode is authoritative.

The existence of both directions shows that "who has higher priority" is not a global rule but decided call site by call site: text extraction follows the author's declaration, and system font fallback follows the glyph.

font_renderer.js: seac Composite Glyphs

When the endchar of a Type2 charstring carries four arguments, it means "compose from a base character and an accent", that is, seac. Composition requires turning character codes into glyph names, then into code points, and finally finding glyph ids in the CFF cmap:

        case 14: // endchar
          if (stack.length >= 4) {
            const achar = stack.pop();
            const bchar = stack.pop();
            y = stack.pop();
            x = stack.pop();
            cmds.save();
            cmds.translate(x, y);
            let cmap = lookupCmap(
              font.cmap,
              String.fromCharCode(font.glyphNameMap[StandardEncoding[achar]])
            );
            compileCharString(
              font.glyphs[cmap.glyphId],
              cmds,
              font,
              cmap.glyphId
            );
            cmds.restore();
 
            cmap = lookupCmap(
              font.cmap,
              String.fromCharCode(font.glyphNameMap[StandardEncoding[bchar]])
            );
            compileCharString(
              font.glyphs[cmap.glyphId],
              cmds,
              font,
              cmap.glyphId
            );
          }

font.glyphNameMap is exactly the table stored in the Type2Compiled constructor:

this.glyphNameMap = getGlyphsUnicode();

This chain is worth noting: StandardEncoding[achar] gives the name, glyphNameMap gives the code point, String.fromCharCode gives the character, and lookupCmap finds the glyph id by character in the CFF cmap. None of the four steps can be skipped, because the seac arguments are "character codes in the standard encoding" while the CFF cmap is keyed by Unicode characters.

standard_fonts.js: Building a Glyph-Id-to-Unicode Map from the Name Table

Fonts with a Mac-order post table need a "glyph id to Unicode" table, while MacStandardGlyphOrdering provides "glyph id to glyph name". AGL is exactly the bridge between them:

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;
    }
  }
});

This is a "table inside a table": the outer factory calls another factory inside its own initializer. Because getGlyphsUnicode() is already memoised, this does not rebuild the table, it just takes a reference. Holes are skipped during the walk, so names in MacStandardGlyphOrdering that are not in AGL produce no entry.

The line t[2] = 10 deserves a separate explanation: glyph id 0 is .notdef, and 1 and 2 have no corresponding character in Mac order, so the code maps id 2 directly to 0x000A (line feed) and only starts walking by name table from id 3. This kind of hard-coding shows that a "glyph id to Unicode" table cannot be fully derived from names; the first few positions need to be filled by hand.

The place that really uses this table is very specific: when a standard font is embedded as a CID font, uses Identity encoding, and provides no glyph mapping, PDF.js needs to build its own cidToGidMap:

      if (/Trebuchet/i.test(name)) {
        // TrebuchetMS doesn't share the glyph ordering of the standard fonts,
        // hence using the latter would map e.g. "š" to "ž" (issue 21713).
        applyStandardFontGlyphMap(map, getGlyphMapForMacOrderedFonts());
        applyStandardFontGlyphMap(map, getSupplementalGlyphMapForTrebuchetMS());
      } else {
        applyStandardFontGlyphMap(map, getGlyphMapForStandardFonts());

Most standard fonts share the same glyph order, so getGlyphMapForStandardFonts() is enough; Trebuchet MS is the exception and its order is close to the Mac standard order, so the Mac-order table is laid down first and then its own supplemental table. The example in the comment is typical: using the wrong table maps š to ž — an offset of one code point, and the user sees a completely different letter.

The Consumer Layers and Their Timing

Rearranging the consumers above by "when they look up" shows that this table is used repeatedly but paid for only at a few moments:

ConsumerLookup timingFrequencyPurpose
type1FontGlyphMappingfont construction, and only when a difference name is not foundat most 256 per font, and mostly not triggereddifference name recovery
buildToFontCharfont construction, in the fallbackToSystemFont branch256 plus difference entries per fontsystem font fallback
adjustType1ToUnicode / adjustTrueTypeToUnicodelate font construction, repairing toUnicode256 per fontcompleting the text mapping
_simpleFontToUnicodetext extraction initialisation256 per fontmain text path
recoverGlyphNamethe TrueType 3,1 cmap branchonce per character code, returns immediately on a hitstandard name recovery
the seac case of Type2Compiledrendering, on endchar with four argumentsonce per composite glyphcomposing accent glyphs
getGlyphMapForMacOrderedFontsthe first call of the standard font tableonce per Workerbuilding the glyph id mapping

Only two paths are genuinely high-frequency: the batch of 256 lookups during font construction, and the per-character lookups during text extraction. The former happens once per font, the latter once per character, but both rest on the premise that "the table already exists". The table is built only once, and all queries afterwards are pure reads.

evaluator.js: The Complete Text Extraction Flow

_simpleFontToUnicode, the text extraction routine, is where lookups are densest and the most complete example of "the table only provides facts, policy is above". It has six steps, and the first two depend directly on AGL.

Step zero: merge the differences and skip .notdef.

const toUnicode = [];
const encoding = properties.defaultEncoding.slice();
const baseEncodingName = properties.baseEncodingName;
// Merge in the differences array.
const differences = properties.differences;
for (const charcode in differences) {
  const glyphName = differences[charcode];
  if (glyphName === '.notdef') {
    // Skip .notdef to prevent rendering errors, e.g. boxes appearing
    // where there should be spaces (fixes issue5256.pdf).
    continue;
  }
  encoding[charcode] = glyphName;
}

defaultEncoding.slice() makes a copy first to avoid polluting the font object; /Differences overwrites it; .notdef is skipped explicitly. This step does no lookup, but it decides the input to the lookups that follow. The reason for skipping .notdef is in the comment: some files use .notdef to mean a space, and looking it up as usual would yield U+0000 and put boxes in the text layer.

Steps one and two: the a and b steps required by the specification.

    const glyphsUnicodeMap = getGlyphsUnicode();
    for (const charcode in encoding) {
      // a) Map the character code to a character name.
      let glyphName = encoding[charcode];
      if (glyphName === "") {
        continue;
      }
      // b) Look up the character name in the Adobe Glyph List (see the
      //    Bibliography) to obtain the corresponding Unicode value.
      let unicode = glyphsUnicodeMap[glyphName];
      if (unicode !== undefined) {
        toUnicode[charcode] = String.fromCharCode(unicode);
        continue;
      }

An empty name is skipped, because the empty string is not in the table and continuing into the heuristics would be pointless. On a hit it goes straight to String.fromCharCode and continue, entering no branch below. This is the hot path: the vast majority of normal characters stop here.

Step three: the first-character heuristics.

This step is only reached when AGL does not find the name; it splits by the first character of the name into five categories:

      // (undocumented) c) Few heuristics to recognize unknown glyphs
      // NOTE: Adobe Reader does not do this step, but OSX Preview does
      let code = 0;
      switch (glyphName[0]) {
        case "G": // Gxx glyph
          if (glyphName.length === 3) {
            code = parseInt(glyphName.substring(1), 16);
          }
          break;
        case "g": // g00xx glyph
          if (glyphName.length === 5) {
            code = parseInt(glyphName.substring(1), 16);
          }
          break;
        case "C": // Cdd{d} glyph
        case "c": // cdd{d} glyph
          if (glyphName.length >= 3 && glyphName.length <= 4) {
            const codeStr = glyphName.substring(1);
 
            if (forceGlyphs) {
              code = parseInt(codeStr, 16);
              break;
            }
            // Normally the Cdd{d}/cdd{d} glyphName format will contain
            // regular, i.e. base 10, charCodes (see issue4550.pdf)...
            code = +codeStr;
 
            // ... however some PDF generators violate that assumption by
            // containing glyph, i.e. base 16, codes instead.
            // In that case we need to re-parse the *entire* encoding to
            // prevent broken text-selection (fixes issue9655_reduced.pdf).
            if (Number.isNaN(code) && Number.isInteger(parseInt(codeStr, 16))) {
              return this._simpleFontToUnicode(
                properties,
                /* forceGlyphs */ true
              );
            }
          }
          break;
        case "u": // 'uniXXXX'/'uXXXX{XX}' glyphs
          unicode = getUnicodeForGlyph(glyphName, glyphsUnicodeMap);
          if (unicode !== -1) {
            code = unicode;
          }
          break;
        default:
          // Support (some) non-standard ligatures.
          switch (glyphName) {
            case "f_h":
            case "f_t":
            case "T_h":
              toUnicode[charcode] = glyphName.replaceAll("_", "");
              continue;
          }
          break;
      }

The rules of the five branches can be laid out in a table:

First characterName lengthHow it is parsedExampleResult
Gexactly 3the last two digits as hexadecimalG41U+0041
gexactly 5the last four digits as hexadecimalg0041U+0041
C or c3 to 4decimal by default, hexadecimal under forceGlyphsC6565 or 0x65
udecided by getUnicodeForGlyphuniXXXX or uXXXX{XX}uni0041U+0041
anything elseanyfirst the three hand-written ligatures, then give upf_hthe string fh

The length limits for G and g are strict: G41 is exactly three characters and g0041 exactly five; one character more or less and it is not parsed. This is because these two name classes come from the fixed format of a specific generator, and the length is part of the format.

The decimal/hexadecimal ambiguity of C and c is the most troublesome. The comment explains that most generators use decimal (C65 means character code 65) while a few use hexadecimal. The code parses as decimal first; when that yields NaN it tries hexadecimal. If it really does look hexadecimal, it is not content with fixing the current character but re-parses the whole font, because other characters in the same font may be affected too. This return is the only early exit in the whole passage; the cost is one full rescan, and the benefit is that text selection is no longer misaligned.

Step four: code-point validity checks and a base-encoding recheck.

      if (code > 0 && code <= 0x10ffff && Number.isInteger(code)) {
        // If `baseEncodingName` is one the predefined encodings, and `code`
        // equals `charcode`, using the glyph defined in the baseEncoding
        // seems to yield a better `toUnicode` mapping (fixes issue 5070).
        if (baseEncodingName && code === +charcode) {
          const baseEncoding = getEncoding(baseEncodingName);
          if (baseEncoding && (glyphName = baseEncoding[charcode])) {
            toUnicode[charcode] = String.fromCharCode(
              glyphsUnicodeMap[glyphName]
            );
            continue;
          }
        }
        toUnicode[charcode] = String.fromCodePoint(code);
      }
    }
    return toUnicode;

Four checks:

CheckPurpose
code > 0excludes NUL, avoiding mapping a bad name to an empty character
code <= 0x10ffffexcludes values beyond the Unicode range
Number.isInteger(code)excludes the NaN produced by parseInt
code === +charcoderecognises the case where "the heuristic code point happens to equal the character code"

The last is the fix for issue 5070: if the heuristic code point equals the character code, the font very likely just wants to reuse the base encoding, and re-looking up the glyph name from the base encoding table in AGL usually gives a more accurate result than the heuristic. Note that this does another table lookup and does not check whether the value found is undefined — if it unfortunately misses, String.fromCharCode(undefined) produces NUL. This is one of the few undefended places in the code; triggering it in practice requires a name that is in the base encoding table yet not in AGL, an extremely low probability.

Step five: hand it to the caller.

_simpleFontToUnicode returns a sparse array, and buildToUnicode uses it to build a ToUnicodeMap:

if (!properties.composite /* is simple font */) {
  return new ToUnicodeMap(this._simpleFontToUnicode(properties));
}

For a simple font that has a /ToUnicode but incomplete data, it also computes an extra fallback:

if (!properties.composite && properties.hasEncoding) {
  properties.fallbackToUnicode = this._simpleFontToUnicode(properties);
}

This branch shows why the name table cannot be skipped even in documents that have /ToUnicode: an incomplete /ToUnicode has to be filled in from glyph names. That filling happens in amendFallbackToUnicode in fonts.js and only fills character codes not already covered by /ToUnicode:

for (const charCode in properties.fallbackToUnicode) {
  if (properties.toUnicode.has(charCode)) {
    continue; // The font dictionary has a `ToUnicode` entry.
  }
  toUnicode[charCode] = properties.fallbackToUnicode[charCode];
}

That continue line sets the priority: the /ToUnicode in the font dictionary always wins, and glyph names only fill the gaps. This is the concrete expression of the principle "the author's declaration takes priority over heuristics".

The line in the comment saying "Adobe Reader does not do this step, but OSX Preview does" is also crucial: these heuristics are not a specification requirement but exist to make copied text closer to other readers. The table provides only definite mappings; the heuristics fill in the real world beyond the specification.

Design note: All consumers share the same table reference, and nobody copies it. Copying a 4000-key object costs far more than one property lookup with zero benefit; and a shared reference makes the invariant "the table content exists in exactly one place" hold, so there is no worry about a font holding a stale copy when the data is fixed.

Name Variants, uniXXXX and Lookup Failure

The Table Only Matches Literally

This must be stated first: a table lookup in glyphlist.js is just glyphsUnicodeMap[name], with no trim, no case folding, no removal of a dotted suffix, and no splitting out of uniXXXX for parsing. The table has 4328 keys, and it accepts exactly what the key says.

There is no hidden normalisation in the source either. Only two places in the core layer split a string on a dot: parseXFAPath in core_utils.js (parsing the hierarchy of an XFA path) and the code in annotation.js that takes the file extension of an attachment name; neither has anything to do with glyph names. In other words:

NameIn the tableDirect lookup result
Aacuteyes0x00C1
aacuteyes0x00E1
uni0041noundefined
u0041noundefined
A.scnoundefined
A.altnoundefined
Aaltnoundefined
.notdefyes0

The first conclusion: dotted suffixes are not resolved by glyphlist.js. Names such as A.sc and A.alt have no entry in the table, because AGL accepts only standard names, not a font's style variants. Handling them is the font layer's job, not the name table's.

getUnicodeForGlyph: Only Two Prefixes Are Handled

What actually handles uniXXXX and uXXXX is a small function in unicode.js, not glyphlist.js:

function getUnicodeForGlyph(name, glyphsUnicodeMap) {
  let unicode = glyphsUnicodeMap[name];
  if (unicode !== undefined) {
    return unicode;
  }
  if (!name) {
    return -1;
  }
  // Try to recover valid Unicode values from 'uniXXXX'/'uXXXX{XX}' glyphs.
  if (name[0] === 'u') {
    const nameLen = name.length;
    let hexStr;
 
    if (nameLen === 7 && name[1] === 'n' && name[2] === 'i') {
      // 'uniXXXX'
      hexStr = name.substring(3);
    } else if (nameLen >= 5 && nameLen <= 7) {
      // 'uXXXX{XX}'
      hexStr = name.substring(1);
    } else {
      return -1;
    }
    // Check for upper-case hexadecimal characters, to avoid false positives.
    if (hexStr === hexStr.toUpperCase()) {
      unicode = parseInt(hexStr, 16);
      if (unicode >= 0) {
        return unicode;
      }
    }
  }
  return -1;
}

The decision rules can be listed as a table:

NameLengthWhich branchResult
uni00417the uni prefix, take the last 4 characters0x0041
uniFFFF7the uni prefix, take the last 4 characters0xFFFF
u00415the u prefix, take the last 4 characters0x0041
u00005the u prefix, take the last 4 characters0, a legal code point
u1D4006the u prefix, take the last 5 characters0x1D400
uABCDE6the u prefix, take the last 5 characters0xABCDE
uABCDEF7the u prefix, take the last 6 characters0xABCDEF
uFFFFFF7the u prefix, take the last 6 characters0xFFFFFF, beyond the Unicode range
uni0041009length is not 5 to 7, nor a 7-character uni-1
uni1236length 6 but characters 2 and 3 are not ni, so the u prefix takes ni123-1, lowercase fails the uppercase check
uniform7characters 2 and 3 are ni, so it takes form-1, lowercase fails the uppercase check
uabcde6lowercase hexadecimal-1, fails the uppercase check
u00G15the u prefix, 00G1 is all uppercase and passes the check0, parseInt truncates at the G
U00415the first character is an uppercase U, not u-1
A.sc4the first character is not u-1

A few details:

  1. The length must be exact. uni accepts only 4 hexadecimal digits; writing uni004100 is rejected instead. This is the AGL specification's convention, not an arbitrary implementation limit.
  2. It must be all uppercase. An ordinary word such as uniform first hits the u branch, but form contains lowercase letters and is blocked by the uppercase check. The test hexStr === hexStr.toUpperCase() exists to avoid misparsing English words as code points.
  3. It only checks case, not whether every character is hexadecimal. The G in u00G1 is an uppercase letter, so it passes the check, and parseInt truncates at the illegal character to 0. This is a fairly loose edge; it almost never appears in real fonts, but it should be known when reading the code.
  4. It does not check the Unicode upper bound. uFFFFFF returns 0xFFFFFF; the function only guarantees "a non-negative integer can be parsed", not "it is a legal code point". Range checking happens in the caller: _simpleFontToUnicode tests code <= 0x10ffff and discards anything beyond. The benefit of splitting responsibilities is that getUnicodeForGlyph keeps pure-function semantics, and different callers can filter by their own rules.
  5. Failure returns -1, not undefined. Because 0 is a legal code point (.notdef, U+0000), it cannot be used to mean failure. -1 also cannot be used normally by String.fromCharCode, so it is a safe sentinel.

The .sc / .alt Suffixes: Not Handled, Passed Back to the Upper Layer

The conclusion for names such as A.sc and A.alt is simple: no code in this version of the core layer removes a dotted suffix. A lookup only matches literally, and getUnicodeForGlyph only additionally handles the uni/u prefixes; neither turns A.sc into A.

After a failure there are two ready fallback paths, both in the upper layer:

CallerAction after failureFinal result
recoverGlyphName in fonts_utils.jswalks AGL backwards to find a standard name by code point; if it finds none it logs an info warning and returns the original namethe original name continues into glyphNames.indexOf and falls to .notdef if not found
_simpleFontToUnicode in evaluator.jswhen the first character is not G, g, C, c or u, it enters the default branchthat character code produces no toUnicode entry, so the text layer is missing a character
buildToFontChar in fonts.jsskips the assignment when getUnicodeForGlyph returns -1a hole is left in toFontChar, and that character has no Unicode during fallback
_spaceWidth in fonts.jsa direct subscript yields undefined, so the width is not foundthe next candidate name is tried

The reverse path of recoverGlyphName also fails on A.sc: A.sc is not in the table, and getUnicodeForGlyph returns -1 immediately because the first character is A, so the function logs a warning and returns the original name. This is the direct consequence of "the table does not guess": A.sc is not "cleverly" guessed to be A.

Some readers strip a dotted suffix as a style hint; PDF.js does not. The reason can be seen from the risk: .sc may mean small caps, .alt an alternate glyph, and stripping the suffix and guessing a base character may be right for the text but wrong for glyph selection; once the guess is wrong, the character the user copies is wrong. Leaving this step to the specific font parser (which knows which glyphs the font has) is safer than guessing in a global name table.

The Cost of a Reverse Lookup: One Full Table Scan

recoverGlyphName is the only place besides the forward lookup that touches the whole table. Its first step is a forward lookup, and on a hit it returns the original name immediately; only on a miss does it run getUnicodeForGlyph and then walk the whole table looking for a name with the same code point:

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;
}

The results for several inputs:

InputForward lookupgetUnicodeForGlyphReverse result
Ahitnot runA
afii10017hitnot runafii10017
uni0041miss0x0041A, the first key in the table whose value is 0x0041
u0041miss0x0041A
A.scmiss-1returns A.sc and logs one info
a1miss-1returns a1 and logs one info

The fourth row is the key capability: a non-standard name such as uni0041 can be recovered into the standard name A, which then takes part in the Type1 glyphNames.indexOf query. The cost is one walk over 4328 keys.

There are two call sites, with very different costs:

Call siteTrigger conditionMaximum reverse lookups
type1FontGlyphMappinga name in /Differences is not found in the font's name listthe number of difference entries, at most 256
the TrueType 3,1 cmap branch of fonts.jsevery character code has to be resolved into a standard name256

The second is called per character code:

        for (let charCode = 0; charCode < 256; charCode++) {
          // ...
          // Ensure that non-standard glyph names are resolved to valid ones.
          const standardGlyphName = recoverGlyphName(
            glyphName,
            glyphsUnicodeMap
          );

When the hit rate is high, though, the cost is negligible: recoverGlyphName does a forward lookup first, and as long as the name is standard it returns immediately without entering the walk. The full-table scan is paid only when a generator wrote a non-standard name, and each character code scans at most once, stopping at the first match. Compared with a worst case of 256 full walks, this design pushes the cost onto "the number of bad names" rather than "the total number of character codes".

Design note: The reverse lookup builds no reverse index. A 4328-entry "code point to name" table would take up another chunk of memory and would still have to choose one when several names point at the same code point; scanning with for...in on demand, stopping at a hit, pays only when a bad name appears. Trading time for space works because bad names are rare — and in real PDFs they are.

The Return-Value Contract for a Failed Lookup

Looking at the three layers of return values together:

LayerMissHitNote
glyphsUnicodeMap[name]undefinedan integer code pointpure data access, no sentinel
getUnicodeForGlyph(name, map)-1an integer code pointhandles the uni/u prefixes
recoverGlyphName(name, map)the original namea standard namelogs info on failure
buildToFontCharskips the indexwrites the code pointoutputs a sparse array
_simpleFontToUnicodewrites nothing to toUnicodewrites a stringcontinues with the next character code

This table shows a division of labour: the lower down, the more it only answers facts; the higher up, the more it must decide what to do. glyphlist.js sits at the bottom and only answers "is there one, and what is it".

What Happens to the Text After a Failure

Many people assume a failed lookup causes a rendering failure; it does not. The rendering path depends on the glyph data in the file, and as long as an embedded font exists the display is still correct. What a failure affects is the text layer:

ScenarioDisplayText layer
Embedded font, no /ToUnicode, standard namenormalthe lookup succeeds, correct text can be copied
Embedded font, no /ToUnicode, name is uniXXXXnormalgetUnicodeForGlyph parses it, copyable
Embedded font, no /ToUnicode, name has a .sc suffixnormalno Unicode for that character, copying yields empty or a replacement character
No embedded font, standard namesystem font fallback, relying on toFontCharcopyable
No embedded font, name cannot be resolvedsystem font fallback may pick the wrong glyphtext missing

The last two rows are the real risk zone: system font fallback depends entirely on toFontChar, and toFontChar depends entirely on name resolution. The name table is not an "optimisation" here; it is the only data source.

Five Common Misconceptions

When reading this file, five judgements are easy to take for granted:

Misconception one: the table is parsed at runtime from a compact string.

There is no parsing. The initializer of getGlyphsUnicode contains 4328 line-by-line assignments, with no separators, no split and no traversal conversion. There was indeed a flat-array version historically, but that was only a conversion from an array to an object, not string parsing. Imagining this table as "encoded data plus a decoder" sends you completely in the wrong reading direction.

Misconception two: a failed name lookup makes the page fail to render.

The rendering path uses the glyph data in the font, not Unicode. When a name is not found, the canvas still draws by glyph id and the page displays as usual; only copy, search and read-aloud in the text layer are affected. This is also why PDF.js can still "look normal" with an incomplete name table.

Misconception three: every call to the factory returns a new object you can modify freely.

getLookupTableFactory memoises the result and returns the same reference every time within one Worker. Writing an extra property onto the returned object pollutes the lookup results of every later font and every page. The table is neither frozen nor guarded; the correct usage is read-only.

Misconception four: uniXXXX parsing lives in glyphlist.js.

glyphlist.js only matches literally, and looking up uni0041 directly returns undefined. Parsing happens in getUnicodeForGlyph in unicode.js, and only when the caller actively calls it. glyphlist.js does not even know that the uni prefix means anything special.

Misconception five: the table is released when the document is destroyed.

The table hangs on a module closure and has the same lifetime as the Worker. cleanup does not handle it, and switching documents does not rebuild it. This also means that after "opening a document that needed AGL", this 4000-key object stays in memory even if all later documents are pure images. This is the direct cost of lazy loading and the inevitable result of choosing "one per Worker, never released".

Read those five the other way round and they are exactly the boundaries of this table: it only stores data, only matches literally, is read-only, global and resident. Anything beyond that boundary lives in another module.

Design note: A name resolution failure must be a "continuable failure". Every layer returns a value that can flow on (undefined, -1, the original name, a skipped index) instead of throwing. A PDF with a few bad names can therefore still be opened, scrolled and displayed for the vast majority of pages, degraded only in the copying of individual characters.

Conclusion

glyphlist.js is the font layer's name dictionary: it translates glyph name strings from a PDF into Unicode code points, for fonts.js to build toUnicode and toFontChar, for evaluator.js to do text extraction, for font_renderer.js to handle seac composition, and for standard_fonts.js to construct the mapping for Mac-order fonts.

Where It Sits in the Overall Chain

StageModuleInputOutput
Encodingencodings.jssingle-byte character codeglyph name
Differencesthe font dictionary /Differencescharacter code and nameoverwritten glyph name
CMapcmap.jscharacter codeCID or /ToUnicode
Glyph name resolutionglyphlist.jsglyph namecode point
Font assemblyfonts.jscode point, glyph, widthtoUnicode, toFontChar, fontChar
Rendering and textcanvas, text layerfontChar, unicodevisible glyphs, copyable text

Stringing the order together in a piece of code:

// character code
//   → encodings.js / Differences      glyph name
//   → glyphlist.js                    Unicode code point (this article)
//   → unicode.js getUnicodeForGlyph   uniXXXX / uXXXX fallback
//   → fonts.js buildToFontChar        toFontChar and toUnicode
//   → canvas and text layer           glyphs and characters

Six Design Principles

  1. Separate data from policy. The table only answers "does this name have a code point"; it does not judge whether a name is valid, does not guess suffixes and does not normalise. All repair logic is concentrated in unicode.js, fonts_utils.js and evaluator.js, each able to be tested and to evolve independently.
  2. Split tables by name space instead of one big table. AGL and ZapfDingbats each correspond to a class of font, and consumers pick an encoding table and a glyph table as a pair by font name. Merging would hide the error "this font should not use this table".
  3. Lazy-load, build once, share globally. getLookupTableFactory defers the 4328 assignments until first use and returns the same reference afterwards; the table is not cleared with the document and is not attached to any document object. Documents that do not use the font layer pay nothing for it.
  4. A null-prototype container carries untrusted keys. The keys come from PDF files and may be names such as constructor or __proto__; Object.create(null) makes inherited properties unable to cause false hits.
  5. A sentinel must not intersect with legal values. 0 is a legal code point, so "not found" uses undefined and "parse failure" uses -1. Mapping .notdef to 0 is deliberate, and consumers must skip it explicitly.
  6. Failure must be continuable. Every layer returns a value that can flow on rather than throwing. A few bad names affect only the text of the corresponding characters, not the opening and rendering of the whole document.

This table looks like mere data, but it decides whether PDF.js can copy correct text from "old files without /ToUnicode". Once it is understood, looking again at the encoding tables in encodings.js, the ToUnicode parsing in cmap.js and the construction of toFontChar in fonts.js makes it clear that the three share the same name and code-point semantics rather than each maintaining its own mapping.

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