StreamPDFStreamPDF
Back to blog

PDF.js Source Walkthrough: css_utils.js

October 27, 2026 · StreamPDF Team

PDF.js Source Walkthrough: css_utils.js

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:

BlockLinesResponsibility
CONTROL_CHAR_REGEXP16Recognize Unicode control characters in category Cc
isCSSString25-50Decide whether an existing value can safely be reused verbatim as a CSS string
serializeFontFamily58-74Return a safely quoted and escaped font-family value
Exports76Export 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 Cc control 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:

CallerUse of the returned valueWhy the shared contract matters
FontFaceLoader.createNativeFontFaceFamily argument to new FontFace(...)Registers the native font under the serialized family name
FontFaceLoader.createFontFaceRulefont-family inside an @font-face rulePrevents PDF data from breaking out of the declaration and injecting CSS
XFA setFontFamilystyle.fontFamily, including substituted family namesUses the same representation as font registration so the names match
validateFontNameReuses CONTROL_CHAR_REGEXP, not the serializerRejects 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:

  1. Reuse an existing value only when isCSSString proves that its delimiters and escapes are safe.
  2. Otherwise emit a quoted CSS string, avoiding generic-family and CSS-wide-keyword interpretations.
  3. Escape quotes, backslashes, and control characters according to CSS string syntax, including the required hexadecimal-escape terminator.
  4. 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