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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Unreleased

- Add support for Embedded Files and File Attachment Annotations
- Accessibility support
- Replace integration tests by visual regression tests
- Fix access permissions in PDF version 1.7ext3
Expand Down
42 changes: 42 additions & 0 deletions demo/attachment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const PDFDocument = require('../');
const fs = require('fs');
const path = require('path');

const doc = new PDFDocument({ pdfVersion: '1.4' });

doc.pipe(fs.createWriteStream('attachment.pdf'));

doc.info['Title'] = 'Attachment Test';

// add an embedded file from file system
doc.file(path.join(__dirname, 'images', 'test.png'), {
name: 'test.png',
type: 'image/png',
description: 'this is a test image'
});

// add some text
doc.text(`This PDF contains three text files:
Two file attachment annotations and one embedded file.
If you can see them (not every PDF viewer supports embedded files),
hover over the paperclip to see its description!`);

// add a file attachment annotation
// first, declare the file to be attached
const file = {
src: Buffer.from('buffered input!'),
name: 'embedded.txt',
creationDate: new Date(2020, 3, 1)
};
// then, add the annotation
doc.fileAnnotation(100, 150, 10, doc.currentLineHeight(), file);

// declared files can be reused, but they will show up separately in the PDF Viewer's attachments panel
// we're going to use the paperclip icon for this one together with a short description
// be aware that some PDF Viewers may not render the icon correctly — or not at all
doc.fileAnnotation(150, 150, 10, doc.currentLineHeight(), file, {
Name: 'Paperclip',
Contents: 'Paperclip attachment'
});

doc.end();
Binary file added demo/attachment.pdf
Binary file not shown.
1 change: 1 addition & 0 deletions docs/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and some other properties. Here is a list of the available annotation methods:
* `rectAnnotation(x, y, width, height, options)`
* `ellipseAnnotation(x, y, width, height, options)`
* `textAnnotation(x, y, width, height, text, options)`
* `fileAnnotation(x, y, width, height, file, options)`

Many of the annotations have a `color` option that you can specify. You can
use an array of RGB values, a hex color, or a named CSS color value for that
Expand Down
53 changes: 53 additions & 0 deletions docs/attachments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Attachments in PDFKit

## Embedded Files

Embedded files make it possible to embed any external file into a PDF.
Adding an embedded file is as simple as calling the `file` method and specifying a filepath.

doc.file(path.join(__dirname, 'example.txt'))

It is also possible to embed data directly as a Buffer, ArrayBuffer or base64 encoded string.
If you are embedding data, it is recommended you also specify a filename like this:

doc.file(Buffer.from('this will be a text file'), { name: 'example.txt' })

When embedding a data URL, the `type` option will be set to the data URL's MIME type automatically:

doc.file('data:text/plain;base64,YmFzZTY0IHN0cmluZw==', { name: 'base64.txt' })

There are a few other options for `doc.file`:

* `name` - specify the embedded file's name
* `type` - specify the embedded file's subtype as a [MIME-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)
* `description` - add descriptive text for the embedded file
* `hidden` - if true, do not show file in the list of embedded files
* `creationDate` - override the date and time the file was created
* `modifiedDate` - override the date and time the file was last updated

If you are attaching a file from your file system, creationDate and modifiedDate will be set to the source file's creationDate and modifiedDate.

Setting the `hidden` option prevents this file from showing up in the pdf viewer's attachment panel.
While this may not be very useful for embedded files, it is absolutely necessary for file annotations, to prevent them from showing up twice in the attachment panel.

## File Annotations

A file annotation contains a reference to an embedded file that can be placed anywhere in the document.
File annotations show up in your reader's annotation panel as well as the attachment panel.

In order to add a file annotation, you should first read the chapter on annotations.
Like other annotations, you specify position and size with `x`, `y`, `width` and `height`, unlike other annotations you must also specify a file object.
The file object may contain the same options as `doc.file` in the previous section with the addition of the source file or buffered data in `src`.

Here is an example of adding a file annotation:

const file = {
src: path.join(__dirname, 'example.txt'),
name: 'example.txt',
description: 'file annotation description'
}
const options = { Name: 'Paperclip' }

doc.fileAnnotation(100, 100, 100, 100, file, options)

The annotation's appearance may be changed by setting the `Name` option to one of the three predefined icons `GraphPush`, `Paperclip` or `Push` (default value).
1 change: 1 addition & 0 deletions docs/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ render(doc, 'images.md');
render(doc, 'outline.md');
render(doc, 'annotations.md');
render(doc, 'destinations.md');
render(doc, 'attachments.md');
render(doc, 'accessibility.md');
render(doc, 'you_made_it.md');
doc.end();
1 change: 1 addition & 0 deletions docs/generate_website.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const files = [
'outline.md',
'annotations.md',
'destinations.md',
'attachments.md',
'accessibility.md',
'you_made_it.md'
];
Expand Down
Binary file modified docs/guide.pdf
Binary file not shown.
12 changes: 12 additions & 0 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import AnnotationsMixin from './mixins/annotations';
import OutlineMixin from './mixins/outline';
import MarkingsMixin from './mixins/markings';
import AcroFormMixin from './mixins/acroform';
import AttachmentsMixin from './mixins/attachments';
import LineWrapper from './line_wrapper';

class PDFDocument extends stream.Readable {
Expand Down Expand Up @@ -212,6 +213,16 @@ class PDFDocument extends stream.Readable {
this._root.data.Names.data.Dests.add(name, args);
}

addNamedEmbeddedFile(name, ref) {
if (!this._root.data.Names.data.EmbeddedFiles) {
// disabling /Limits for this tree fixes attachments not showing in Adobe Reader
this._root.data.Names.data.EmbeddedFiles = new PDFNameTree({ limits: false });
}

// add filespec to EmbeddedFiles
this._root.data.Names.data.EmbeddedFiles.add(name, ref);
}

addNamedJavaScript(name, js) {
if (!this._root.data.Names.data.JavaScript) {
this._root.data.Names.data.JavaScript = new PDFNameTree();
Expand Down Expand Up @@ -366,6 +377,7 @@ mixin(AnnotationsMixin);
mixin(OutlineMixin);
mixin(MarkingsMixin);
mixin(AcroFormMixin);
mixin(AttachmentsMixin);

PDFDocument.LineWrapper = LineWrapper;

Expand Down
20 changes: 20 additions & 0 deletions lib/mixins/annotations.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ export default {
return this.annotate(x, y, w, h, options);
},

fileAnnotation(x, y, w, h, file = {}, options = {}) {
// create hidden file
const filespec = this.file(
file.src,
Object.assign({ hidden: true }, file)
);

options.Subtype = 'FileAttachment';
options.FS = filespec;

// add description from filespec unless description (Contents) has already been set
if (options.Contents) {
options.Contents = new String(options.Contents);
} else if (filespec.data.Desc) {
options.Contents = filespec.data.Desc;
}

return this.annotate(x, y, w, h, options);
},

_convertRect(x1, y1, w, h) {
// flip y1 and y2
let y2 = y1;
Expand Down
118 changes: 118 additions & 0 deletions lib/mixins/attachments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const fs = require('fs');
const { createHash } = require('crypto');

export default {
/**
* Embed contents of `src` in PDF
* @param {Buffer | ArrayBuffer | string} src input Buffer, ArrayBuffer, base64 encoded string or path to file
* @param {object} options
* * options.name: filename to be shown in PDF, will use `src` if none set
* * options.type: filetype to be shown in PDF
* * options.description: description to be shown in PDF
* * options.hidden: if true, do not add attachment to EmbeddedFiles dictionary. Useful for file attachment annotations
* * options.creationDate: override creation date
* * options.modifiedDate: override modified date
* @returns filespec reference
*/
file(src, options = {}) {
options.name = options.name || src;

const refBody = {
Type: 'EmbeddedFile',
Params: {}
};
let data;

if (!src) {
throw new Error('No src specified');
}
if (Buffer.isBuffer(src)) {
data = src;
} else if (src instanceof ArrayBuffer) {
data = Buffer.from(new Uint8Array(src));
} else {
let match;
if ((match = /^data:(.*);base64,(.*)$/.exec(src))) {
if (match[1]) {
refBody.Subtype = match[1].replace('/', '#2F');
}
data = Buffer.from(match[2], 'base64');
} else {
data = fs.readFileSync(src);
if (!data) {
throw new Error(`Could not read contents of file at filepath ${src}`);
}

// update CreationDate and ModDate
const { birthtime, ctime } = fs.statSync(src);
refBody.Params.CreationDate = birthtime;
refBody.Params.ModDate = ctime;
}
}

// override creation date and modified date
if (options.creationDate instanceof Date) {
refBody.Params.CreationDate = options.creationDate;
}
if (options.modifiedDate instanceof Date) {
refBody.Params.ModDate = options.modifiedDate;
}
// add optional subtype
if (options.type) {
refBody.Subtype = options.type.replace('/', '#2F');
}

// add checksum and size information
const checksum = createHash('md5')
.update(data)
.digest('hex');
refBody.Params.CheckSum = new String(checksum);
refBody.Params.Size = data.byteLength;

// save some space when embedding the same file again
// if a file with the same name and metadata exists, reuse its reference
let ref;
if (!this._fileRegistry) this._fileRegistry = {};
let file = this._fileRegistry[options.name];
if (file && isEqual(refBody, file)) {
ref = file.ref;
} else {
ref = this.ref(refBody);
ref.end(data);

this._fileRegistry[options.name] = { ...refBody, ref };
}
// add filespec for embedded file
const fileSpecBody = {
Type: 'Filespec',
F: new String(options.name),
EF: { F: ref },
UF: new String(options.name)
};
if (options.description) {
fileSpecBody.Desc = new String(options.description);
}
const filespec = this.ref(fileSpecBody);
filespec.end();

if (!options.hidden) {
this.addNamedEmbeddedFile(options.name, filespec);
}

return filespec;
}
};

/** check two embedded file metadata objects for equality */
function isEqual(a, b) {
if (
a.Subtype !== b.Subtype ||
a.Params.CheckSum.toString() !== b.Params.CheckSum.toString() ||
a.Params.Size !== b.Params.Size ||
a.Params.CreationDate !== b.Params.CreationDate ||
a.Params.ModDate !== b.Params.ModDate
) {
return false;
}
return true;
}
7 changes: 5 additions & 2 deletions lib/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ PDFTree - abstract base class for name and number tree objects
import PDFObject from './object';

class PDFTree {
constructor() {
constructor(options = {}) {
this._items = {};
// disable /Limits output for this tree
this.limits =
typeof options.limits === 'boolean' ? options.limits : true;
}

add(key, val) {
Expand All @@ -24,7 +27,7 @@ class PDFTree {
);

const out = ['<<'];
if (sortedKeys.length > 1) {
if (this.limits && sortedKeys.length > 1) {
const first = sortedKeys[0],
last = sortedKeys[sortedKeys.length - 1];
out.push(
Expand Down
Loading