StreamPDFStreamPDF
Back to blog

PDF.js Source Walkthrough: scripting_utils

October 27, 2026 · StreamPDF Team

PDF.js Source Walkthrough: scripting_utils

Introduction

src/shared/scripting_utils.js contains the small value-conversion layer shared by PDF.js's scripting and annotation paths. It exports ColorConverters, DateFormats, and TimeFormats; it does not execute JavaScript actions, parse dates, or create annotation elements itself.

That scope is intentional. The file header warns that display and scripting bundles both import it, so added dependencies can enlarge otherwise unrelated build outputs. Its sole import is MathClamp, and its implementation stays with numeric conversion and constant lookup.

Module Overview

Export or helperExported?Responsibility
makeColorComp(n)NoClamp a normalized component, floor its 0–255 value, and format two hexadecimal digits
scaleAndClamp(x)NoClamp a normalized component and scale it to 0–255 without rounding
ColorConvertersYesConvert among tagged PDF.js colors, CSS colors, and annotation-storage RGB values
DateFormatsYesFourteen index-based Acrobat date format presets
TimeFormatsYesFour index-based Acrobat time format presets

The source cites section 10.3 of the PDF specification above ColorConverters. The implementation performs direct component arithmetic; it is not an ICC color-management engine and does not add profile or gamma handling.

makeColorComp and scaleAndClamp: Similar Inputs, Different Outputs

function makeColorComp(n) {
  return Math.floor(MathClamp(n, 0, 1) * 255)
    .toString(16)
    .padStart(2, '0');
}
 
function scaleAndClamp(x) {
  return MathClamp(x, 0, 1) * 255;
}

Both helpers expect a normalized component and clamp it to the closed interval from 0 to 1. Their output contracts differ:

  • scaleAndClamp returns a number from 0 to 255 and preserves fractions. A midpoint becomes 127.5.
  • makeColorComp applies Math.floor, then returns exactly two hexadecimal digits for the clamped byte. The same midpoint becomes 7f, not 80.

The unit tests make that distinction visible:

const color = [0.5];
expect(ColorConverters.G_rgb(color)).toEqual([127.5, 127.5, 127.5]);
expect(ColorConverters.G_HTML(color)).toEqual('#7f7f7f');

Neither helper is exported, and neither validates non-numeric input. Higher-level scripting code checks color arrays before conversion; these functions should not be mistaken for general CSS color parsers.

ColorConverters: The Fourteen Actual Conversion Methods

Naming and return shapes

A method name has the form source_destination. Uppercase G, RGB, CMYK, and T denote grayscale, RGB, CMYK, and transparent forms. The destination determines the return shape:

DestinationReturn shapeUse in the surrounding code
G, RGB, CMYKAn array whose first item is the destination tagScripting color conversion
rgbA bare array of 0–255 components, or [null] for transparencyAnnotation-storage values
HTMLA CSS hexadecimal color stringAnnotation element styles

Callers such as Color.convert remove the source tag before invoking a converter, so methods receive only the component array. The following table preserves the current source's arithmetic and return values rather than inferring additional conversions from the naming pattern.

Grayscale and RGB methods

MethodExact behavior
G_CMYK([g])Returns ["CMYK", 0, 0, 0, 1 - g]
G_RGB([g])Returns ["RGB", g, g, g]
G_rgb([g])Scales g once and returns the same number three times
G_HTML([g])Converts g with makeColorComp and repeats the component in #rrggbb form
RGB_G([r, g, b])Returns ["G", 0.3 * r + 0.59 * g + 0.11 * b]
RGB_rgb(color)Maps every component through scaleAndClamp
RGB_HTML(color)Maps components through makeColorComp and joins them after #
RGB_CMYK([r, g, b])Computes c = 1-r, m = 1-g, y = 1-b, k = min(c,m,y) and returns ["CMYK", c, m, y, k]

RGB_CMYK computes a shared black component but does not subtract it from c, m, or y. It is exactly the simple conversion in the source, not a more elaborate undercolor-removal pipeline.

CMYK methods and their literal parameter order

MethodExact behavior
CMYK_G([c, y, m, k])Returns ["G", 1 - min(1, 0.3*c + 0.59*m + 0.11*y + k)]
CMYK_RGB([c, y, m, k])Returns red 1-min(1,c+k), green 1-min(1,m+k), blue 1-min(1,y+k), with an RGB tag
CMYK_rgb([c, y, m, k])Applies scaleAndClamp to those same three RGB component formulas and returns a bare array
CMYK_HTML(components)Calls CMYK_RGB, removes its tag, and passes the components to RGB_HTML

The signatures deserve literal reading: all three CMYK_* input methods destructure [c, y, m, k], whereas RGB_CMYK constructs ["CMYK", c, m, y, k]. That is the current implementation and is also reflected in unit-test expectations. Code reusing these methods should not silently rename positions or substitute another library's CMYK conventions.

CMYK_HTML also calls the other converters through this. Existing dynamic calls such as ColorConverters[method](components) preserve that receiver; detaching the method and invoking it without a receiver would break this internal delegation.

Transparent methods are deliberately limited

static T_HTML() {
  return "#00000000";
}
 
static T_rgb() {
  return [null];
}

Only these two converters have T as their source. In T_HTML and T_rgb, the destinations are HTML and rgb: the first returns eight-digit transparent black, while the second returns the storage sentinel [null], not a CSS string or [0, 0, 0]. There are no T_G, T_RGB, or G_T methods here. A requested transparent destination follows a different path: Color.convert returns ["T"] directly before dispatching to ColorConverters, so the absence of destination-side converters is part of the real API boundary.

DateFormats and TimeFormats: Numeric Preset Tables

const DateFormats = [
  'm/d',
  'm/d/yy',
  'mm/dd/yy',
  'mm/yy',
  'd-mmm',
  'd-mmm-yy',
  'dd-mmm-yy',
  'yy-mm-dd',
  'mmm-yy',
  'mmmm-yy',
  'mmm d, yyyy',
  'mmmm d, yyyy',
  'm/d/yy h:MM tt',
  'm/d/yy HH:MM',
];
const TimeFormats = ['HH:MM', 'h:MM tt', 'HH:MM:ss', 'h:MM:ss tt'];

Array position is part of the Acrobat scripting API. For example, date index 0 selects m/d, index 1 selects m/d/yy, and time index 3 selects h:MM:ss tt. Reordering or alphabetizing these arrays would change what existing PDF actions mean.

The entries are format tokens consumed by PDF.js's scripting date utilities, not locale-ready strings generated by Intl.DateTimeFormat. m and mm, month names, 12-hour tt, 24-hour HH, and optional seconds remain in the tables so the downstream parser and formatter can interpret them.

Real callers use the tables in two distinct ways:

  • AForm.AFDate_Format(pdf) and AFTime_Format(pdf) evaluate table[pdf] ?? pdf, allowing a known numeric preset to expand while passing another supplied format through to the extended formatter.
  • AFDate_Keystroke(pdf) and AFTime_Keystroke(pdf) proceed only when pdf is within the corresponding array bounds; they do not use the formatter's fallback expression.
  • Core annotation parsing also resolves numeric format actions through these arrays. Time tokens in a selected date format can make the resulting field datetime-local; a date-only format selects date, while a time format selects time with an appropriate step.

The shared arrays therefore connect two layers without implementing either one: the scripting sandbox receives the preset string, and annotation code receives enough format information to choose native input behavior.

Summary

scripting_utils.js has a deliberately narrow surface. Its color helpers distinguish fractional 0–255 values from floored hexadecimal components; ColorConverters exposes exactly fourteen conversions across tagged colors, CSS, storage RGB, and transparency; and the date/time arrays preserve Acrobat's numeric preset ordering.

The module's correctness depends on literal contracts rather than broad assumptions: CMYK parameter order, the [null] transparency sentinel, CMYK_HTML's this delegation, and the different fallback behavior of formatting versus keystroke methods all matter. Keeping those details in a dependency-light shared file lets scripting and annotation code agree without pulling either subsystem into the other.

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