diff --git a/apps/deno/tests/test1.ts b/apps/deno/tests/test1.ts index 42e423b78..52957b5eb 100644 --- a/apps/deno/tests/test1.ts +++ b/apps/deno/tests/test1.ts @@ -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 diff --git a/apps/node/tests/test1.ts b/apps/node/tests/test1.ts index 01d60a1b8..113b70e25 100644 --- a/apps/node/tests/test1.ts +++ b/apps/node/tests/test1.ts @@ -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 diff --git a/apps/rn/src/tests/test1.js b/apps/rn/src/tests/test1.js index 3ded89aa6..afce41ec5 100644 --- a/apps/rn/src/tests/test1.js +++ b/apps/rn/src/tests/test1.js @@ -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 diff --git a/apps/web/test1.html b/apps/web/test1.html index f0ef47c43..5bf04a8b9 100644 --- a/apps/web/test1.html +++ b/apps/web/test1.html @@ -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 diff --git a/src/api/PDFDocument.ts b/src/api/PDFDocument.ts index 646addfc0..8894d7632 100644 --- a/src/api/PDFDocument.ts +++ b/src/api/PDFDocument.ts @@ -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. @@ -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, @@ -202,6 +205,7 @@ export default class PDFDocument { this.images = []; this.embeddedPages = []; this.embeddedFiles = []; + this.javaScripts = []; if (!ignoreEncryption && this.isEncrypted) throw new EncryptedPDFError(); @@ -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 @@ -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); } /** diff --git a/src/api/PDFJavaScript.ts b/src/api/PDFJavaScript.ts new file mode 100644 index 000000000..8727d6d7c --- /dev/null +++ b/src/api/PDFJavaScript.ts @@ -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 { + 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; + } + } +} diff --git a/src/api/index.ts b/src/api/index.ts index ed301b629..1c7e069c1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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'; diff --git a/src/core/embedders/JavaScriptEmbedder.ts b/src/core/embedders/JavaScriptEmbedder.ts new file mode 100644 index 000000000..b054601c5 --- /dev/null +++ b/src/core/embedders/JavaScriptEmbedder.ts @@ -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 { + 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; diff --git a/tests/api/PDFDocument.spec.ts b/tests/api/PDFDocument.spec.ts index 2d88d6515..601cc5223 100644 --- a/tests/api/PDFDocument.spec.ts +++ b/tests/api/PDFDocument.spec.ts @@ -3,7 +3,10 @@ import fs from 'fs'; import { EncryptedPDFError, ParseSpeeds, + PDFArray, + PDFDict, PDFDocument, + PDFHexString, PDFName, PDFPage, } from 'src/index'; @@ -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'); + }); + }); });