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

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.jsin the current repository. This version has no compact string or flat array form of the data: both tables assignt.Name = 0xNNNNentry by entry inside the initializer ofgetLookupTableFactory. Historically there was indeed a flat-array version (October 2020 introducedgetArrayLookupTableFactory, reverted that November), but the current source has neithergetArrayLookupTableFactorynor 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 thefsection; 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:
| Block | Key members or tables | Responsibility |
|---|---|---|
| Dependency | getLookupTableFactory | Provides the lazy-loading shell that builds the table on the first call and returns it afterwards |
| General glyph table | getGlyphsUnicode, 4328 entries | AGL 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 table | getDingbatsGlyphsUnicode, 203 entries | ZapfDingbats numbered names such as a1 to a206 to code points, mostly in the U+2700 block |
| Export | export { 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 textNote 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:
| Term | Behaviour |
|---|---|
| Call form | Zero arguments, getGlyphsUnicode() or getDingbatsGlyphsUnicode() |
| Return type | Null-prototype object, keys are glyph name strings, values are integer code points |
| Object identity | Always returns the same object within one Worker, never a new one |
| Build time | Runs the initializer on the first call, returns the reference afterwards |
| Lifetime | Same as the Worker, does not take part in any document-level cleanup |
| Miss | Returns 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']; // undefinedThe 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.jstherefore 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.
From Glyph Name to Unicode: The Missing Link in the Chain
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:
| Path | Input | Intermediate result | Output | Consequence if missing |
|---|---|---|---|---|
| Rendering | character code | glyph name or glyph id | fontChar / glyph program | a missing glyph on screen, or a fallback to .notdef |
| Text | character code | /ToUnicode or glyph name | unicode string | garbled copy, search and read-aloud |
| Fallback | font name + character code | standard font name table | system font and toFontChar | whole 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:
| Format | Where the names live | Role |
|---|---|---|
| Type1 | keys of the /CharStrings dictionary | the key of each glyph program is the name; programs are fetched by name |
| CFF | charset | glyph id to SID or custom string, all ultimately restored to names |
| TrueType | post table 2.0 | glyph id to name; the first 258 are the standard Mac ordering names |
encoding tables and /Differences | character 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:
| Font | Encoding table | Character code | Name found | Glyph table | Code point |
|---|---|---|---|---|---|
| Ordinary text font | StandardEncoding | 0x41 | A | getGlyphsUnicode | 0x0041 |
| Symbol | SymbolSetEncoding | 0x41 | Alpha | getGlyphsUnicode | 0x0391 |
| ZapfDingbats | ZapfDingbatsEncoding | 0x21 | a1 | getDingbatsGlyphsUnicode | 0x2701 |
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:
- The official basis for glyph name to Unicode is the Adobe Glyph List, not any table inside the font;
- The lookup result goes straight into
String.fromCharCode, which shows that the table stores code point integers, not strings; - 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:
| Property | Behaviour | Impact |
|---|---|---|
| Global | not attached to Catalog or Font | cleanup does not need to handle it |
| Read-only | no write path at all | safe for concurrent lookups across fonts and pages |
| Idempotent | the factory returns the same object every time | callers may call it freely without passing references around |
| Silent | a miss returns undefined | error handling is the caller's responsibility |
Design note:
glyphlist.jsseparates "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 inunicode.js,fonts_utils.jsandevaluator.jsrespectively. 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 shape | Examples | Meaning |
|---|---|---|
| Single letters and Latin extensions | A, AE, Abreve | basic Latin and letters with diacritics |
| Greek names | Alpha, Alphatonos | Greek letters and their accented forms |
| Monospace forms | Amonospace | fullwidth or halfwidth monospaced variants, landing at U+FF21 |
| small variants | AEsmall, Aacutesmall | small capitals from the Expert character set, landing in the private use area |
| afii numbers | afii10017 | numbered 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 family | Count | Representative entry | Where the code point goes |
|---|---|---|---|
cyrillic | 222 | Acyrillic | U+0400 block |
arabic | 244 | ainarabic | the U+0600 block and the U+FE70 presentation forms block |
hebrew | 169 | alefhebrew | the U+0590 block and the U+FB1D presentation forms |
katakana | 93 | akatakana | U+30A0 block |
hiragana | 86 | ahiragana | U+3040 block |
halfwidth | 62 | akatakanahalfwidth | U+FF00 block |
monospace | 99 | Amonospace | U+FF00 block |
thai | 109 | Thai letter names such as kothai | U+0E00 block |
bopomofo | 37 | aibopomofo | U+3100 block |
small | 100 | AEsmall | private use area U+F600 to U+F8FF |
oldstyle | 12 | oneoldstyle | private use area |
superior | 42 | superscript digit and letter variants | the U+2070 block and the private use area |
inferior | 17 | subscript digit variants | the U+2080 block and the private use area |
display | 14 | summationdisplay | mathematical 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:
| Name | Shape | Code point |
|---|---|---|
ainarabic | isolated | U+0639 |
aininitialarabic | initial | around U+FEBB |
ainmedialarabic | medial | around U+FECC |
ainfinalarabic | final | around 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:
| Metric | Value | Note |
|---|---|---|
| Entries | 4328 | 4327 dotted assignments plus one .notdef |
| Source size | about 115.7 KB | table body only, excluding comments and exports |
| Code point range | U+0000 to U+FFE6 | all inside the Basic Multilingual Plane |
| Private use values | 192 entries | falling in U+F600 to U+F8FF, that is, PUA |
| ASCII values | 159 entries | values no greater than U+007F, including .notdef at 0 |
| Supplementary plane values | 0 entries | no 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 range | Count | Contents |
|---|---|---|
| U+0000 to U+007F | 159 | Basic Latin and control characters |
| U+0080 to U+00FF | 102 | Latin-1 Supplement |
| U+0100 to U+017F | 159 | Latin Extended-A |
| U+0180 to U+024F | 152 | Latin Extended-B |
| U+0250 to U+036F | 219 | IPA, modifier letters, combining marks |
| U+0370 to U+03FF | 111 | Greek and Coptic |
| U+0400 to U+052F | 327 | Cyrillic |
| U+0530 to U+058F | 85 | Armenian |
| U+0590 to U+05FF | 259 | Hebrew |
| U+0600 to U+06FF | 175 | Arabic main block |
| U+0900 to U+0DFF | 344 | Indic scripts |
| U+0E00 to U+0E7F | 87 | Thai |
| U+1E00 to U+1EFF | 246 | Latin Extended Additional |
| U+3000 to U+30FF | 224 | CJK punctuation, hiragana, katakana |
| U+3100 to U+312F | 37 | Bopomofo |
| U+3130 to U+318F | 94 | Hangul compatibility letters |
| U+3200 to U+33DD | 236 | enclosed CJK and CJK compatibility |
| U+FB00 to U+FB4F | 78 | Latin ligatures |
| U+F600 to U+F8FF | 192 | private use area |
| U+FF00 to U+FFEF | 162 | halfwidth 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:
| Table | Latin | Greek and Cyrillic | Middle Eastern and Indic | Kana and CJK | Symbols and private use |
|---|---|---|---|---|---|
| AGL | about 900 entries | about 440 entries | about 1150 entries | about 600 entries | about 1030 entries |
| Dingbats | 0 | 0 | 0 | 0 | 201 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:
| Dimension | Examples | Block of the code point |
|---|---|---|
| Single diacritic | Aacute, Agrave, Atilde | Latin-1 Supplement |
| Stacked diacritics | Abreveacute, Acircumflexhookabove | Latin Extended Additional |
| Non-Latin adaptation | Abrevecyrillic, Adieresiscyrillic | Cyrillic block |
| Style or decoration | Acircle, Asmall, Amonospace | enclosed 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.
| Name | Code point | Actual character | Use |
|---|---|---|---|
Delta | 0x2206 | increment | mathematical typesetting |
Deltagreek | 0x0394 | Greek capital delta | Greek text |
Omega | 0x2126 | ohm | physics and engineering |
Omegagreek | 0x03A9 | Greek capital omega | Greek text |
Upsilon1 | 0x03D2 | upsilon with hook | Greek 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:
| Phenomenon | Examples | Note |
|---|---|---|
| Same name across scripts | alef and alefarabic | Hebrew alef is U+05D0, Arabic alef is U+0627 |
| Same name, same code point | alef and alefhebrew | both names point at the same Hebrew letter |
| Shape suffixes | ...finalarabic, ...medialarabic | final and medial shapes, in the presentation forms block |
| Combinations and vocalisation | alefdageshhebrew, alefpatahhebrew | combinations 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;| Name | Code point | Form |
|---|---|---|
ahiragana | 0x3042 | hiragana a |
akatakana | 0x30a2 | katakana a |
akatakanahalfwidth | 0xff71 | halfwidth katakana |
aibopomofo | 0x311e | bopomofo 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:
| Characteristic | Behaviour | Reason |
|---|---|---|
| Name shape | a plus a number, numbered from a1 to a206, with 5 numbers missing | names come from the ZapfDingbats font's glyph list, not from Unicode names |
| Order | neither name-sorted nor code-point-sorted | preserves the order of Adobe's original list |
| Main code point block | 174 of the 203 entries fall in U+2700 to U+27BF | Unicode's Dingbats block is encoded according to this symbol set |
| Outlying entries | 29 fall outside that block | including .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 values | 0 | every name maps to a unique code point |
| Trailing notes | 14 entries carry comments such as // 0xF8D7 | preserve 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:
| Position | Name | Code point |
|---|---|---|
| 2nd entry | a1 | 0x2701 |
| 3rd entry | a2 | 0x2702 |
| 4th entry | a202 | 0x2703 |
| 5th entry | a3 | 0x2704 |
| 6th entry | a4 | 0x260E |
| 7th entry | a5 | 0x2706 |
| 8th entry | a119 | 0x2707 |
| 9th entry | a118 | 0x2708 |
| 10th entry | a117 | 0x2709 |
| 11th entry | a11 | 0x261B |
Three counter-intuitive points:
a202sits betweena2anda3. 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.a4has 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.a117,a118anda119appear 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
| Dimension | AGL (getGlyphsUnicode) | ZapfDingbats (getDingbatsGlyphsUnicode) |
|---|---|---|
| Coverage | Latin, Greek, Cyrillic, Armenian, Hebrew, Arabic, Indic, Thai, kana, bopomofo, Hangul compatibility letters, CJK compatibility area, mathematical symbols | one symbol font, ZapfDingbats, with 201 a-numbered glyphs |
| Name source | the Adobe Glyph List, plus afii numbers and TeX additions | the ZapfDingbats font's glyph list |
| Name shape | mixed-case letter names, small suffix, afiiNNNNN, math names | a plus a number |
| Entries | 4328 (4211 alphabetical entries plus .notdef plus 116 TeX entries) | 203 (space plus 201 a-numbered names plus .notdef) |
| Source size | table body about 113 KiB (115,699 bytes) | table body about 3.9 KiB (3,972 bytes) |
| Code point range | U+0000 to U+FFE6 | U+0020 to U+27BE |
| Main use | general text semantics, system-font fallback, Type1 and CFF glyph name resolution | used only by the ZapfDingbats font |
| Consumers | fonts.js, evaluator.js, font_renderer.js, standard_fonts.js, fonts_utils.js | the /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:
- Polluted queries: AGL has no
a4, while in Dingbatsa4is U+260E. After merging, any path that queriesa4with AGL would suddenly hit a telephone symbol. A font encoding containinga4should not hit anything in the first place; hitting something actually hides the fact that "this font is not Dingbats". - Loss of choice: which table to use is not decided by the name but by the font name.
fonts.jsroutes 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 name | Code point in AGL | Later normalisation |
|---|---|---|
universal | 0x2200 | unchanged |
existential | 0x2203 | unchanged |
suchthat | 0x220B | unchanged |
theta1 | 0x03D1 | unchanged |
Upsilon1 | 0x03D2 | unchanged |
arrowvertex | 0xF8E6 | 0x23D0 |
copyrightserif | 0xF6D9 | 0x00A9 |
registerserif | 0xF6DA | 0x00AE |
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".
a1is 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:
| Structure | Lookup by name | How it is built | Memory | Keys and order |
|---|---|---|---|---|
| Plain object literal | property lookup | allocated once at module evaluation | medium | strings, subject to prototype pollution |
Object.create(null) | property lookup | entry-by-entry assignment in the initializer | medium | strings, no prototype |
Map | get | entry-by-entry set | higher, each entry has extra overhead | any type, insertion order |
| Array | not applicable | needs preallocation by index | depends on sparsity | numeric indices |
| Flat array | needs conversion to an object or a linear scan | doubles the peak during conversion | high | mixed |
There are three reasons for choosing a null-prototype object:
- The keys are strings, and mostly look like identifiers.
Aacute,uni0041andafii10017can all be property names directly, and V8 has dedicated optimisations for this kind of access.Map.getneeds a method call and a hash lookup, slightly slower on the hot path. - Building can be one assignment at a time.
t.A = 0x0041is a property write;Mapneedst.set("A", 0x0041), an extra call layer. 4328 entries mean 4328 call differences, small but with no reason to pick the slower option. - 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",| Dimension | encodings.js | glyphlist.js |
|---|---|---|
| Keys | integers from 0 to 255 | arbitrary glyph name strings |
| Container | ordinary array, the index is the character code | null-prototype object, the key is the name |
| Size | 256 entries per table, seven tables in total | 4328 entries and 203 entries |
| Loading | built directly at module evaluation | lazily loaded through getLookupTableFactory |
| Empty value | the empty string "" | undefined (the key simply does not exist) |
| Lookup | arr[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:
| Reason | Explanation |
|---|---|
| Lazy loading | an 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 checking | one 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:
| Stage | How often | Cost |
|---|---|---|
| Module evaluation | once per Worker | only creates two closures, builds no table |
First getGlyphsUnicode() | once per Worker | runs 4328 assignments, allocates an object with 4328 keys |
| Later calls | every lookup | returns the same reference, constant time |
| Single query | every glyph | one 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:
| Operation | Magnitude | Note |
|---|---|---|
| Single lookup | 1 property access | string-key lookup on a dictionary-mode object |
| One table build | 4328 assignments | happens only once per Worker |
Building toFontChar for one font | 256 lookups plus difference entries | once per simple font |
| Text extraction for a whole document | one per character | one 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:
| Item | Impact |
|---|---|
| Bundling location | goes into pdf.worker.js, not into the main-thread bundle |
| Syntax parsing | over 4500 lines and 4531 assignment statements; parsing cost exists but is far below the first execution |
| Transfer compression | the repeated t. prefix and hexadecimal literals are gzip-friendly, so the actual compression ratio is fairly high |
| Tree shaking | both factories are referenced by several core modules, so they cannot be shaken out |
| Splitting | splitting 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:
| Item | Entry-by-entry assignment | Flat array |
|---|---|---|
| Source size | one name plus one code point per line | each name and code point takes one array element, halving the line count |
| First build | writes directly into the target object | allocates an array first, then walks half its length writing into an object, and finally discards the array |
| Peak memory | one object | one array plus one object |
| Readability | one record per line | two aligned columns, like CSV |
| Formatting | normal formatting | requires // 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:
| Step | Action | Reason |
|---|---|---|
| 1 | First check whether Adobe's official AGL already has it | a self-invented name cannot be aligned with other readers |
| 2 | Decide whether it goes into AGL or Dingbats | decides whether consumers can hit it |
| 3 | Upstream entries are inserted alphabetically; local patches go nearby or are appended at the end | keep upstream comparable |
| 4 | Values are written in lowercase four-digit hexadecimal | minimal diff noise |
| 5 | Call getUnicodeForGlyph manually to verify | confirm the parse result directly |
| 6 | Add an assertion in test/unit/unicode_spec.js | prevent 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); // 0x00a9The 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:
| Risk | Consequence | Current defence |
|---|---|---|
| Duplicate keys | the later value silently overwrites the earlier one | no automatic check, only a global search for the name |
| Insertion position | changes for...in order and therefore which entry a reverse lookup picks | patches are placed next to the relevant letter section, and the TeX section is kept at the end |
| Wrong case | becomes another glyph: found but semantically wrong | strictly copy the upstream naming |
| Inconsistent radix or digit count | values still correct, but noisy diffs | the 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.jswould 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:
| Names | Code point | Relationship |
|---|---|---|
Dcroat | 0x0110 | exactly the same value as Dslash |
Dslash | 0x0110 | same as above |
Zdot | 0x017b | same value as Zdotaccent |
Zdotaccent | 0x017b | same as above |
radical | 0x221a | same value as radicalbig and radicalBig in the TeX section |
radicalbig | 0x221a | TeX section, size variants share the radical code point |
parenleftBig | 0x0028 | four sizes in the TeX section share the left parenthesis code point |
parenleftbigg | 0x0028 | same as above |
space | 0x0020 | same 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:
| Query | The table's answer | Semantics |
|---|---|---|
| A name that does not exist | undefined | not found |
.notdef | 0 | found, but the code point is NUL |
| A real space | 0x0020 | found, 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
undefinedfor "absent" and-1for "parse failure", precisely because0is already a legal code point; the presence of.notdefnails down the fact that "0 is a legal result", so any scheme using0for 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:
| Condition | Is AGL built |
|---|---|
The font has no /Differences | no |
There are differences, but every name is found in glyphNames | no |
At least one difference name is not found and recoverGlyphName is needed | yes |
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);
}
}| Function | Name source | Lookup method | Reason |
|---|---|---|---|
adjustTrueTypeToUnicode | the fixed WinAnsiEncoding | direct subscript glyphsUnicodeMap[glyphName] | the names in the encoding table are all standard names, so parsing uniXXXX is unnecessary |
adjustType1ToUnicode | the font's built-in encoding | getUnicodeForGlyph | built-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:
| Consumer | Lookup timing | Frequency | Purpose |
|---|---|---|---|
type1FontGlyphMapping | font construction, and only when a difference name is not found | at most 256 per font, and mostly not triggered | difference name recovery |
buildToFontChar | font construction, in the fallbackToSystemFont branch | 256 plus difference entries per font | system font fallback |
adjustType1ToUnicode / adjustTrueTypeToUnicode | late font construction, repairing toUnicode | 256 per font | completing the text mapping |
_simpleFontToUnicode | text extraction initialisation | 256 per font | main text path |
recoverGlyphName | the TrueType 3,1 cmap branch | once per character code, returns immediately on a hit | standard name recovery |
the seac case of Type2Compiled | rendering, on endchar with four arguments | once per composite glyph | composing accent glyphs |
getGlyphMapForMacOrderedFonts | the first call of the standard font table | once per Worker | building 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 character | Name length | How it is parsed | Example | Result |
|---|---|---|---|---|
G | exactly 3 | the last two digits as hexadecimal | G41 | U+0041 |
g | exactly 5 | the last four digits as hexadecimal | g0041 | U+0041 |
C or c | 3 to 4 | decimal by default, hexadecimal under forceGlyphs | C65 | 65 or 0x65 |
u | decided by getUnicodeForGlyph | uniXXXX or uXXXX{XX} | uni0041 | U+0041 |
| anything else | any | first the three hand-written ligatures, then give up | f_h | the 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:
| Check | Purpose |
|---|---|
code > 0 | excludes NUL, avoiding mapping a bad name to an empty character |
code <= 0x10ffff | excludes values beyond the Unicode range |
Number.isInteger(code) | excludes the NaN produced by parseInt |
code === +charcode | recognises 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:
| Name | In the table | Direct lookup result |
|---|---|---|
Aacute | yes | 0x00C1 |
aacute | yes | 0x00E1 |
uni0041 | no | undefined |
u0041 | no | undefined |
A.sc | no | undefined |
A.alt | no | undefined |
Aalt | no | undefined |
.notdef | yes | 0 |
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:
| Name | Length | Which branch | Result |
|---|---|---|---|
uni0041 | 7 | the uni prefix, take the last 4 characters | 0x0041 |
uniFFFF | 7 | the uni prefix, take the last 4 characters | 0xFFFF |
u0041 | 5 | the u prefix, take the last 4 characters | 0x0041 |
u0000 | 5 | the u prefix, take the last 4 characters | 0, a legal code point |
u1D400 | 6 | the u prefix, take the last 5 characters | 0x1D400 |
uABCDE | 6 | the u prefix, take the last 5 characters | 0xABCDE |
uABCDEF | 7 | the u prefix, take the last 6 characters | 0xABCDEF |
uFFFFFF | 7 | the u prefix, take the last 6 characters | 0xFFFFFF, beyond the Unicode range |
uni004100 | 9 | length is not 5 to 7, nor a 7-character uni | -1 |
uni123 | 6 | length 6 but characters 2 and 3 are not ni, so the u prefix takes ni123 | -1, lowercase fails the uppercase check |
uniform | 7 | characters 2 and 3 are ni, so it takes form | -1, lowercase fails the uppercase check |
uabcde | 6 | lowercase hexadecimal | -1, fails the uppercase check |
u00G1 | 5 | the u prefix, 00G1 is all uppercase and passes the check | 0, parseInt truncates at the G |
U0041 | 5 | the first character is an uppercase U, not u | -1 |
A.sc | 4 | the first character is not u | -1 |
A few details:
- The length must be exact.
uniaccepts only 4 hexadecimal digits; writinguni004100is rejected instead. This is the AGL specification's convention, not an arbitrary implementation limit. - It must be all uppercase. An ordinary word such as
uniformfirst hits theubranch, butformcontains lowercase letters and is blocked by the uppercase check. The testhexStr === hexStr.toUpperCase()exists to avoid misparsing English words as code points. - It only checks case, not whether every character is hexadecimal. The
Ginu00G1is an uppercase letter, so it passes the check, andparseInttruncates 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. - It does not check the Unicode upper bound.
uFFFFFFreturns 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:_simpleFontToUnicodetestscode <= 0x10ffffand discards anything beyond. The benefit of splitting responsibilities is thatgetUnicodeForGlyphkeeps pure-function semantics, and different callers can filter by their own rules. - Failure returns
-1, notundefined. Because 0 is a legal code point (.notdef, U+0000), it cannot be used to mean failure.-1also cannot be used normally byString.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:
| Caller | Action after failure | Final result |
|---|---|---|
recoverGlyphName in fonts_utils.js | walks AGL backwards to find a standard name by code point; if it finds none it logs an info warning and returns the original name | the original name continues into glyphNames.indexOf and falls to .notdef if not found |
_simpleFontToUnicode in evaluator.js | when the first character is not G, g, C, c or u, it enters the default branch | that character code produces no toUnicode entry, so the text layer is missing a character |
buildToFontChar in fonts.js | skips the assignment when getUnicodeForGlyph returns -1 | a hole is left in toFontChar, and that character has no Unicode during fallback |
_spaceWidth in fonts.js | a direct subscript yields undefined, so the width is not found | the 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:
| Input | Forward lookup | getUnicodeForGlyph | Reverse result |
|---|---|---|---|
A | hit | not run | A |
afii10017 | hit | not run | afii10017 |
uni0041 | miss | 0x0041 | A, the first key in the table whose value is 0x0041 |
u0041 | miss | 0x0041 | A |
A.sc | miss | -1 | returns A.sc and logs one info |
a1 | miss | -1 | returns 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 site | Trigger condition | Maximum reverse lookups |
|---|---|---|
type1FontGlyphMapping | a name in /Differences is not found in the font's name list | the number of difference entries, at most 256 |
the TrueType 3,1 cmap branch of fonts.js | every character code has to be resolved into a standard name | 256 |
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...inon 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:
| Layer | Miss | Hit | Note |
|---|---|---|---|
glyphsUnicodeMap[name] | undefined | an integer code point | pure data access, no sentinel |
getUnicodeForGlyph(name, map) | -1 | an integer code point | handles the uni/u prefixes |
recoverGlyphName(name, map) | the original name | a standard name | logs info on failure |
buildToFontChar | skips the index | writes the code point | outputs a sparse array |
_simpleFontToUnicode | writes nothing to toUnicode | writes a string | continues 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:
| Scenario | Display | Text layer |
|---|---|---|
Embedded font, no /ToUnicode, standard name | normal | the lookup succeeds, correct text can be copied |
Embedded font, no /ToUnicode, name is uniXXXX | normal | getUnicodeForGlyph parses it, copyable |
Embedded font, no /ToUnicode, name has a .sc suffix | normal | no Unicode for that character, copying yields empty or a replacement character |
| No embedded font, standard name | system font fallback, relying on toFontChar | copyable |
| No embedded font, name cannot be resolved | system font fallback may pick the wrong glyph | text 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
| Stage | Module | Input | Output |
|---|---|---|---|
| Encoding | encodings.js | single-byte character code | glyph name |
| Differences | the font dictionary /Differences | character code and name | overwritten glyph name |
| CMap | cmap.js | character code | CID or /ToUnicode |
| Glyph name resolution | glyphlist.js | glyph name | code point |
| Font assembly | fonts.js | code point, glyph, width | toUnicode, toFontChar, fontChar |
| Rendering and text | canvas, text layer | fontChar, unicode | visible 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 charactersSix Design Principles
- 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.jsandevaluator.js, each able to be tested and to evolve independently. - 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".
- Lazy-load, build once, share globally.
getLookupTableFactorydefers 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. - A null-prototype container carries untrusted keys. The keys come from PDF files and may be names such as
constructoror__proto__;Object.create(null)makes inherited properties unable to cause false hits. - A sentinel must not intersect with legal values. 0 is a legal code point, so "not found" uses
undefinedand "parse failure" uses-1. Mapping.notdefto 0 is deliberate, and consumers must skip it explicitly. - 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
- PDF.js Deep Dive from Demo 10: CanvasGraphics and operatorList RenderingOctober 27, 2026
- PDF.js Deep Dive from Demo 9: Network Streams and Range RequestsOctober 27, 2026
- PDF.js Deep Dive from Demo 8: PDFWorker.create and the Real/Fake Worker HandshakeOctober 27, 2026
- PDF.js Deep Dive from Demo 7: WorkerTransport InitializationOctober 27, 2026
- PDF.js Deep Dive from Demo 6: MessageHandler Cross-Thread CommunicationOctober 27, 2026
- PDF.js Deep Dive from Demo 5: renderOctober 27, 2026
- PDF.js Deep Dive from Demo 4: getViewportOctober 27, 2026
- PDF.js Deep Dive from Demo 3: getPageOctober 27, 2026
- PDF.js Deep Dive from Demo 2: getDocumentOctober 27, 2026
- PDF.js Source Walkthrough: TextLayerOctober 27, 2026
- PDF.js Source Walkthrough: font_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_displayOctober 27, 2026
- PDF.js Source Walkthrough: PDFObjectsOctober 27, 2026
- PDF.js Source Walkthrough: Network StreamsOctober 27, 2026
- PDF.js Source Walkthrough: internal_evt.jsOctober 27, 2026
- PDF.js Source Walkthrough: math_clamp.jsOctober 27, 2026
- PDF.js Source Walkthrough: css_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: scripting_utilsOctober 27, 2026
- PDF.js Source Walkthrough: MurmurHash3_64October 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_utilsOctober 27, 2026
- PDF.js Source Walkthrough: image_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: BasePDFStreamOctober 27, 2026
- PDF.js Source Walkthrough: Shared UtilitiesOctober 27, 2026
- PDF.js Source Walkthrough: MessageHandlerOctober 27, 2026
- PDF.js Source Walkthrough: apiOctober 27, 2026
- PDF.js Source Walkthrough: fontsOctober 27, 2026
- PDF.js Source Walkthrough: binary_cmapOctober 27, 2026
- PDF.js Source Walkthrough: cmapOctober 27, 2026
- PDF.js Source Walkthrough: encodingsOctober 27, 2026
- PDF.js Source Walkthrough: metricsOctober 27, 2026
- PDF.js Source Walkthrough: standard_fontsOctober 27, 2026
- PDF.js Source Walkthrough: fonts_utilsOctober 27, 2026
- PDF.js Source Walkthrough: default_appearanceOctober 27, 2026
- PDF.js Source Walkthrough: metadata_parserOctober 27, 2026
- PDF.js Source Walkthrough: name_number_treeOctober 27, 2026
- PDF.js Source Walkthrough: struct_treeOctober 27, 2026
- PDF.js Source Walkthrough: catalogOctober 27, 2026
- PDF.js Source Walkthrough: pdf_managerOctober 27, 2026
- PDF.js Source Walkthrough: object_loaderOctober 27, 2026
- PDF.js Source Walkthrough: obj_bin_transform_coreOctober 27, 2026
- PDF.js Source Walkthrough: cryptoOctober 27, 2026
- PDF.js Source Walkthrough: core_utilsOctober 27, 2026
- PDF.js Source Walkthrough: primitivesOctober 27, 2026
- PDF.js Source Walkthrough: worker_streamOctober 27, 2026
- PDF.js Source Walkthrough: ccitt_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpx_streamOctober 27, 2026
- PDF.js Source Walkthrough: jbig2_streamOctober 27, 2026
- PDF.js Source Walkthrough: jpeg_streamOctober 27, 2026
- PDF.js Source Walkthrough: decrypt_streamOctober 27, 2026
- PDF.js Source Walkthrough: brotli_streamOctober 27, 2026
- PDF.js Source Walkthrough: run_length_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_hex_streamOctober 27, 2026
- PDF.js Source Walkthrough: ascii_85_streamOctober 27, 2026
- PDF.js Source Walkthrough: lzw_streamOctober 27, 2026
- PDF.js Source Walkthrough: predictor_streamOctober 27, 2026
- PDF.js Source Walkthrough: flate_streamOctober 27, 2026
- PDF.js Source Walkthrough: decode_streamOctober 27, 2026
- PDF.js Source Walkthrough: chunked_streamOctober 27, 2026
- PDF.js Source Walkthrough: streamOctober 27, 2026
- PDF.js Source Walkthrough: base_streamOctober 27, 2026
- PDF.js Deep Dive 11: ParserOctober 26, 2026
- PDF.js Deep Dive 10: XRefOctober 26, 2026
- PDF.js Deep Dive 9: CanvasGraphicsOctober 26, 2026
- PDF.js Deep Dive 8: PDFDocumentOctober 26, 2026
- The AI Era: What Should We Do to Survive? (Reflections on Founding StreamPDF)October 26, 2026
- PDF.js Deep Dive 7: PartialEvaluatorOctober 25, 2026
- PDF.js Deep Dive 6: PDFManager.initDocumentOctober 25, 2026
- PDF.js Deep Dive 5: PDFFetchStream and PDFWorkerStreamOctober 25, 2026
- PDF.js Deep Dive 4: MessageHandlerOctober 22, 2026
- PDF.js Deep Dive 3: getViewport & renderOctober 22, 2026
- PDF.js Deep Dive from Demo 1: Rendering a PDF with a Custom ViewerOctober 19, 2026
- PDF.js Deep Dive 2: getDocument & getPageOctober 19, 2026
- PDF.js Deep Dive 1: The Architecture of PDF.jsOctober 16, 2026