From 8fa013cba57715dbb09901ef24b8a1fedbca416b Mon Sep 17 00:00:00 2001 From: Ralf Sternberg Date: Sat, 18 Jan 2025 11:23:10 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Support=20embedded=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Embedding files in a PDF document can be useful for archival purposes. This commit adds support for embedded files via the `embeddedFiles` property in the document definition. --- CHANGELOG.md | 5 +++++ README.md | 33 +++++++++++++++++++++++++++++ src/api/document.ts | 40 +++++++++++++++++++++++++++++++++++ src/read-document.ts | 26 +++++++++++++++++++++++ src/render/render-document.ts | 10 +++++++++ 5 files changed, 114 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4afba05..d46711f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [0.5.6] - Unreleased +### Added + +- Support for embedded files via the `embeddedFiles` property in the + document definition. + ## [0.5.5] - 2024-12-18 The minimum EcmaScript version has been raised to ES2022. diff --git a/README.md b/README.md index faee631..881f9c3 100644 --- a/README.md +++ b/README.md @@ -594,6 +594,39 @@ The following properties are supported: - `producer`: The name of the application that converted the original content into a PDF. +## Embedded files + +Supplementary files can be stored directly within a PDF document. This +can be useful for creating self-contained documents, such as for +archival purposes. Those files can be added to the document using the +`embeddedFiles` property, which accepts an array of objects, each +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. +- `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 + +```ts +const document = { + content: [text('Hello World!')], + embeddedFiles: [ + { + 'logo.png': { + content: await readFile('path/to/logo.png'), + description: 'The company logo', + mimeType: 'image/png', + }, + }, + ], +}; +``` + ## Dev tools ### Visual Debugging diff --git a/src/api/document.ts b/src/api/document.ts index 8c918c4..749cf5c 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -84,6 +84,12 @@ export type DocumentDefinition = { */ customData?: Record<`XX${string}`, string | Uint8Array>; + /** + * Files to be stored directly within a PDF document. These files can + * be displayed and extracted by PDF viewers and other tools. + */ + embeddedFiles?: EmbeddedFile[]; + dev?: { /** * When set to true, additional guides are drawn to help analyzing @@ -95,6 +101,40 @@ export type DocumentDefinition = { }; }; +export type EmbeddedFile = { + /** + * The binary content of the file. + */ + content: Uint8Array; + + /** + * The name of the file as it will appear in the list of attachments + * in the PDF viewer. + */ + fileName: string; + + /** + * The MIME type of the file. + */ + mimeType: string; + + /** + * A brief description of the file's content or purpose. This + * information an be displayed to the user in the PDF viewer. + */ + description?: string; + + /** + * The date and time when the file was created. + */ + creationDate?: Date; + + /** + * The date and time when the file was last modified. + */ + modificationDate?: Date; +}; + /** * @deprecated Use `InfoProps` instead. */ diff --git a/src/read-document.ts b/src/read-document.ts index c66f578..37495e5 100644 --- a/src/read-document.ts +++ b/src/read-document.ts @@ -23,6 +23,14 @@ export type DocumentDefinition = { footer?: (info: PageInfo) => Block; content: Block[]; customData?: Record; + embeddedFiles?: { + content: Uint8Array; + fileName: string; + mimeType: string; + description?: string; + creationDate?: Date; + modificationDate?: Date; + }[]; }; export type Metadata = { @@ -52,6 +60,7 @@ export function readDocumentDefinition(input: unknown): DocumentDefinition { defaultStyle: optional(readInheritableAttrs), dev: optional(types.object({ guides: optional(types.boolean()) })), customData: optional(readCustomData), + embeddedFiles: optional(types.array(readEmbeddedFiles)), }); const tBlock = (block: unknown) => readBlock(block, def1.defaultStyle); const def2 = readObject(input, { @@ -92,3 +101,20 @@ function readCustomData(input: unknown) { Object.entries(readObject(input)).map(([key, value]) => [key, readAs(value, key, readValue)]), ); } + +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()), + }); +} + +function readData(input: unknown): Uint8Array { + if (input instanceof Uint8Array) return input; + throw typeError('Uint8Array', input); +} diff --git a/src/render/render-document.ts b/src/render/render-document.ts index 76d6515..d02cd56 100644 --- a/src/render/render-document.ts +++ b/src/render/render-document.ts @@ -12,6 +12,16 @@ export async function renderDocument(def: DocumentDefinition, pages: Page[]): Pr setCustomData(def.customData, pdfDoc); } pages.forEach((page) => renderPage(page, pdfDoc)); + + for (const file of def.embeddedFiles ?? []) { + await pdfDoc.attach(file.content, file.fileName, { + mimeType: file.mimeType, + description: file.description, + creationDate: file.creationDate, + modificationDate: file.modificationDate, + }); + } + const idInfo = { creator: 'pdfmkr', time: new Date().toISOString(),