StreamPDFStreamPDF
Back to blog

PDF.js Source Walkthrough: obj_bin_transform_utils

October 27, 2026 · StreamPDF Team

PDF.js Source Walkthrough: obj_bin_transform_utils

Introduction

The 83-line src/shared/obj_bin_transform_utils.js neither compiles nor decodes binary objects. It defines the agreement shared by the worker-side writer and the main-thread reader: field order, fixed offsets, pattern-header positions, and the text codecs used at both ends.

The core and display articles follow the actual writing and reading of font, pattern, and glyph-path buffers. This article stays at the schema layer. The central problem is that an ArrayBuffer carries no property names: unless both sides import the same layout, a correct write can still become a misaligned read.

Only InfoUtils contains behavior. The other four exports are classes made entirely of static schema data; there is no general-purpose serializer hidden behind their names.

Module Overview

ExportSchema or serviceConsumer-facing role
CSS_FONT_INFOThree ordered string namesIndexes the standalone CSS-font string block
SYSTEM_FONT_INFOFour ordered string namesIndexes the regular system-font string block
FONT_INFOBoolean, number, and string order plus five offsetsDefines the fixed prefix of a compiled font record
PATTERN_INFOEight byte offsetsLocates flags and counts in the 20-byte pattern header
InfoUtilsLazily installed TextEncoder and TextDecoderReuses UTF-8 codecs without caching field values

The dependency direction is deliberately small: core and display import this module, while this module imports only shadow from util.js. Neither side's buffer implementation becomes a dependency of the other.

CSS_FONT_INFO and SYSTEM_FONT_INFO: Property Order Is the Wire Format

class CSS_FONT_INFO {
  static strings = ['fontFamily', 'fontWeight', 'italicAngle'];
}
 
class SYSTEM_FONT_INFO {
  static strings = ['css', 'loadedName', 'baseFontName', 'src'];
}

These arrays do more than list expected properties. Their indexes are the protocol:

IndexCSS_FONT_INFO.stringsSYSTEM_FONT_INFO.strings
0fontFamilycss
1fontWeightloadedName
2italicAnglebaseFontName
3—src

The core writer loops over an array to encode each value; the display reader uses the same array length to validate an index and scans length-prefixed entries until it reaches that index. Property names never appear in the buffer, and entries have no individual type tags. Reordering an array therefore changes meaning even when the property set stays unchanged.

These classes do not define the surrounding length prefixes, the system-font guessFallback byte, or its optional style block. Those details belong to the writer and reader; the shared layer fixes only the order that both must follow.

FONT_INFO: Three Field Groups and a Computed Offset Chain

Field groups determine indexes and widths

static bools = [
  "black",
  "bold",
  "disableFontFace",
  "fontExtraProperties",
  "isInvalidPDFjsFont",
  "isType3Font",
  "italic",
  "missingFile",
  "remeasure",
  "vertical",
];
 
static numbers = ["ascent", "defaultWidth", "descent"];
static strings = ["fallbackName", "loadedName", "mimetype", "name"];

The boolean group uses two bits per entry, allowing four entries per byte. Core emits 00 for undefined, 01 for false, and 10 for true; this is why the schema computes a bitfield size rather than a byte-per-boolean size. The number group contains three eight-byte values. The string group again makes array position, not a serialized key, the field identity.

Offsets are derived, not independently numbered

static OFFSET_NUMBERS = Math.ceil((this.bools.length * 2) / 8);
static OFFSET_BBOX = this.OFFSET_NUMBERS + this.numbers.length * 8;
static OFFSET_FONT_MATRIX = this.OFFSET_BBOX + 1 + 2 * 4;
static OFFSET_DEFAULT_VMETRICS = this.OFFSET_FONT_MATRIX + 1 + 8 * 6;
static OFFSET_STRINGS = this.OFFSET_DEFAULT_VMETRICS + 1 + 2 * 3;

Inside static field initializers, this is FONT_INFO, so each expression builds on the schema declared immediately before it. With the current arrays, the boundaries are:

ConstantValueDerivationRegion beginning there
OFFSET_NUMBERS3Ten two-bit booleans, rounded up to bytesThree Float64 values
OFFSET_BBOX273 + 3 * 8Length byte plus four Int16 slots
OFFSET_FONT_MATRIX3627 + 1 + 4 * 2Length byte plus six Float64 slots
OFFSET_DEFAULT_VMETRICS8536 + 1 + 6 * 8Length byte plus three Int16 slots
OFFSET_STRINGS9285 + 1 + 3 * 2Total string-block length, then the variable tail

The 1 in each array-region formula is its length marker. The payload width remains in the offset calculation even when the writer marks an array absent, so later regions never move. Some numeric offsets are unaligned; readers use DataView, so the schema does not insert alignment padding.

OFFSET_STRINGS is not the end of the complete font record. It marks the start of the variable-length tail containing the string block, nested system/CSS information, and font data. Those later regions have lengths rather than shared absolute offsets because their positions depend on earlier content.

The formulas prevent duplicated arithmetic, but they do not make every schema change safe. Changing the number of booleans or numbers updates downstream offsets; reordering names changes field semantics without changing any offset. The fixed array counts and element widths are also embedded directly in the formulas, so they must change together with core and display.

PATTERN_INFO: Byte Positions in the Pattern Header

ConstantOffsetWidth used by the protocolMeaning
KIND01 byte1 axial, 2 radial, 3 mesh
HAS_BBOX11 byteWhether the optional bounding box is present
HAS_BACKGROUND21 byteWhether a mesh background is present
SHADING_TYPE31 byteMesh shading type; unused for other kinds
N_COORD44 bytesNumber of coordinate pairs
N_COLOR84 bytesNumber of RGBA-stride color entries
N_STOP124 bytesNumber of gradient stops
N_FIGURES164 bytesNumber of figures; currently written as zero and not read

The four N_* values are entry counts, not byte counts. Their positions leave a 20-byte header, after which core and display both start the payload. That 20 is not exported as a HEADER_SIZE constant, and body offsets are not described here: the shared class covers only the header vocabulary.

Current core writes zero to N_FIGURES, and current display does not consult it. It should therefore be read as reserved capacity in this version, not as evidence of an implemented figure payload. The class itself performs no kind validation, count validation, or buffer-size calculation.

InfoUtils.decoder and InfoUtils.encoder: Cached Codecs, Not Cached Values

class InfoUtils {
  static get decoder() {
    return shadow(this, 'decoder', new TextDecoder());
  }
 
  static get encoder() {
    return shadow(this, 'encoder', new TextEncoder());
  }
}

On first access, this is InfoUtils. shadow replaces the static accessor with a data property containing the codec and returns that instance, so later property reads reuse it instead of running the getter again.

The defaults matter: these codecs convert between JavaScript strings and UTF-8 bytes. The cache holds only the codec objects. It does not memoize encoded byte arrays, decoded strings, or any font property; display getters can still scan and decode again on every access. Each JavaScript realm also has its own module state—no codec object crosses the worker boundary with the buffer.

Protocol Boundaries: What This Module Does Not Guarantee

  • No version negotiation. There is no magic number, schema version, or feature mask in these classes. Writer and reader are expected to come from the same compatible PDF.js build.
  • No byte-order policy. Offsets and widths live here, but each DataView setter or getter in core/display determines big-endian, little-endian, or native typed-array interpretation.
  • No validation layer. Bounds checks and layout assertions live in the implementations. Importing these constants does not make an arbitrary buffer safe to decode.
  • No complete object schema. The variable font tail, pattern body, and raw glyph-path representation require additional agreements in core and display. In particular, raw font paths have no field definition in this file.

These omissions keep the shared module small, but they also mean it is one part of the protocol rather than a standalone binary format specification.

Summary

obj_bin_transform_utils.js is the shared vocabulary of PDF.js's font and pattern binary protocol. CSS_FONT_INFO, SYSTEM_FONT_INFO, and FONT_INFO turn property order into indexes; FONT_INFO derives fixed boundaries from field widths; PATTERN_INFO names the header positions; and InfoUtils reuses UTF-8 codecs on both sides.

Its key design decision is to share the schema without coupling the writer to the reader. The trade-off is equally clear: constants prevent offset drift, but ordering, byte order, versioning, and the remaining payload layout still require disciplined changes on both sides.

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