From 71d761cdf138dd2f70c526bddd3c513cb7ea6740 Mon Sep 17 00:00:00 2001 From: Shay Merrill Date: Fri, 2 Jan 2026 16:56:18 -0700 Subject: [PATCH 1/2] working withh some extra changes in the pdf's js --- src/core/annotation.js | 4 +-- src/core/document.js | 3 +- src/display/annotation_layer.js | 2 ++ src/scripting_api/app_utils.js | 2 +- src/scripting_api/doc.js | 59 ++++++++++++++++++++++++--------- src/scripting_api/field.js | 25 ++++++++++++-- web/app.js | 2 ++ web/app_options.js | 2 +- 8 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index da706ffce0058..fffc52640b6c3 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -2779,10 +2779,10 @@ class TextWidgetAnnotation extends WidgetAnnotation { warn("Barcodes are not supported"); } - // this.data.hasOwnCanvas = this.data.readOnly && !this.data.noHTML; + this.data.hasOwnCanvas = this.data.readOnly && !this.data.noHTML; // MasterControl turned this off to avoid issues with chromium // browsers with hardware accelleration on. - this.data.hasOwnCanvas = false; + // this.data.hasOwnCanvas = false; this._hasText = true; // The field value is always a string. diff --git a/src/core/document.js b/src/core/document.js index a3892751f03b2..afb59cf87adf2 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -1620,7 +1620,8 @@ class PDFDocument { break; default: if (value instanceof Name) { - customValue = value; + // Convert Name objects to strings for serialization + customValue = value.name; } break; } diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 27e54495767cb..ec814772ddf51 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -1205,6 +1205,8 @@ class TextAnnotationElement extends AnnotationElement { class WidgetAnnotationElement extends AnnotationElement { render() { // Show only the container for unsupported field types. + // Apply any properties that were set via JavaScript before rendering + this._setDefaultPropertiesFromJS(this.container); return this.container; } diff --git a/src/scripting_api/app_utils.js b/src/scripting_api/app_utils.js index b46f8ea2640dd..3764a798168b3 100644 --- a/src/scripting_api/app_utils.js +++ b/src/scripting_api/app_utils.js @@ -13,7 +13,7 @@ * limitations under the License. */ -const VIEWER_TYPE = "PDF.js"; +const VIEWER_TYPE = "mcPDF.js"; const VIEWER_VARIATION = "Full"; const VIEWER_VERSION = 21.00720099; const FORMS_VERSION = 21.00720099; diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index c76ccc047483f..f880b5cb38e49 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -76,21 +76,50 @@ class Doc extends PDFObject { // info has case insensitive properties // and they're are read-only. - this._info = new Proxy( - { - title: this._title, - author: this._author, - authors: data.authors || [this._author], - subject: this._subject, - keywords: this._keywords, - creator: this._creator, - producer: this._producer, - creationdate: this._creationDate, - moddate: this._modDate, - trapped: data.Trapped || "Unknown", - }, - InfoProxyHandler - ); + const infoObject = { + title: this._title, + author: this._author, + authors: data.authors || [this._author], + subject: this._subject, + keywords: this._keywords, + creator: this._creator, + producer: this._producer, + creationdate: this._creationDate, + moddate: this._modDate, + trapped: data.Trapped || "Unknown", + }; + + // Include custom metadata fields from the PDF's Info dictionary + // Exclude only fields that pdf.js adds (not from the PDF itself) + const pdfJsAddedFields = new Set([ + // Fields added by pdf.js internals (from initialization.js and app.js) + 'send', 'globalEval', 'externalCall', 'proxyHandler', '_document', + 'baseURL', 'filesize', 'filename', 'metadata', 'numPages', 'URL', + 'calculationOrder', 'actions', 'pageNum', 'layout', 'zoom', 'docID', + // Standard fields already processed above + 'Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', + 'CreationDate', 'ModDate', 'Trapped', 'authors', 'EncryptFilterName', + // PDF structure fields (not metadata) + 'PDFFormatVersion', 'Language', 'IsLinearized', 'IsAcroFormPresent', + 'IsXFAPresent', 'IsCollectionPresent', 'IsSignaturesPresent', 'Custom' + ]); + + for (const key in data) { + if (data.hasOwnProperty(key)) { + const value = data[key]; + // Include custom metadata if: not a pdf.js field, not already in info, and is a primitive value + if (!pdfJsAddedFields.has(key) && + value !== null && + value !== undefined && + typeof value !== 'function' && + typeof value !== 'object') { + // Add with lowercase key since InfoProxyHandler converts to lowercase + infoObject[key.toLowerCase()] = value; + } + } + } + + this._info = new Proxy(infoObject, InfoProxyHandler); this._zoomType = ZoomType.none; this._zoom = data.zoom || 100; diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index b5de544e783e4..a64263ad69756 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -38,12 +38,12 @@ class Field extends PDFObject { this.doNotScroll = data.doNotScroll; this.doNotSpellCheck = data.doNotSpellCheck; this.delay = data.delay; - this.display = data.display; + this._display = data.display; this.doc = data.doc.wrapped; this.editable = data.editable; this.exportValues = data.exportValues; this.fileSelect = data.fileSelect; - this.hidden = data.hidden; + this._hidden = data.hidden; this.highlight = data.highlight; this.lineWidth = data.lineWidth; this.multiline = data.multiline; @@ -197,6 +197,27 @@ class Field extends PDFObject { this.strokeColor = color; } + get hidden() { + return this._hidden; + } + + set hidden(hidden) { + this._hidden = !!hidden; + this._send({ id: this._id, hidden: this._hidden }); + } + + get display() { + return this._display; + } + + set display(display) { + // display values: 0=visible, 1=hidden, 2=noPrint, 3=noView + if (typeof display === "number" && display >= 0 && display <= 3) { + this._display = display; + this._send({ id: this._id, display: this._display }); + } + } + get page() { return this._page; } diff --git a/web/app.js b/web/app.js index 32f9ca81c83bf..6168e317c7448 100644 --- a/web/app.js +++ b/web/app.js @@ -1649,6 +1649,8 @@ const PDFViewerApplication = { return { ...this.documentInfo, + // Flatten custom metadata from documentInfo.Custom to top level + ...(this.documentInfo?.Custom || {}), baseURL: this.baseUrl, filesize: this._contentLength, filename: this._docFilename, diff --git a/web/app_options.js b/web/app_options.js index 6456c2062d152..c320fa8ae2aa1 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -165,7 +165,7 @@ const defaultOptions = { }, annotationMode: { /** @type {number} */ - value: 1, + value: 2, kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, capCanvasAreaFactor: { From f195501979446f3ec7ee154e941ef69660996b72 Mon Sep 17 00:00:00 2001 From: Shay Merrill Date: Thu, 12 Mar 2026 16:55:32 -0600 Subject: [PATCH 2/2] Add JavaScript execution support for MasterControl published PDFs Enable execution of embedded JavaScript in ADAPT and Adlib published PDFs to manage overlays, headers, footers, and expiration logic. Key Changes: - Custom metadata extraction with case-insensitive access (doc.js) - Multi-page field updates via siblings mechanism (field.js, pdf_scripting_manager.js) - Field property updates (hidden, value, readonly) propagate to all sibling widgets - Date formatting with defensive null checks (util.js) - Fixed annotation storage cross-contamination by using shallow copy (annotation_layer.js) - Race condition handling for fast scrolling/page jumps Technical Details: - ADAPT PDFs use MC_Overlay, MC_Header, MC_Footer fields - Adlib PDFs use ___MASTERContolObscure___, ___1___, ___2___, ___3___ fields - One Field object represents all instances across pages with siblings array - Hidden action stores {hidden, noPrint, noView} regardless of container state - _setDefaultPropertiesFromJS creates shallow copy to prevent shared storage modification Co-Authored-By: Claude Sonnet 4.6 --- README.md | 21 ++++++++ src/display/annotation_layer.js | 52 +++++++++++++++++-- src/scripting_api/doc.js | 92 +++++++++++++++++++++++++++++---- src/scripting_api/field.js | 18 ++++++- src/scripting_api/util.js | 11 +++- web/pdf_scripting_manager.js | 1 + 6 files changed, 177 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c502529a3cf45..d9dea015bce9f 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,27 @@ Use a **squash merge PR for new feature work** so that all your development comm * We don't allow annotating/inking the pdf (web/viewer.html) (currently just hiding the button, we may want to find a more pdf.js way to disable editing) * We don't allow page management actions (copy/cut/delete/save pages) - hidden via viewsManagerStatus element (web/viewer.html) +#### JavaScript Execution for MasterControl Published PDFs +* **Purpose**: Execute embedded JavaScript in MasterControl published PDFs to manage overlays, headers, footers, and expiration logic +* **Supported PDF types**: + - ADAPT PDFs: Use fields named `MC_Overlay`, `MC_Header`, `MC_Footer` + - Adlib PDFs: Use fields named `___MASTERContolObscure___`, `___1___`, `___2___`, `___3___` +* **Implementation**: + - Custom metadata extraction from PDF Info dictionary with case-insensitive access (src/scripting_api/doc.js) + - Multi-page field updates via siblings mechanism - one Field object represents all instances across pages (src/scripting_api/field.js) + - Field property updates (hidden, value, readonly) propagate to all sibling widgets (src/scripting_api/field.js, web/pdf_scripting_manager.js) + - Date formatting with defensive null checks for invalid dates (src/scripting_api/util.js) + - Annotation storage uses shallow copy to prevent cross-field contamination when applying stored properties (src/display/annotation_layer.js) + - Race condition handling: hidden action always stores to annotation storage even if container not ready, ensuring properties apply on delayed render (src/display/annotation_layer.js) +* **Key files modified**: + - `src/scripting_api/doc.js`: Metadata access, field retrieval, page box dimensions + - `src/scripting_api/field.js`: Field property setters with siblings support + - `src/scripting_api/util.js`: Date formatting fixes + - `src/display/annotation_layer.js`: Property application from storage with shallow copy fix + - `web/pdf_scripting_manager.js`: Dispatch events or store for unrendered fields + - `src/core/document.js`: Name object to string conversion for metadata + - `web/app.js`: Flatten custom metadata to top level + #### Styling & UI Customizations * **Theme & Colors**: - Force light mode globally (`color-scheme: only light`) to avoid dark mode conflicts diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index d1ae8e893b816..5c2db63435996 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -545,8 +545,12 @@ class AnnotationElement { }, hidden: event => { const { hidden } = event.detail; - this.container.style.visibility = hidden ? "hidden" : "visible"; + if (this.container) { + this.container.style.visibility = hidden ? "hidden" : "visible"; + } + // Always store for unrendered elements or for print behavior this.annotationStorage.setValue(this.data.id, { + hidden, noPrint: hidden, noView: hidden, }); @@ -606,11 +610,14 @@ class AnnotationElement { } // Some properties may have been updated thanks to JS. - const storedData = this.annotationStorage.getRawValue(this.data.id); - if (!storedData) { + const rawStoredData = this.annotationStorage.getRawValue(this.data.id); + if (!rawStoredData) { return; } + // IMPORTANT: Make a shallow copy to avoid modifying shared storage object + const storedData = Object.assign({}, rawStoredData); + const commonActions = this._commonActions; for (const [actionName, detail] of Object.entries(storedData)) { const action = commonActions[actionName]; @@ -621,7 +628,8 @@ class AnnotationElement { }, target: element, }; - action(eventProxy); + // Call action with correct 'this' context + action.call(this, eventProxy); // The action has been consumed: no need to keep it. delete storedData[actionName]; } @@ -1564,6 +1572,42 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { event.target.scrollLeft = 0; }; + // Add updatefromsandbox listener for all fields when scripting is enabled + // (not just fields with actions), so PDF JavaScript can update any field + if (this.enableScripting) { + element.addEventListener("updatefromsandbox", jsEvent => { + this.showElementAndHideCanvas(jsEvent.target); + const actions = { + value(event) { + elementData.userValue = event.detail.value ?? ""; + if (!hasDateOrTime) { + storage.setValue(id, { + value: elementData.userValue.toString(), + }); + } + event.target.value = elementData.userValue; + }, + formattedValue(event) { + const { formattedValue } = event.detail; + elementData.formattedValue = formattedValue; + if ( + formattedValue !== null && + formattedValue !== undefined && + event.target !== document.activeElement + ) { + event.target.value = formattedValue; + } + const data = { formattedValue }; + if (hasDateOrTime) { + data.value = formattedValue; + } + storage.setValue(id, data); + }, + }; + this._dispatchEventFromSandbox(actions, jsEvent); + }); + } + if (this.enableScripting && this.hasJSActions) { element.addEventListener("focus", event => { if (elementData.focused) { diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index f880b5cb38e49..c2ed02be6190a 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -21,15 +21,62 @@ import { ZoomType } from "./constants.js"; const DOC_EXTERNAL = false; -class InfoProxyHandler { - static get(obj, prop) { +const InfoProxyHandler = { + get(obj, prop) { + // Handle special method toSource() for Adlib PDFs + if (prop === "toSource") { + return function() { + // The Adlib PDF does eval(this.info.toSource()) to clone the object + // Instead of trying to serialize, just build a safe string representation + // that creates a new object with the same properties + const pairs = []; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + // Handle different types + if (value === null) { + pairs.push(`${key}: null`); + } else if (value === undefined) { + pairs.push(`${key}: undefined`); + } else if (typeof value === "string") { + // Escape quotes in strings + const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); + pairs.push(`${key}: "${escaped}"`); + } else if (typeof value === "number" || typeof value === "boolean") { + pairs.push(`${key}: ${value}`); + } else if (value instanceof Date) { + pairs.push(`${key}: null`); // Date objects become null for safety + } else if (Array.isArray(value)) { + pairs.push(`${key}: []`); // Arrays become empty for safety + } + // Skip functions and other complex objects + } + } + return `({${pairs.join(", ")}})`; + }; + } return obj[prop.toLowerCase()]; - } + }, - static set(obj, prop, value) { + set(obj, prop, value) { throw new Error(`doc.info.${prop} is read-only`); - } -} + }, + + ownKeys(obj) { + return Object.keys(obj); + }, + + getOwnPropertyDescriptor(obj, prop) { + // Required for ownKeys trap to work properly + if (prop in obj) { + return { + enumerable: true, + configurable: true, + }; + } + return undefined; + }, +}; class Doc extends PDFObject { constructor(data) { @@ -113,8 +160,13 @@ class Doc extends PDFObject { value !== undefined && typeof value !== 'function' && typeof value !== 'object') { - // Add with lowercase key since InfoProxyHandler converts to lowercase - infoObject[key.toLowerCase()] = value; + // Add the key with its original casing for PDF JavaScript enumeration + infoObject[key] = value; + // Also add lowercase version for case-insensitive access + const lowerKey = key.toLowerCase(); + if (lowerKey !== key) { + infoObject[lowerKey] = value; + } } } } @@ -973,7 +1025,8 @@ class Doc extends PDFObject { return searchedField; } - const parts = cName.split("#"); + // Handle both '#' and '.' as page separators (Adlib uses '.') + const parts = cName.split(/[#.]/); let childIndex = NaN; if (parts.length === 2) { childIndex = Math.floor(parseFloat(parts[1])); @@ -1080,8 +1133,25 @@ class Doc extends PDFObject { /* Not implemented */ } - getPageBox() { - /* TODO */ + getPageBox(cBox, nPage) { + // cBox can be: "ArtBox", "BleedBox", "BBox", "CropBox", "MediaBox", "TrimBox" + // For most PDFs, CropBox and MediaBox are the same + // Returns [x1, y1, x2, y2] where (x1,y1) is lower-left, (x2,y2) is upper-right + + if (typeof nPage !== "number" || nPage < 0 || nPage >= this._numPages) { + return undefined; + } + + // TODO: HARDCODED PAGE SIZE - Need to fetch actual page dimensions from PDF + // Current implementation returns a static letter-size page box (8.5" x 11" = 612 x 792 points) + // This works for most standard PDFs but will cause field positioning issues for: + // - Non-standard page sizes (A4, Legal, Tabloid, custom sizes) + // - PDFs with mixed page sizes + // - Landscape orientation pages + // To fix: Pass page dimensions during Doc initialization or implement externalCall + // to fetch page boxes from the main thread synchronously + // Used by: Adlib PDF's Reposition() function for header/footer field positioning + return [0, 0, 612, 792]; } getPageLabel() { diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index a64263ad69756..944c5eaa6e555 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -203,7 +203,7 @@ class Field extends PDFObject { set hidden(hidden) { this._hidden = !!hidden; - this._send({ id: this._id, hidden: this._hidden }); + this._send({ id: this._id, siblings: this._siblings, hidden: this._hidden }); } get display() { @@ -214,7 +214,7 @@ class Field extends PDFObject { // display values: 0=visible, 1=hidden, 2=noPrint, 3=noView if (typeof display === "number" && display >= 0 && display <= 3) { this._display = display; - this._send({ id: this._id, display: this._display }); + this._send({ id: this._id, siblings: this._siblings, display: this._display }); } } @@ -288,12 +288,26 @@ class Field extends PDFObject { ) { this._originalValue = undefined; this._value = value; + // Send value update to display layer + this._send({ + id: this._id, + siblings: this._siblings, + value: this._value, + formattedValue: this._value != null ? this._value.toString() : "", + }); return; } this._originalValue = value; const _value = value.trim().replace(",", "."); this._value = !isNaN(_value) ? parseFloat(_value) : value; + // Send value update to display layer + this._send({ + id: this._id, + siblings: this._siblings, + value: this._value, + formattedValue: this._value != null ? this._value.toString() : "", + }); } get _initialValue() { diff --git a/src/scripting_api/util.js b/src/scripting_api/util.js index 91229b0d73d19..333c5cfa7a0e4 100644 --- a/src/scripting_api/util.js +++ b/src/scripting_api/util.js @@ -209,6 +209,12 @@ class Util extends PDFObject { } printd(cFormat, oDate) { + // Handle undefined or invalid dates + if (!oDate || !(oDate instanceof Date) || isNaN(oDate.getTime())) { + // Return empty string for invalid dates (console may not be available yet) + return ''; + } + switch (cFormat) { case 0: return this.printd("D:yyyymmddHHMMss", oDate); @@ -220,7 +226,10 @@ class Util extends PDFObject { const handlers = { mmmm: data => this._months[data.month], - mmm: data => this._months[data.month].substring(0, 3), + mmm: data => { + const month = this._months[data.month]; + return month ? month.substring(0, 3) : ''; + }, mm: data => (data.month + 1).toString().padStart(2, "0"), m: data => (data.month + 1).toString(), dddd: data => this._days[data.dayOfWeek], diff --git a/web/pdf_scripting_manager.js b/web/pdf_scripting_manager.js index 402613f87f507..4ecedd03ae32e 100644 --- a/web/pdf_scripting_manager.js +++ b/web/pdf_scripting_manager.js @@ -376,6 +376,7 @@ class PDFScriptingManager { this.#pdfDocument?.annotationStorage.setValue(elementId, detail); } } + } async #dispatchPageOpen(pageNumber, initialize = false) {