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

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
| Export | Schema or service | Consumer-facing role |
|---|---|---|
CSS_FONT_INFO | Three ordered string names | Indexes the standalone CSS-font string block |
SYSTEM_FONT_INFO | Four ordered string names | Indexes the regular system-font string block |
FONT_INFO | Boolean, number, and string order plus five offsets | Defines the fixed prefix of a compiled font record |
PATTERN_INFO | Eight byte offsets | Locates flags and counts in the 20-byte pattern header |
InfoUtils | Lazily installed TextEncoder and TextDecoder | Reuses 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:
| Index | CSS_FONT_INFO.strings | SYSTEM_FONT_INFO.strings |
|---|---|---|
| 0 | fontFamily | css |
| 1 | fontWeight | loadedName |
| 2 | italicAngle | baseFontName |
| 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:
| Constant | Value | Derivation | Region beginning there |
|---|---|---|---|
OFFSET_NUMBERS | 3 | Ten two-bit booleans, rounded up to bytes | Three Float64 values |
OFFSET_BBOX | 27 | 3 + 3 * 8 | Length byte plus four Int16 slots |
OFFSET_FONT_MATRIX | 36 | 27 + 1 + 4 * 2 | Length byte plus six Float64 slots |
OFFSET_DEFAULT_VMETRICS | 85 | 36 + 1 + 6 * 8 | Length byte plus three Int16 slots |
OFFSET_STRINGS | 92 | 85 + 1 + 3 * 2 | Total 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
| Constant | Offset | Width used by the protocol | Meaning |
|---|---|---|---|
KIND | 0 | 1 byte | 1 axial, 2 radial, 3 mesh |
HAS_BBOX | 1 | 1 byte | Whether the optional bounding box is present |
HAS_BACKGROUND | 2 | 1 byte | Whether a mesh background is present |
SHADING_TYPE | 3 | 1 byte | Mesh shading type; unused for other kinds |
N_COORD | 4 | 4 bytes | Number of coordinate pairs |
N_COLOR | 8 | 4 bytes | Number of RGBA-stride color entries |
N_STOP | 12 | 4 bytes | Number of gradient stops |
N_FIGURES | 16 | 4 bytes | Number 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
DataViewsetter 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
- 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: image_utils.jsOctober 27, 2026
- PDF.js Source Walkthrough: BasePDFStreamOctober 27, 2026
- PDF.js Source Walkthrough: Shared UtilitiesOctober 27, 2026
- PDF.js Source Walkthrough: MessageHandlerOctober 27, 2026
- PDF.js Source Walkthrough: apiOctober 27, 2026
- PDF.js Source Walkthrough: fontsOctober 27, 2026
- PDF.js Source Walkthrough: glyphlistOctober 27, 2026
- PDF.js Source Walkthrough: binary_cmapOctober 27, 2026
- PDF.js Source Walkthrough: cmapOctober 27, 2026
- PDF.js Source Walkthrough: encodingsOctober 27, 2026
- PDF.js Source Walkthrough: metricsOctober 27, 2026
- PDF.js Source Walkthrough: 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