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

Introduction
This installment examines src/shared/css_utils.js, a compact module at the boundary between PDF font data and browser CSS.
Despite its broad filename, the current source contains no CSS-unit conversion, zoom calculation, or transformation-matrix helper. Its actual scope is narrower and specific: serialize font-family names so that data originating in a PDF can be used as a CSS string without changing the surrounding CSS grammar.
That distinction matters. A font name is not necessarily a safe identifier: it may contain quotes, backslashes, control characters, or a word such as inherit. Passing it directly into a FontFace constructor or an @font-face rule would confuse data with syntax. This module defines the serialization contract used at those boundaries.
Module Overview
The file has one constant, one private predicate, and one public serializer:
| Block | Lines | Responsibility |
|---|---|---|
CONTROL_CHAR_REGEXP | 16 | Recognize Unicode control characters in category Cc |
isCSSString | 25-50 | Decide whether an existing value can safely be reused verbatim as a CSS string |
serializeFontFamily | 58-74 | Return a safely quoted and escaped font-family value |
| Exports | 76 | Export the control-character regexp and the serializer; keep the predicate private |
The public contract is deliberately small: given a JavaScript string, return CSS source text representing one string token. The function does not load a font, select a fallback, normalize font metrics, or calculate any visual geometry.
CONTROL_CHAR_REGEXP and isCSSString(): Deciding When Reuse Is Safe
const CONTROL_CHAR_REGEXP = /\p{Cc}/u;The Unicode property escape targets category Cc, the control-character category. The regexp has no global flag, so repeated .test() calls do not carry lastIndex state. It is also exported for the related font-name validation path in src/core/core_utils.js.
isCSSString is the private fast-path guard:
function isCSSString(str) {
const quote = str[0];
if (str.length < 2 || (quote !== `"` && quote !== `'`) || str.at(-1) !== quote) {
return false;
}
const end = str.length - 1;
for (let i = 1; i < end; i++) {
const char = str[i];
if (char === quote || CONTROL_CHAR_REGEXP.test(char)) {
return false;
}
if (char === '\\') {
if (++i >= end || CONTROL_CHAR_REGEXP.test(str[i])) {
return false;
}
}
}
return true;
}The checks form one coherent contract rather than a general CSS parser:
- the value must begin and end with the same single or double quote;
- an unescaped instance of that quote inside the body is rejected;
- control characters are rejected even when they follow a backslash;
- a backslash consumes the following character during the scan, so an escaped quote does not look like a delimiter;
- a backslash immediately before the closing quote is rejected because it would escape that closing quote.
An empty quoted string passes, and the opposite quote may appear unescaped inside the string. Those are valid outcomes for this focused predicate. If the value is not already a reusable CSS string, the serializer takes the safer path instead of trying to patch the original form.
serializeFontFamily(): Producing a CSS String Token
function serializeFontFamily(fontFamily) {
if (isCSSString(fontFamily)) {
return fontFamily;
}
const escaped = fontFamily.replaceAll(/["\\\p{Cc}]/gu, char =>
char === `"` || char === '\\' ? `\\${char}` : `\\${char.codePointAt(0).toString(16)} `,
);
return `"${escaped}"`;
}There are two paths. A value that already satisfies isCSSString is returned unchanged, preserving either quote style. Every other value is treated as font-family data, escaped, and wrapped in double quotes. The result is therefore always a CSS string token, but not necessarily a newly constructed one.
Why a quoted string instead of a custom identifier?
CSS also permits font families written as identifier sequences, but that representation has additional grammar and keyword rules. A PDF font literally named serif must not silently become the generic family selector, and a name such as inherit must not become a CSS-wide keyword. A quoted string preserves the name as data and avoids both interpretations.
This is serialization, not HTML escaping or URL encoding. Its output is meant for CSS-specific consumers, not for direct insertion into arbitrary HTML or JavaScript contexts.
Escaping rules and concrete results
The fallback handles exactly the characters that need special treatment in its double-quoted output:
"becomes\";\becomes\\;- each
Cccontrol character becomes a hexadecimal CSS escape followed by a space.
The trailing space terminates a hexadecimal escape. Without it, a following hexadecimal digit could be consumed as part of the escape and change the family name. Hexadecimal escapes also avoid relying on a backslash followed by a literal line terminator, which has different CSS semantics.
The following examples show CSS source text; A<newline>B denotes a value containing an actual newline:
Font-family value Emitted CSS text
Acme "Acme"
A"B "A\"B"
A\B "A\\B"
A<newline>B "A\a B"A pre-quoted value such as "Acme" is returned unchanged rather than quoted a second time.
Caller contract: the same name at every CSS boundary
The serializer is used wherever the same PDF-originated family name enters CSS through a different API:
| Caller | Use of the returned value | Why the shared contract matters |
|---|---|---|
FontFaceLoader.createNativeFontFace | Family argument to new FontFace(...) | Registers the native font under the serialized family name |
FontFaceLoader.createFontFaceRule | font-family inside an @font-face rule | Prevents PDF data from breaking out of the declaration and injecting CSS |
XFA setFontFamily | style.fontFamily, including substituted family names | Uses the same representation as font registration so the names match |
validateFontName | Reuses CONTROL_CHAR_REGEXP, not the serializer | Rejects unsuitable quoted names during core-side validation |
The last row is a useful separation of responsibilities: validation decides whether a name is acceptable on that core path, while serialization safely represents a name at the CSS output boundary. One does not replace the other.
Summary
css_utils.js is currently a font-family serialization module, not a general CSS geometry toolkit. Its design rests on four precise decisions:
- Reuse an existing value only when
isCSSStringproves that its delimiters and escapes are safe. - Otherwise emit a quoted CSS string, avoiding generic-family and CSS-wide-keyword interpretations.
- Escape quotes, backslashes, and control characters according to CSS string syntax, including the required hexadecimal-escape terminator.
- Share the same serialization across
FontFace,@font-face, and XFA styles so registration and consumption agree on the family name.
Understanding this contract is more useful than treating the filename as a collection of unrelated CSS helpers: the module protects one specific data-to-syntax boundary and does so without taking on rendering or layout responsibilities.
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: 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: 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