diff --git a/CHANGELOG.md b/CHANGELOG.md index 93bfeeb..aa57868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to `@vitaeflow/pdf-embed` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.3] - 2026-07-20 + +### Added + +- Support embedded-file creation and modification dates in `embed()` and `list()` + ## [0.1.2] - 2026-07-20 ### Documentation diff --git a/README.md b/README.md index ed4527e..3be8b02 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ const result = await embed(new Uint8Array(pdf), data, { mimeType: 'application/json', relationship: 'Alternative', description: 'Structured data', + creationDate: new Date('2025-02-03T10:11:12Z'), + modificationDate: new Date('2026-07-20T14:15:16Z'), }); writeFileSync('output.pdf', result); diff --git a/package-lock.json b/package-lock.json index 908f24d..77054c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vitaeflow/pdf-embed", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vitaeflow/pdf-embed", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "dependencies": { "pdf-lib": "^1.17.1" diff --git a/package.json b/package.json index 8263b31..d2db968 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vitaeflow/pdf-embed", - "version": "0.1.2", + "version": "0.1.3", "description": "Embed and extract associated files in PDF documents", "type": "module", "main": "./dist/index.js", diff --git a/src/embed.ts b/src/embed.ts index f6f1039..73b05d7 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -52,6 +52,8 @@ export async function embed( await pdfDoc.attach(fileData, options.filename, { mimeType: options.mimeType ?? 'application/octet-stream', description: options.description, + creationDate: options.creationDate, + modificationDate: options.modificationDate, afRelationship: AF_RELATIONSHIP_MAP[relationship], }); diff --git a/src/list.ts b/src/list.ts index f221db0..470cad9 100644 --- a/src/list.ts +++ b/src/list.ts @@ -3,6 +3,7 @@ import type { AFRelationship, EmbeddedFileInfo } from './types.js'; import { getEmbeddedFilesDict, collectNameTreeEntries, + pdfDateValue, pdfStringValue, readPdfNumber, resolve, @@ -72,6 +73,16 @@ export async function list(pdf: Uint8Array): Promise { if (params instanceof PDFDict) { const size = readPdfNumber(params, 'Size', pdfDoc.context); if (size !== undefined) info.size = size; + + const creationDate = pdfDateValue( + resolve(params.get(PDFName.of('CreationDate')), pdfDoc.context), + ); + if (creationDate) info.creationDate = creationDate; + + const modificationDate = pdfDateValue( + resolve(params.get(PDFName.of('ModDate')), pdfDoc.context), + ); + if (modificationDate) info.modificationDate = modificationDate; } if (info.size === 0) { info.size = stream.contents.length; diff --git a/src/pdf-utils.ts b/src/pdf-utils.ts index a7ccb69..5e3a922 100644 --- a/src/pdf-utils.ts +++ b/src/pdf-utils.ts @@ -39,6 +39,22 @@ export function pdfStringValue(obj: unknown): string | undefined { return undefined; } +/** + * Extract a date from a PDF date string. Malformed dates are ignored so that + * listing attachments remains resilient when reading third-party PDFs. + */ +export function pdfDateValue(obj: unknown): Date | undefined { + if (!(obj instanceof PDFString) && !(obj instanceof PDFHexString)) { + return undefined; + } + + try { + return obj.decodeDate(); + } catch { + return undefined; + } +} + /** * Get the EmbeddedFiles name tree dict from a PDF document catalog. */ diff --git a/src/types.ts b/src/types.ts index 8e6800c..b6b9aae 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,10 @@ export interface EmbedOptions { filename: string; /** MIME type of the embedded file. Defaults to 'application/octet-stream'. */ mimeType?: string; + /** Creation date stored in the embedded file's PDF metadata. */ + creationDate?: Date; + /** Last modification date stored in the embedded file's PDF metadata. */ + modificationDate?: Date; /** Relationship between the file and the PDF. Defaults to 'Unspecified'. */ relationship?: AFRelationship; /** Human-readable description of the file. */ @@ -47,6 +51,10 @@ export interface EmbeddedFileInfo { mimeType?: string; /** Size in bytes of the embedded file. */ size: number; + /** Creation date from the embedded file's PDF metadata, if specified. */ + creationDate?: Date; + /** Last modification date from the embedded file's PDF metadata, if specified. */ + modificationDate?: Date; /** AFRelationship, if specified. */ relationship?: AFRelationship; /** Description, if specified. */ diff --git a/tests/list.test.ts b/tests/list.test.ts index 4469a11..aaa1203 100644 --- a/tests/list.test.ts +++ b/tests/list.test.ts @@ -12,12 +12,16 @@ describe('list', () => { it('should list embedded files with metadata', async () => { const pdf = await createBlankPdf(); const data = JSON.stringify({ test: true }); + const creationDate = new Date('2025-02-03T10:11:12.000Z'); + const modificationDate = new Date('2026-07-20T14:15:16.000Z'); const withFile = await embed(pdf, data, { filename: 'vitaeflow.json', mimeType: 'application/json', relationship: 'Alternative', description: 'Structured resume data', + creationDate, + modificationDate, }); const files = await list(withFile); @@ -26,5 +30,7 @@ describe('list', () => { expect(files[0].relationship).toBe('Alternative'); expect(files[0].description).toBe('Structured resume data'); expect(files[0].size).toBeGreaterThan(0); + expect(files[0].creationDate).toEqual(creationDate); + expect(files[0].modificationDate).toEqual(modificationDate); }); });