Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
});

Expand Down
11 changes: 11 additions & 0 deletions src/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { AFRelationship, EmbeddedFileInfo } from './types.js';
import {
getEmbeddedFilesDict,
collectNameTreeEntries,
pdfDateValue,
pdfStringValue,
readPdfNumber,
resolve,
Expand Down Expand Up @@ -72,6 +73,16 @@ export async function list(pdf: Uint8Array): Promise<EmbeddedFileInfo[]> {
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;
Expand Down
16 changes: 16 additions & 0 deletions src/pdf-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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. */
Expand Down
6 changes: 6 additions & 0 deletions tests/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
});
});
Loading