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

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 helper | Exported? | Responsibility |
|---|---|---|
makeColorComp(n) | No | Clamp a normalized component, floor its 0–255 value, and format two hexadecimal digits |
scaleAndClamp(x) | No | Clamp a normalized component and scale it to 0–255 without rounding |
ColorConverters | Yes | Convert among tagged PDF.js colors, CSS colors, and annotation-storage RGB values |
DateFormats | Yes | Fourteen index-based Acrobat date format presets |
TimeFormats | Yes | Four 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:
scaleAndClampreturns a number from 0 to 255 and preserves fractions. A midpoint becomes127.5.makeColorCompappliesMath.floor, then returns exactly two hexadecimal digits for the clamped byte. The same midpoint becomes7f, not80.
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:
| Destination | Return shape | Use in the surrounding code |
|---|---|---|
G, RGB, CMYK | An array whose first item is the destination tag | Scripting color conversion |
rgb | A bare array of 0–255 components, or [null] for transparency | Annotation-storage values |
HTML | A CSS hexadecimal color string | Annotation 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
| Method | Exact 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
| Method | Exact 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)andAFTime_Format(pdf)evaluatetable[pdf] ?? pdf, allowing a known numeric preset to expand while passing another supplied format through to the extended formatter.AFDate_Keystroke(pdf)andAFTime_Keystroke(pdf)proceed only whenpdfis 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 selectsdate, while a time format selectstimewith 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
- 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: 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