Skip to content

Commit

Permalink
Add PDFDocument.addJavaScript method (#654)
Browse files Browse the repository at this point in the history
* Add high-level API for adding document JavaScripts (#643)

* Add high-level API for adding document JavaScripts

* Add example to `addJavascript` documentation and switch parameter order.

* Add check for existing catalog entries in `addJavascript`

* Add unit tests for `addJavascript`

* Add Node.js integration test for `addJavascript`

* Add integration test for `addJavascript` in web

* Add integration test for `addJavascript` in react native

* Add integration test for `addJavascript` in deno

* Cleanup PDFDocument.addJavaScript

* Revert scratchpad

Co-authored-by: Julian Dax <[email protected]>
  • Loading branch information
Hopding and brodo authored Oct 31, 2020
1 parent 18ff428 commit aaf7590
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 0 deletions.
5 changes: 5 additions & 0 deletions apps/deno/tests/test1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export default async (assets: Assets) => {

const size = 750;

pdfDoc.addJavaScript(
'main',
'console.show(); console.println("Hello World!")',
);

/********************** Page 1 **********************/

// This page tests different drawing operations as well as adding custom
Expand Down
5 changes: 5 additions & 0 deletions apps/node/tests/test1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export default async (assets: Assets) => {

const size = 750;

pdfDoc.addJavaScript(
'main',
'console.show(); console.println("Hello World!")',
);

/********************** Page 1 **********************/

// This page tests different drawing operations as well as adding custom
Expand Down
5 changes: 5 additions & 0 deletions apps/rn/src/tests/test1.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ export default async () => {

const size = 750;

pdfDoc.addJavaScript(
'main',
'console.show(); console.println("Hello World!")',
);

/********************** Page 1 **********************/

// This page tests different drawing operations as well as adding custom
Expand Down
5 changes: 5 additions & 0 deletions apps/web/test1.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@

const size = 750;

pdfDoc.addJavaScript(
'main',
'console.show(); console.println("Hello World!")',
);

/********************** Page 1 **********************/

// This page tests different drawing operations as well as adding custom
Expand Down
40 changes: 40 additions & 0 deletions src/api/PDFDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import {
} from 'src/utils';
import FileEmbedder from 'src/core/embedders/FileEmbedder';
import PDFEmbeddedFile from 'src/api/PDFEmbeddedFile';
import PDFJavaScript from 'src/api/PDFJavaScript';
import JavaScriptEmbedder from 'src/core/embedders/JavaScriptEmbedder';

/**
* Represents a PDF document.
Expand Down Expand Up @@ -182,6 +184,7 @@ export default class PDFDocument {
private readonly images: PDFImage[];
private readonly embeddedPages: PDFEmbeddedPage[];
private readonly embeddedFiles: PDFEmbeddedFile[];
private readonly javaScripts: PDFJavaScript[];

private constructor(
context: PDFContext,
Expand All @@ -202,6 +205,7 @@ export default class PDFDocument {
this.images = [];
this.embeddedPages = [];
this.embeddedFiles = [];
this.javaScripts = [];

if (!ignoreEncryption && this.isEncrypted) throw new EncryptedPDFError();

Expand Down Expand Up @@ -713,6 +717,41 @@ export default class PDFDocument {
return copiedPages;
}

/**
* Add JavaScript to this document. The supplied `script` is executed when the
* document is opened. The `script` can be used to perform some operation
* when the document is opened (e.g. logging to the console), or it can be
* used to define a function that can be referenced later in a JavaScript
* action. For example:
* ```js
* // Show "Hello World!" in the console when the PDF is opened
* pdfDoc.addJavaScript(
* 'main',
* 'console.show(); console.println("Hello World!");'
* );
*
* // Define a function named "foo" that can be called in JavaScript Actions
* pdfDoc.addJavaScript(
* 'foo',
* 'function foo() { return "foo"; }'
* );
* ```
* See the [JavaScript for Acrobat API Reference](https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/js_api_reference.pdf)
* for details.
* @param name The name of the script. Must be unique per document.
* @param script The JavaScript to execute.
*/
addJavaScript(name: string, script: string) {
assertIs(name, 'name', ['string']);
assertIs(script, 'script', ['string']);

const embedder = JavaScriptEmbedder.for(script, name);

const ref = this.context.nextRef();
const javaScript = PDFJavaScript.of(ref, this, embedder);
this.javaScripts.push(javaScript);
}

/**
* Add an attachment to this document. Attachments are visible in the
* "Attachments" panel of Adobe Acrobat and some other PDF readers. Any
Expand Down Expand Up @@ -1130,6 +1169,7 @@ export default class PDFDocument {
await this.embedAll(this.images);
await this.embedAll(this.embeddedPages);
await this.embedAll(this.embeddedFiles);
await this.embedAll(this.javaScripts);
}

/**
Expand Down
82 changes: 82 additions & 0 deletions src/api/PDFJavaScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Embeddable from 'src/api/Embeddable';
import PDFDocument from 'src/api/PDFDocument';
import JavaScriptEmbedder from 'src/core/embedders/JavaScriptEmbedder';
import { PDFName, PDFArray, PDFDict, PDFHexString, PDFRef } from 'src/core';

/**
* Represents JavaScript that has been embedded in a [[PDFDocument]].
*/
export default class PDFJavaScript implements Embeddable {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFDocument.addJavaScript]] method, which will
* create instances of [[PDFJavaScript]] for you.
*
* Create an instance of [[PDFJavaScript]] from an existing ref and script
*
* @param ref The unique reference for this script.
* @param doc The document to which the script will belong.
* @param embedder The embedder that will be used to embed the script.
*/
static of = (ref: PDFRef, doc: PDFDocument, embedder: JavaScriptEmbedder) =>
new PDFJavaScript(ref, doc, embedder);

/** The unique reference assigned to this embedded script within the document. */
readonly ref: PDFRef;

/** The document to which this embedded script belongs. */
readonly doc: PDFDocument;

private alreadyEmbedded = false;
private readonly embedder: JavaScriptEmbedder;

private constructor(
ref: PDFRef,
doc: PDFDocument,
embedder: JavaScriptEmbedder,
) {
this.ref = ref;
this.doc = doc;
this.embedder = embedder;
}

/**
* > **NOTE:** You probably don't need to call this method directly. The
* > [[PDFDocument.save]] and [[PDFDocument.saveAsBase64]] methods will
* > automatically ensure all JavaScripts get embedded.
*
* Embed this JavaScript in its document.
*
* @returns Resolves when the embedding is complete.
*/
async embed(): Promise<void> {
if (!this.alreadyEmbedded) {
const { catalog, context } = this.doc;

const ref = await this.embedder.embedIntoContext(
this.doc.context,
this.ref,
);

if (!catalog.has(PDFName.of('Names'))) {
catalog.set(PDFName.of('Names'), context.obj({}));
}
const Names = catalog.lookup(PDFName.of('Names'), PDFDict);

if (!Names.has(PDFName.of('JavaScript'))) {
Names.set(PDFName.of('JavaScript'), context.obj({}));
}
const Javascript = Names.lookup(PDFName.of('JavaScript'), PDFDict);

if (!Javascript.has(PDFName.of('Names'))) {
Javascript.set(PDFName.of('Names'), context.obj([]));
}
const JSNames = Javascript.lookup(PDFName.of('Names'), PDFArray);

JSNames.push(PDFHexString.fromText(this.embedder.scriptName));
JSNames.push(ref);

this.alreadyEmbedded = true;
}
}
}
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export { default as PDFFont } from 'src/api/PDFFont';
export { default as PDFImage } from 'src/api/PDFImage';
export { default as PDFPage } from 'src/api/PDFPage';
export { default as PDFEmbeddedPage } from 'src/api/PDFEmbeddedPage';
export { default as PDFJavaScript } from 'src/api/PDFJavaScript';
export { default as Embeddable } from 'src/api/Embeddable';
34 changes: 34 additions & 0 deletions src/core/embedders/JavaScriptEmbedder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';

class JavaScriptEmbedder {
static for(script: string, scriptName: string) {
return new JavaScriptEmbedder(script, scriptName);
}

private readonly script: string;
readonly scriptName: string;

private constructor(script: string, scriptName: string) {
this.script = script;
this.scriptName = scriptName;
}

async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const jsActionDict = context.obj({
Type: 'Action',
S: 'JavaScript',
JS: PDFHexString.fromText(this.script),
});

if (ref) {
context.assign(ref, jsActionDict);
return ref;
} else {
return context.register(jsActionDict);
}
}
}

export default JavaScriptEmbedder;
41 changes: 41 additions & 0 deletions tests/api/PDFDocument.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import fs from 'fs';
import {
EncryptedPDFError,
ParseSpeeds,
PDFArray,
PDFDict,
PDFDocument,
PDFHexString,
PDFName,
PDFPage,
} from 'src/index';
Expand Down Expand Up @@ -299,4 +302,42 @@ describe(`PDFDocument`, () => {
);
});
});

describe(`addJavaScript method`, () => {
it(`adds the script to the catalog`, async () => {
const pdfDoc = await PDFDocument.create();
pdfDoc.addJavaScript(
'main',
'console.show(); console.println("Hello World");',
);
await pdfDoc.flush();

expect(pdfDoc.catalog.has(PDFName.of('Names')));
const Names = pdfDoc.catalog.lookup(PDFName.of('Names'), PDFDict);
expect(Names.has(PDFName.of('JavaScript')));
const Javascript = Names.lookup(PDFName.of('JavaScript'), PDFDict);
expect(Javascript.has(PDFName.of('Names')));
const JSNames = Javascript.lookup(PDFName.of('Names'), PDFArray);
expect(JSNames.lookup(0, PDFHexString).decodeText()).toEqual('main');
});

it(`does not overwrite scripts`, async () => {
const pdfDoc = await PDFDocument.create();
pdfDoc.addJavaScript(
'first',
'console.show(); console.println("First");',
);
pdfDoc.addJavaScript(
'second',
'console.show(); console.println("Second");',
);
await pdfDoc.flush();

const Names = pdfDoc.catalog.lookup(PDFName.of('Names'), PDFDict);
const Javascript = Names.lookup(PDFName.of('JavaScript'), PDFDict);
const JSNames = Javascript.lookup(PDFName.of('Names'), PDFArray);
expect(JSNames.lookup(0, PDFHexString).decodeText()).toEqual('first');
expect(JSNames.lookup(2, PDFHexString).decodeText()).toEqual('second');
});
});
});

0 comments on commit aaf7590

Please sign in to comment.