Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -1580,7 +1580,8 @@ class PDFDocument {
break;
default:
if (value instanceof Name) {
customValue = value;
// Convert Name objects to strings for serialization
customValue = value.name;
}
break;
}
Expand Down
54 changes: 50 additions & 4 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -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];
Expand All @@ -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];
}
Expand Down Expand Up @@ -1284,6 +1292,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;
}

Expand Down Expand Up @@ -1562,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) {
Expand Down
2 changes: 1 addition & 1 deletion src/scripting_api/app_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
147 changes: 123 additions & 24 deletions src/scripting_api/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,62 @@

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() {

Check failure on line 28 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Insert `·`

Check failure on line 28 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Insert `·`
// The Adlib PDF does eval(this.info.toSource()) to clone the object
// Instead of trying to serialize, just build a safe string representation

Check failure on line 30 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

This line has a comment length of 82. Maximum allowed is 80

Check failure on line 30 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

This line has a comment length of 82. Maximum allowed is 80
// 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");

Check failure on line 43 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Prefer `String#replaceAll()` over `String#replace()`

Check failure on line 43 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Prefer `String#replaceAll()` over `String#replace()`

Check failure on line 43 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Prefer `String#replaceAll()` over `String#replace()`

Check failure on line 43 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `.replace(/\\/g,·"\\\\").replace(/"/g,·'\\"')` with `⏎················.replace(/\\/g,·"\\\\")⏎················.replace(/"/g,·'\\"')⏎················`

Check failure on line 43 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Prefer `String#replaceAll()` over `String#replace()`

Check failure on line 43 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Prefer `String#replaceAll()` over `String#replace()`

Check failure on line 43 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Prefer `String#replaceAll()` over `String#replace()`

Check failure on line 43 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `.replace(/\\/g,·"\\\\").replace(/"/g,·'\\"')` with `⏎················.replace(/\\/g,·"\\\\")⏎················.replace(/"/g,·'\\"')⏎················`
pairs.push(`${key}: "${escaped}"`);
} else if (typeof value === "number" || typeof value === "boolean") {

Check failure on line 45 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `typeof·value·===·"number"·||·typeof·value·===·"boolean"` with `⏎··············typeof·value·===·"number"·||⏎··············typeof·value·===·"boolean"⏎············`

Check failure on line 45 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `typeof·value·===·"number"·||·typeof·value·===·"boolean"` with `⏎··············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) {
Expand Down Expand Up @@ -76,21 +123,55 @@

// 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',

Check failure on line 143 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `'send',·'globalEval',·'externalCall',·'proxyHandler',·'_document'` with `"send",⏎······"globalEval",⏎······"externalCall",⏎······"proxyHandler",⏎······"_document"`

Check failure on line 143 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `'send',·'globalEval',·'externalCall',·'proxyHandler',·'_document'` with `"send",⏎······"globalEval",⏎······"externalCall",⏎······"proxyHandler",⏎······"_document"`
'baseURL', 'filesize', 'filename', 'metadata', 'numPages', 'URL',

Check failure on line 144 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `'baseURL',·'filesize',·'filename',·'metadata',·'numPages',·'URL'` with `"baseURL",⏎······"filesize",⏎······"filename",⏎······"metadata",⏎······"numPages",⏎······"URL"`

Check failure on line 144 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `'baseURL',·'filesize',·'filename',·'metadata',·'numPages',·'URL'` with `"baseURL",⏎······"filesize",⏎······"filename",⏎······"metadata",⏎······"numPages",⏎······"URL"`
'calculationOrder', 'actions', 'pageNum', 'layout', 'zoom', 'docID',

Check failure on line 145 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `'calculationOrder',·'actions',·'pageNum',·'layout',·'zoom',·'docID'` with `"calculationOrder",⏎······"actions",⏎······"pageNum",⏎······"layout",⏎······"zoom",⏎······"docID"`

Check failure on line 145 in src/scripting_api/doc.js

View workflow job for this annotation

GitHub Actions / Lint (lts/*)

Replace `'calculationOrder',·'actions',·'pageNum',·'layout',·'zoom',·'docID'` with `"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 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;
}
}
}
}

this._info = new Proxy(infoObject, InfoProxyHandler);

this._zoomType = ZoomType.none;
this._zoom = data.zoom || 100;
Expand Down Expand Up @@ -944,7 +1025,8 @@
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]));
Expand Down Expand Up @@ -1051,8 +1133,25 @@
/* 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() {
Expand Down
Loading