diff --git a/README.md b/README.md index d058f4c..b118439 100644 --- a/README.md +++ b/README.md @@ -603,14 +603,18 @@ archival purposes. Those files can be added to the document using the representing a file with the following properties: - `content`: The binary content of the file as a `Uint8Array`. -- `fileName`: The name of the file as it will appear in the - list of attachments in the PDF viewer. +- `fileName`: The name of the file as it will appear in the list of + attachments in the PDF viewer. - `mimeType`: The MIME type of the file. - `description` (optional): A brief description of the file's content or purpose. This information can be displayed to the user in the PDF viewer. -- `creationDate` (optional): The date and time when the file was created. -- `modificationDate` (optional): The date and time when the file was last +- `creationDate` (optional): The date and time when the file was + created. +- `modificationDate` (optional): The date and time when the file was + last modified. +- `relationship` (optional): A name that specifies the relationship + between the file and the document. ```ts const document = { diff --git a/src/api/document.ts b/src/api/document.ts index 68c595f..969811d 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -101,6 +101,30 @@ export type DocumentDefinition = { }; }; +/** + * Describes the relationship between the embedded file and the PDF + * document. + * - `Source`: The embedded file is the source material for the + * document. + * - `Data`: The embedded file contains data related to the document. + * - `Alternative`: The embedded file is an alternative representation + * of the document. + * - `Supplement`: The embedded file supplements the document. + * - `EncryptedPayload`: The embedded file is an encrypted payload. + * - `FormData`: The embedded file contains form data. + * - `Schema`: The embedded file contains a schema. + * - `Unspecified`: No specific relationship. + */ +export type FileRelationShip = + | 'Source' + | 'Data' + | 'Alternative' + | 'Supplement' + | 'EncryptedPayload' + | 'FormData' + | 'Schema' + | 'Unspecified'; + export type EmbeddedFile = { /** * The binary content of the file. @@ -133,6 +157,11 @@ export type EmbeddedFile = { * The date and time when the file was last modified. */ modificationDate?: Date; + + /** + * The relationship between the file and the PDF document. + */ + relationship?: FileRelationShip; }; /** diff --git a/src/read-document.test.ts b/src/read-document.test.ts index fd4af07..757150b 100644 --- a/src/read-document.test.ts +++ b/src/read-document.test.ts @@ -113,6 +113,13 @@ describe('readDocumentDefinition', () => { ]); }); + it(`includes onRenderDocument hook`, () => { + const onRenderDocument = () => {}; + const def = readDocumentDefinition({ ...input, onRenderDocument }); + + expect(def.onRenderDocument).toBe(onRenderDocument); + }); + (['header', 'footer'] as const).forEach((name) => { it(`supports dynamic ${name}`, () => { const defaultStyle = { fontSize: 23 }; diff --git a/src/read-document.ts b/src/read-document.ts index 37495e5..29b7726 100644 --- a/src/read-document.ts +++ b/src/read-document.ts @@ -1,3 +1,6 @@ +import type { PDFDocument } from 'pdf-lib'; + +import type { FileRelationShip } from './api/document.ts'; import type { BoxEdges, Size } from './box.ts'; import { parseEdges } from './box.ts'; import type { FontDef } from './fonts.ts'; @@ -30,7 +33,9 @@ export type DocumentDefinition = { description?: string; creationDate?: Date; modificationDate?: Date; + relationship?: FileRelationShip; }[]; + onRenderDocument?: (pdfDoc: PDFDocument) => void; }; export type Metadata = { @@ -61,6 +66,7 @@ export function readDocumentDefinition(input: unknown): DocumentDefinition { dev: optional(types.object({ guides: optional(types.boolean()) })), customData: optional(readCustomData), embeddedFiles: optional(types.array(readEmbeddedFiles)), + onRenderDocument: optional(), }); const tBlock = (block: unknown) => readBlock(block, def1.defaultStyle); const def2 = readObject(input, { @@ -102,15 +108,33 @@ function readCustomData(input: unknown) { ); } +/** + * From the PDF-A3 specification, section **3.1. Requirements - + * General**. See: + * https://www.pdfa.org/wp-content/uploads/2018/10/PDF20_AN002-AF.pdf + */ +const readFileRelationship = types.string({ + enum: [ + 'Source', + 'Data', + 'Alternative', + 'Supplement', + 'EncryptedPayload', + 'FormData', + 'Schema', + 'Unspecified', + ], +}); + function readEmbeddedFiles(input: unknown) { return readObject(input, { - url: optional(types.string()), content: required(readData), fileName: required(types.string()), mimeType: required(types.string()), description: optional(types.string()), creationDate: optional(types.date()), modificationDate: optional(types.date()), + relationship: optional(readFileRelationship), }); } diff --git a/src/render/render-document.test.ts b/src/render/render-document.test.ts index f9bda8b..c392ec6 100644 --- a/src/render/render-document.test.ts +++ b/src/render/render-document.test.ts @@ -68,4 +68,20 @@ describe('renderDocument', () => { expect(lookup('XXFoo').getContentsString()).toBe('Foo'); expect(lookup('XXBar').getContents()).toEqual(Uint8Array.of(1, 2, 3)); }); + + it('calls custom render hook', async () => { + const def = { + content: [], + onRenderDocument: (pdfDoc: PDFDocument) => { + pdfDoc.setTitle('Test Title'); + }, + }; + + const pdfData = await renderDocument(def, []); + + const pdfDoc = await PDFDocument.load(pdfData, { updateMetadata: false }); + const infoDict = pdfDoc.context.lookup(pdfDoc.context.trailerInfo.Info) as PDFDict; + const getInfo = (name: string) => infoDict.get(PDFName.of(name)); + expect(getInfo('Title')).toEqual(PDFHexString.fromText('Test Title')); + }); }); diff --git a/src/render/render-document.ts b/src/render/render-document.ts index d02cd56..ce8f728 100644 --- a/src/render/render-document.ts +++ b/src/render/render-document.ts @@ -1,4 +1,4 @@ -import type { PDFDict } from 'pdf-lib'; +import type { AFRelationship, PDFDict } from 'pdf-lib'; import { PDFDocument, PDFHexString, PDFName } from 'pdf-lib'; import type { Page } from '../page.ts'; @@ -19,9 +19,11 @@ export async function renderDocument(def: DocumentDefinition, pages: Page[]): Pr description: file.description, creationDate: file.creationDate, modificationDate: file.modificationDate, + afRelationship: file.relationship as AFRelationship | undefined, }); } + def.onRenderDocument?.(pdfDoc); const idInfo = { creator: 'pdfmkr', time: new Date().toISOString(),