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

Introduction
This installment examines src/shared/util.js, a 1,229-line module imported throughout the parser, renderer, annotation system, and editor.
It is tempting to dismiss this file as a miscellaneous utility bag. That misses its real role: it defines the low-level vocabulary and contracts shared by otherwise independent PDF.js subsystems. Numeric PDF flags, operator identifiers, exception shapes, binary-string conversions, browser probes, and geometry routines all need exactly one interpretation. Duplicating them across worker and display code would create subtle incompatibilities.
The module also sits on two different boundaries. Some helpers interpret PDF data, where malformed input and specification-defined numbers are normal concerns. Others adapt to the JavaScript host, where Node.js, browser, worker, and transpiled builds expose different capabilities. Reading the file by functional family makes those boundaries—and the design tradeoffs around them—clear.
Module Overview
The source can be divided into eight implementation blocks plus its export list:
| Block | Lines | Responsibility |
|---|---|---|
| Environment detection and shared constants | 21-384 | Host classification, PDF flags, annotation values, operator numbers, and logging levels |
| Diagnostics | 386-426 | Verbosity state, info, warn, unreachable, and assert |
| URLs, paths, and property definition | 428-519 | Protocol allowlisting, hash updates, path stripping, and shadow |
| Exceptions and byte strings | 524-616 | Typed failures plus binary string conversion |
FeatureTest | 618-718 | Lazy, cached host-capability probes |
Util | 720-1028 | Colors, affine transforms, scale recovery, rectangles, and Bézier bounds |
| Text, identity, and destinations | 1030-1127 | UTF-8 byte strings, Unicode normalization, UUID generation, and explicit-destination validation |
| Factories and compatibility patches | 1129-1164 | Reusable collection factories and small standard-library shims |
| Exports | 1166-1229 | Expose the shared constants, classes, helpers, and factories |
There are two especially useful themes to keep in mind:
- A constant can be a compatibility interface. Renumbering
OPSwould break consumers just as surely as changing a function signature. - Mutation is intentional, not accidental. Several hot-path methods write into caller-provided arrays to avoid allocations; other methods deliberately return copies. The distinction is part of each API.
Constants Are Contracts, Not Just Labels
Rendering intent and annotation modes
RenderingIntentFlag uses separate powers of two, from ANY through OPLIST, so independent facts can travel in one integer: display versus print, annotation handling, editing state, saving, and operator-list production.
AnnotationMode, by contrast, is a set of mutually exclusive numeric options: DISABLE, ENABLE, ENABLE_FORMS, and ENABLE_STORAGE. It is not another bit mask. WorkerTransport.getRenderingIntent in display/api.js translates the selected mode into at most one annotation flag, then adds the editing and operator-list flags when needed:
switch (annotationMode) {
case AnnotationMode.DISABLE:
renderingIntent += RenderingIntentFlag.ANNOTATIONS_DISABLE;
break;
case AnnotationMode.ENABLE:
break;
case AnnotationMode.ENABLE_FORMS:
renderingIntent += RenderingIntentFlag.ANNOTATIONS_FORMS;
break;
case AnnotationMode.ENABLE_STORAGE:
renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE;
annotationStorageSerializable = annotationStorage.serializable;
break;
default:
warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`);
}
if (isEditing) {
renderingIntent += RenderingIntentFlag.IS_EDITING;
}
if (isOpList) {
renderingIntent += RenderingIntentFlag.OPLIST;
}The source uses +=; it is safe here because each contributing flag is distinct and selected at most once. Consumers then recover facts with bitwise tests. For example, OptionalContentConfig checks DISPLAY and PRINT, while PDFPageProxy.render uses PRINT to select print-specific behavior. The resulting integer also participates in the rendering cache key, so these bits affect both behavior and cache identity.
The other constant families serve different contractual roles:
| Family | Contract |
|---|---|
PermissionFlag, AnnotationFlag, AnnotationFieldFlag | Bit masks for document permissions, annotation behavior, and form fields; permission values follow ISO 32000-1, Table 22 |
AnnotationType, AnnotationBorderStyleType, AnnotationReplyType, AnnotationRenditionOperation | Numeric or string vocabularies used when interpreting annotation dictionaries and rendition actions |
AnnotationActionEventType, DocumentActionEventType, PageActionEventType | Maps PDF trigger keys such as E, WC, and O to scripting event names |
AnnotationPrefix, AnnotationEditorPrefix | Namespaces for internal annotation/editor identifiers and editor storage keys |
AnnotationEditorType, AnnotationEditorParamsType | Internal editor kinds and parameter-update commands; these should not be confused with a single PDF-spec enumeration |
TextRenderingMode | PDF text modes 0 through 7, plus FILL_STROKE_MASK and ADD_TO_PATH_FLAG for decoding the packed modes |
ImageKind, MeshFigureType | Image storage layouts and mesh figure categories shared across parsing and rendering |
PasswordResponses, VerbosityLevel | Password request outcomes and diagnostic thresholds |
TextRenderingMode is a good example of why these values deserve explanation rather than blind copying. Modes 4 through 7 combine the low fill/stroke behavior with “add to path.” The canvas renderer therefore uses mode & FILL_STROKE_MASK for the basic operation and mode & ADD_TO_PATH_FLAG for clipping-path behavior; a long chain of equality checks would lose that structure.
Stable operation numbers
OPS assigns numeric identifiers to operator-list commands. Two comments in the source define its compatibility rules:
- numbering starts at
1, making an uninitialized zero easy to recognize; - removed entries remain as comments because renumbering would risk breaking third-party users.
The list consequently contains gaps at 78, 79, and 82. Those gaps are intentional. Worker code writes numeric function IDs into an operator list, and display code dispatches on those same IDs in canvas.js; the numbers are part of the data format, not merely switch labels.
DrawOPS has a different requirement. Its five path commands occupy the contiguous range 0 through 4, which the source explicitly keeps suitable for a fast switch jump table. makePathFromDrawOPS in display/display_utils.js consumes that compact representation. Stable numbering matters for OPS; dense numbering matters for DrawOPS.
Shared numeric constants
The remaining constants prevent tiny policy decisions from drifting between modules:
FONT_IDENTITY_MATRIXis[0.001, 0, 0, 0.001, 0, 0], the usual 1/1000 glyph-unit scale used as the font-matrix fallback.LINE_FACTOR = 1.35andLINE_DESCENT_FACTOR = 0.35encode the single-line field metrics Acrobat appears to use.BASELINE_FACTORis derived from their ratio rather than duplicated as another literal.BBOX_INITandF32_BBOX_INITprovide ordinary-array andFloat32Arraytemplates for empty accumulated bounds.SVG_NScentralizes the SVG namespace string used by DOM/SVG construction.Util.hexNumslazily builds all 256 two-digit hexadecimal bytes.Util.makeHexColor(r, g, b)indexes that table, so its inputs are expected to be byte values; it does not clamp or validate them.
These are small definitions, but they connect font rendering, annotation appearance, editor line layout, SVG generation, and color serialization to one shared policy.
Diagnostics and the Exception Hierarchy
The module owns one mutable diagnostic variable, initialized to VerbosityLevel.WARNINGS:
let verbosity = VerbosityLevel.WARNINGS;
function setVerbosityLevel(level) {
if (Number.isInteger(level)) {
verbosity = level;
}
}
function getVerbosityLevel() {
return verbosity;
}The setter accepts any integer, not only the three named levels. That is consistent with threshold-based output: warn emits when verbosity >= WARNINGS, while info requires verbosity >= INFOS. warn is explicitly non-fatal. unreachable, in contrast, always throws an Error, and assert routes a failed condition through it. These are programming-failure tools, not replacements for recoverable PDF parse errors.
BaseException supplies the common message/name shape for PDF.js exceptions. Its legacy prototype setup inherits from Error, and direct construction of BaseException itself is rejected in unbuilt or testing code. Derived classes add the information callers need to make decisions:
| Exception | Additional state | Meaning conveyed by the type |
|---|---|---|
PasswordException | code | Password is required or incorrect; PasswordResponses supplies the codes |
UnknownErrorException | details | An unrecognized failure plus diagnostic detail |
InvalidPDFException | — | The document is not a valid PDF |
ResponseException | status, missing | A network response failed, with status and missing-resource information |
FormatError | — | PDF data could not be parsed as expected |
AbortException | — | A task was cancelled rather than completed unsuccessfully |
This taxonomy is used across the worker boundary. message_handler.js reconstructs recognized exceptions from their name, preserving fields such as code, status, missing, and details. Consequently, these constructors are protocol definitions: removing a field would weaken error handling even if local code still compiled.
URL Safety, Path Handling, and Lazy Properties
createValidAbsoluteUrl: validation plus controlled recovery
createValidAbsoluteUrl returns a URL object or null; it does not return an arbitrary “fixed up” string. Its protocol allowlist contains http:, https:, ftp:, mailto:, and tel:. In particular, schemes such as javascript: and data: are rejected by the default branch.
Two recovery options apply only when the input is a string:
addDefaultProtocolprefixeshttp://to strings beginning withwww., but only when at least two dots are present. This avoids interpreting a relative filename such aswww.pdfas a website.tryConvertEncodingcallsstringToUTF8Stringfor malformed PDFs that stored UTF-8 bytes despite the specification's 7-bit ASCII URI expectation. Conversion failure is deliberately ignored, leaving the original string for normal URL parsing.
The final step is small but decisive:
const absoluteUrl = baseUrl ? URL.parse(url, baseUrl) : URL.parse(url);
return _isValidProtocol(absoluteUrl) ? absoluteUrl : null;URL.parse resolves relative input when a usable absolute base is supplied, and protocol validation happens after resolution. A falsey input or an unparseable/disallowed URL produces null.
A real trust-boundary example appears in core/catalog.js: link parsing passes both recovery options, assigns resultObj.url only when validation succeeds, and separately preserves the original value as resultObj.unsafeUrl. The raw PDF value is not silently rewritten—but neither is it promoted to the safe navigation field.
updateUrlHash and stripPath
updateUrlHash first tries URL.parse, updates the parsed URL's hash, and returns href. Its optional relative-URL branch is more subtle:
if (allowRel && createValidAbsoluteUrl(url, 'http://example.com')) {
return url.split('#', 1)[0] + `${hash ? `#${hash}` : ''}`;
}
return '';The dummy origin is used only to validate a well-formed relative reference. The returned value remains relative; it is not an unexpected http://example.com URL. Setting hash to an empty string removes the fragment. Without allowRel, an input that cannot be parsed absolutely returns an empty string. display/filter_factory.js uses this helper to remove a URL fragment when establishing its base URL.
stripPath has a much narrower contract: it returns everything after the final forward slash. It is not a general filesystem-path normalizer and does not treat backslashes as separators.
shadow: replacing a getter with a cached value
Many PDF.js objects expose expensive or one-shot values through getters. shadow replaces such a property with a defined data value and returns that value:
function shadow(obj, prop, value, nonSerializable = false) {
if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('TESTING')) {
assert(prop in obj, `shadow: Property "${prop && prop.toString()}" not found in object.`);
}
Object.defineProperty(obj, prop, {
value,
enumerable: !nonSerializable,
configurable: true,
writable: false,
});
return value;
}The result is non-writable but configurable, and it is non-enumerable when nonSerializable is true. In testing, prop in obj catches misspelled or entirely new properties: the helper is meant to shadow an existing accessor, not invent arbitrary API surface. FeatureTest, WorkerTransport.annotationStorage, and numerous other getters reuse this exact caching mechanism.
Byte Strings, UTF-8, and Unicode Normalization
Binary strings are not Unicode text
PDF internals often represent a sequence of bytes as a JavaScript string whose code units are in the byte range. bytesToString and stringToBytes serve that representation; they are not general UTF-8 encoder/decoder functions.
For short input, bytesToString can call String.fromCharCode.apply directly. Larger input is chunked to avoid exceeding engine argument-count limits:
const MAX_ARGUMENT_COUNT = 8192;
if (length < MAX_ARGUMENT_COUNT) {
return String.fromCharCode.apply(null, bytes);
}
const strBuf = [];
for (let i = 0; i < length; i += MAX_ARGUMENT_COUNT) {
const chunkEnd = Math.min(i + MAX_ARGUMENT_COUNT, length);
const chunk = bytes.subarray(i, chunkEnd);
strBuf.push(String.fromCharCode.apply(null, chunk));
}
return strBuf.join('');The fast path accepts array-like objects with a numeric length; the chunked path additionally requires subarray, which typed arrays provide. stringToBytes performs the inverse storage operation into a new Uint8Array, keeping only the low eight bits of each code unit with charCodeAt(i) & 0xff.
Callers reflect this binary contract. The parser and font code use bytesToString for signatures and font data, while getDataProp uses stringToBytes when binary PDF input arrives as a string. Neither helper should be substituted for TextDecoder when the data format actually requires character-set decoding.
The legacy UTF-8 conversion pair
stringToUTF8String and utf8StringToString convert between UTF-8 byte strings and ordinary JavaScript strings:
function stringToUTF8String(str) {
return decodeURIComponent(escape(str));
}
function utf8StringToString(str) {
return unescape(encodeURIComponent(str));
}These functions rely on legacy escape/unescape, but their direction is clear: the first decodes UTF-8 bytes held in a binary string; the second encodes a JavaScript string back into that byte-string form. Malformed input can make the percent-decoding step throw. Higher-level code such as metadata, XMP, datasets, and password handling uses these where the surrounding format has already established the expected representation.
normalizeUnicode: selective NFKC, not blanket normalization
Running NFKC over every extracted text chunk would do unnecessary work and could change characters PDF.js does not intend to fold. normalizeUnicode instead lazily constructs a regular expression covering the specific BMP characters and presentation forms known to require normalization, then replaces matching runs with their NFKC form. A separate map handles ſt as ſt.
The important design point is selectivity. In core/evaluator.js, text chunks are joined, passed through normalizeUnicode unless disableNormalization is set, and only then sent to the bidi algorithm. Normalization is therefore part of text-extraction policy, not an implicit side effect of every string helper in this module.
Feature Detection and Environment Boundaries
isNodeJS: rejecting browser lookalikes
isNodeJS is computed once at module evaluation. It requires a generic or unbuilt source context, a genuine-looking process object, and the absence of NW.js. Electron renderer-like processes are excluded, while Electron's browser process type remains eligible.
That extra care matters because NW.js and Electron can expose Node-shaped objects inside a browser context. A simple typeof process !== "undefined" check would select Node-only factories or networking behavior in the wrong realm. display/node_utils.js relies on the result before loading Node canvas facilities, and display/api.js uses it for defaults such as workers, fetch, and font handling.
FeatureTest: probe once, cache with shadow
Each FeatureTest static getter computes a result and shadows itself on the class. Subsequent reads do not repeat the probe:
| Getter | Probe and purpose |
|---|---|
isLittleEndian | Writes byte 1 into a Uint8Array, reads the same buffer as Uint32Array, and compares with 1; image pixel packing depends on the answer |
isOffscreenCanvasSupported | Checks for OffscreenCanvas; used by canvas and image paths |
isImageDecoderSupported | Checks for the ImageDecoder global; this is API-presence detection, not a claim that every codec is supported |
isFloat16ArraySupported | Checks for Float16Array, allowing float16-specific transform and font paths |
isSanitizerSupported | Checks for the Sanitizer global |
platform | Derives OS booleans from navigator.platform and navigator.userAgent; Firefox is detected from the user agent or forced for MOZCENTRAL builds |
isCanvasFilterSupported | Obtains an Offscreen or DOM 2D context and tests whether the spec-defined default filter property exists |
isAlphaColorInputSupported | Creates a color input with an alpha attribute and checks whether an eight-digit color retains alpha rather than becoming #ff0000 |
isBackdropFilterSupported | Uses CSS.supports("backdrop-filter", "blur(1px)") when CSS exists |
Two probes demonstrate why existence checks are not always enough. For canvas filters, Safari can acquire an ordinary JavaScript filter property after assignment without actually implementing Canvas2D filtering. Testing the default value before assignment avoids that false positive. For alpha color input, the implementation checks behavior—what value the control retains—instead of merely checking that an input element can be created.
The cache is a deliberate tradeoff: platform and capability answers are treated as stable for the lifetime of the realm. It avoids repeated DOM/canvas setup, but it also means these getters are not intended to observe dynamic changes after their first read.
Affine Transforms and Scale Recovery
Util represents a 2D affine transform as [a, b, c, d, e, f], matching canvas and PDF conventions:
x' = m[0] * x + m[2] * y + m[4]
y' = m[1] * x + m[3] * y + m[5]Util.transform(m1, m2) returns the composition equivalent to applying m2 and then m1. multiplyByDOMMatrix performs the same array-times-matrix operation using md.a through md.f, avoiding a conversion of the DOMMatrix into another six-element array. canvas_dependency_tracker.js uses this form directly.
The point routines have more varied mutation contracts:
| Method | Result and mutation contract |
|---|---|
applyTransform(p, m, pos = 0) | Mutates one coordinate pair in p; pos permits packed coordinate arrays |
applyTransformToBezier(p, transform, pos = 0) | Mutates three consecutive coordinate pairs: the two control points and endpoint stored for a PDF curveTo; the current point comes from preceding path data |
inverseTransform(m) | Returns a new six-element inverse matrix |
applyInverseTransform(p, m) | Mutates one pair using the inverse equations directly |
singularValueDecompose2dScale(matrix, output) | Writes two scale components into the supplied output array |
Both inverse paths divide by the determinant m[0] * m[3] - m[1] * m[2]; they do not handle a singular matrix. Invertibility is therefore a caller precondition, not something these hot-path helpers repair.
singularValueDecompose2dScale is more than a width/height approximation. It forms the symmetric product of the linear 2×2 portion, solves for its eigenvalues, and takes square roots to obtain singular-value scales. This remains meaningful under rotation and skew, unlike reading only m[0] and m[3]. The || 1 fallback prevents an exactly zero scale component from propagating. Canvas and pattern code use the resulting scales for scale-sensitive drawing decisions.
The allocation pattern is consistent: composition returns a new array, point application mutates existing packed buffers, and scale recovery writes to an output buffer. Those choices let rendering hot paths control allocation rather than creating temporary arrays for every operation.
Rectangles and Exact Bounding Boxes
Accumulated bounds and rectangle operations
Bounding-box routines use [minX, minY, maxX, maxY] and expand a caller-provided accumulator:
const BBOX_INIT = [Infinity, Infinity, -Infinity, -Infinity];
const F32_BBOX_INIT = new Float32Array(BBOX_INIT);The infinities make the first point or rectangle replace the empty bounds naturally. Callers normally slice() the template or copy it into a typed array before mutation; the exported template itself should not become one document's accumulated result.
pointBoundingBox and rectBoundingBox update all four extrema in place. axialAlignedBoundingBox(rect, transform, output) transforms rectangle corners and unions them into output. When m1 and m2 are both zero, it uses a scaling/translation fast path; otherwise it includes the rotation/skew terms and all four corners. This is why transformed bounds remain axis-aligned without assuming that two opposite corners are sufficient.
Other rectangle APIs deliberately differ:
normalizeRectclones the input and swaps reversed coordinates in the clone, leaving the original untouched.intersecthandles reversed endpoints throughMath.min/Math.max, returnsnullonly when an axis is disjoint, and returns a zero-width or zero-height rectangle when boxes merely touch.axialAlignedBoundingBox,pointBoundingBox, andrectBoundingBoxmutate their accumulator rather than returning a fresh rectangle.
Canvas clipping, annotation rectangles, patterns, operator-list bounds, and the dependency tracker all build on these conventions. core/document.js, for example, intersects crop and media boxes; display code intersects computed bounds with canvas or clip bounds to avoid unnecessary work.
Cubic Bézier bounds without flattening the curve
A cubic curve's control-point hull is easy to compute but can be much larger than the curve's actual bounds. Util.bezierBoundingBox instead includes the two endpoints, finds derivative extrema independently for x and y, and evaluates only roots strictly inside the open interval from 0 to 1.
The private helpers handle three numerical branches:
- a near-zero quadratic coefficient falls back to the linear root when possible;
- a negative discriminant contributes no real extrema;
- otherwise both quadratic roots are tested, while
#getExtremumOnCurverejects roots outside the curve interval.
This is analytic bound computation, not curve flattening or sampling. core/evaluator.js uses it while accumulating path bounds, and the ink/free-draw editor code uses the same routine for user-drawn geometry. One implementation therefore keeps parser and editor bounds consistent.
Destination Validation, Identity, and Collection Factories
_isValidExplicitDest: shared structure, injected PDF types
An explicit destination is structurally an array [page, zoom, ...args]. Validation needs PDF reference and name types, but importing core primitives into this shared module would create the wrong dependency direction. _isValidExplicitDest(validRef, validName, dest) instead receives the two predicates.
The same algorithm then runs on both sides of the architecture:
core/catalog.jsbinds the realReftest andisNamepredicate;display/api_utils.jsbinds structural proxies with integernum/genfields and a stringname.
The validator checks structure, not whether a page actually exists. The page must satisfy validRef or be an integer; zoom must satisfy validName; then the zoom name determines the argument contract:
| Zoom | Accepted arguments after zoom |
|---|---|
XYZ | Two or three numeric/nullable arguments |
Fit, FitB | No arguments |
FitH, FitBH, FitV, FitBV | Zero or one numeric/nullable argument |
FitR | Exactly four numeric arguments; null is not allowed |
| Anything else | Invalid |
All other accepted arguments must be numbers, with null allowed except for FitR. Catalog destination parsing and display-side validation consequently agree on shape without sharing concrete worker-only classes.
Opaque identity, shallow equality, and reusable factories
getUuid prefers crypto.randomUUID() in Firefox builds or whenever that function exists. Its fallback generates 32 random bytes and returns bytesToString(buf):
function getUuid() {
if (
(typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) ||
typeof crypto.randomUUID === 'function'
) {
return crypto.randomUUID();
}
const buf = new Uint8Array(32);
crypto.getRandomValues(buf);
return bytesToString(buf);
}The fallback is an opaque 32-code-unit random identifier, not a hyphenated RFC UUID. Callers must rely on uniqueness, not formatting. ImageManager in the editor combines this random base with an incrementing counter to form image IDs.
Three smaller helpers also have precise limits:
isArrayEqualcompares lengths and indexed elements with!==. It is shallow, works for array-like indexed sequences such as typed arrays, and does not recursively compare nested arrays.makeArr,makeMap,makeObj, andmakeSetare shared factory functions forMap/RefCachecomputed-insertion APIs. Reusing them avoids creating a new closure at every call site.makeObjreturnsObject.create(null), producing a dictionary without inherited keys such asconstructorortoString.
Real call sites pass these factories to getOrInsertComputed or related cache APIs—for example, nested bitmap caches, popup arrays, annotation-storage maps, and glyph sets. The factory's job is only to create the empty value; ownership and insertion remain with the collection API.
Runtime Compatibility Patches
The file ends with three minimal standard-library shims. They centralize compatibility so feature-specific modules can call the modern API directly.
For Blob and Response, a missing .bytes() method is implemented in terms of .arrayBuffer():
Blob.prototype.bytes = async function () {
return new Uint8Array(await this.arrayBuffer());
};The patches are installed only in built contexts where PDFJSDev exists, Babel processing is not skipped, and the native method is absent. Existing native implementations are never replaced. The returned value is consistently a Uint8Array, which is what PDF writer, image, and fetch-related callers consume.
Iterator.prototype.join has a separate fallback:
if (typeof Iterator.prototype.join !== 'function') {
// eslint-disable-next-line no-extend-native
Iterator.prototype.join = function (separator) {
return [...this].join(separator);
};
}This shim is intentionally simple: it exhausts the iterator into an array and delegates to Array.prototype.join. That provides the expected string operation, but it is neither streaming nor constant-memory and is suitable only for finite iterators. The tradeoff is explicit in the implementation—minimal semantics in one place, rather than scattered iterator-to-array conversions.
These patches do modify global prototypes, so their placement at module scope is significant. They are compatibility infrastructure with realm-wide effects, not pure helper functions like stringToBytes or Util.transform.
Summary
util.js is the shared contract layer beneath PDF.js's more visible parser and renderer architecture. Its most important lessons are not individual convenience functions, but the boundaries those functions preserve:
- Numbers are interfaces. PDF flags, text-mode bit packing,
OPScompatibility gaps, and denseDrawOPSvalues must be interpreted according to their consumers. - Errors carry decisions. Exception names and extra fields survive worker transport so callers can distinguish passwords, invalid documents, response failures, cancellation, and unknown errors.
- Validation does not mean silent rewriting. URL recovery is narrowly scoped, protocol allowlisting happens after parsing, and raw unsafe values remain separate from validated navigation URLs.
- Binary strings and Unicode are different representations. Byte truncation, chunked
fromCharCode, explicit UTF-8 conversion, and selective NFKC normalization solve different problems and are not interchangeable. - Host adaptation is centralized.
isNodeJS, cachedFeatureTestprobes, and small prototype shims prevent environment branches from leaking throughout rendering code. - Geometry APIs expose their cost model. New arrays, in-place point updates, output accumulators, analytic Bézier extrema, and lazy caches are deliberate choices for hot rendering paths.
Once these contracts are clear, many apparently unrelated PDF.js modules become easier to read: the same flags determine cache behavior, the same exceptions cross threads, the same matrix layout drives text and patterns, and the same bounding-box accumulator serves parser and editor geometry.
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: MessageHandlerOctober 27, 2026
- PDF.js Source Walkthrough: apiOctober 27, 2026
- PDF.js Source Walkthrough: fontsOctober 27, 2026
- PDF.js Source Walkthrough: glyphlistOctober 27, 2026
- PDF.js Source Walkthrough: binary_cmapOctober 27, 2026
- PDF.js Source Walkthrough: cmapOctober 27, 2026
- PDF.js Source Walkthrough: encodingsOctober 27, 2026
- PDF.js Source Walkthrough: metricsOctober 27, 2026
- PDF.js Source Walkthrough: 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