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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ playground/
build/
js/
.vscode
.idea
coverage
package-lock.json
/examples/browserify/bundle.js
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Unreleased

- Fix garbled text copying in Chrome/Edge for PDFs with >256 unique characters (#1659)
- Fix Link accessibility issues

### [v0.17.2] - 2025-08-30

Expand Down
87 changes: 87 additions & 0 deletions examples/accessible-links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
var PDFDocument = require('../');
var fs = require('fs');

// Create a new PDFDocument
var doc = new PDFDocument({
autoFirstPage: true,
bufferPages: true,
pdfVersion: '1.5',
// @ts-ignore PDF/UA needs to be enforced for PAC accessibility checker
subset: 'PDF/UA',
tagged: true,
displayTitle: true,
lang: 'en-US',
fontSize: 12,
});

doc.pipe(fs.createWriteStream('accessible-links.pdf'));

// Set some meta data
doc.info['Title'] = 'Test Document';
doc.info['Author'] = 'Devon Govett';

// Initialise document logical structure
var struct = doc.struct('Document');
doc.addStructure(struct);

// Register a font name for use later
doc.registerFont('Palatino', 'fonts/PalatinoBold.ttf');

// Set the font and draw some text
struct.add(
doc.struct('P', () => {
doc
.font('Palatino')
.fontSize(25)
.text('Some text with an embedded font! ', 100, 100);
}),
);

// Add another page
doc.addPage();

// Add some text with annotations
var linkSection = doc.struct('Sect');
struct.add(linkSection);

var paragraph = doc.struct('P');
linkSection.add(paragraph);

paragraph.add(
doc.struct('Span', () => {
doc
.font('Palatino')
.fillColor('black')
.text('This is some text before ', 100, 100, {
continued: true,
});
}),
);

paragraph.add(
doc.struct(
'Link',
{
alt: 'Here is a link! ',
},
() => {
doc.fillColor('blue').text('Here is a link!', {
link: 'http://google.com/',
underline: true,
continued: true,
});
},
),
);

paragraph.add(
doc.struct('Span', () => {
doc.fillColor('black').text(' and this is text after the link.');
}),
);

paragraph.end();
linkSection.end();

// End and flush the document
doc.end();
Binary file added examples/accessible-links.pdf
Binary file not shown.
4 changes: 3 additions & 1 deletion examples/kitchen-sink-accessible.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ var doc = new PDFDocument({
pdfVersion: '1.5',
lang: 'en-US',
tagged: true,
displayTitle: true
displayTitle: true,
// @ts-ignore PDF/UA needs to be enforced for PAC accessibility checker
subset: 'PDF/UA',
});

doc.pipe(fs.createWriteStream('kitchen-sink-accessible.pdf'));
Expand Down
Binary file modified examples/kitchen-sink-accessible.pdf
Binary file not shown.
15 changes: 15 additions & 0 deletions lib/mixins/annotations.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import PDFAnnotationReference from '../structure_annotation';

export default {
annotate(x, y, w, h, options) {
options.Type = 'Annot';
Expand All @@ -19,6 +21,9 @@ export default {
options.Dest = new String(options.Dest);
}

const structParent = options.structParent;
delete options.structParent;

// Capitalize keys
for (let key in options) {
const val = options[key];
Expand All @@ -27,6 +32,12 @@ export default {

const ref = this.ref(options);
this.page.annotations.push(ref);

if (structParent && typeof structParent.add === 'function') {
const annotRef = new PDFAnnotationReference(ref);
structParent.add(annotRef);
}

ref.end();
return this;
},
Expand Down Expand Up @@ -77,6 +88,10 @@ export default {
options.A.end();
}

if (options.structParent && !options.Contents) {
options.Contents = new String('');
}

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

Expand Down
7 changes: 7 additions & 0 deletions lib/mixins/markings.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ export default {
endMarkedContent() {
this.page.markings.pop();
this.addContent('EMC');
if (this._textOptions) {
delete this._textOptions.link;
delete this._textOptions.goTo;
delete this._textOptions.destination;
delete this._textOptions.underline;
delete this._textOptions.strike;
}
return this;
},

Expand Down
16 changes: 15 additions & 1 deletion lib/mixins/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,21 @@ export default {

// create link annotations if the link option is given
if (options.link != null) {
this.link(x, y, renderedWidth, this.currentLineHeight(), options.link);
const linkOptions = {};
if (
this._currentStructureElement &&
this._currentStructureElement.dictionary.data.S === 'Link'
) {
linkOptions.structParent = this._currentStructureElement;
}
this.link(
x,
y,
renderedWidth,
this.currentLineHeight(),
options.link,
linkOptions,
);
}
if (options.goTo != null) {
this.goTo(x, y, renderedWidth, this.currentLineHeight(), options.goTo);
Expand Down
7 changes: 7 additions & 0 deletions lib/structure_annotation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class PDFAnnotationReference {
constructor(annotationRef) {
this.annotationRef = annotationRef;
}
}

export default PDFAnnotationReference;
36 changes: 36 additions & 0 deletions lib/structure_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ By Ben Schmidt
*/

import PDFStructureContent from './structure_content';
import PDFAnnotationReference from './structure_annotation';

class PDFStructureElement {
constructor(document, type, options = {}, children = null) {
Expand Down Expand Up @@ -71,6 +72,10 @@ class PDFStructureElement {
this._addContentToParentTree(child);
}

if (child instanceof PDFAnnotationReference) {
this._addAnnotationToParentTree(child.annotationRef);
}

if (typeof child === 'function' && this._attached) {
// _contentForClosure() adds the content to the parent tree
child = this._contentForClosure(child);
Expand All @@ -90,6 +95,15 @@ class PDFStructureElement {
});
}

_addAnnotationToParentTree(annotRef) {
const parentTreeKey = this.document.createStructParentTreeNextKey();

annotRef.data.StructParent = parentTreeKey;

const parentTree = this.document.getStructParentTree();
parentTree.add(parentTreeKey, this.dictionary);
}

setParent(parentRef) {
if (this.dictionary.data.P) {
throw new Error(`Structure element added to more than one parent`);
Expand Down Expand Up @@ -137,13 +151,25 @@ class PDFStructureElement {
return (
child instanceof PDFStructureElement ||
child instanceof PDFStructureContent ||
child instanceof PDFAnnotationReference ||
typeof child === 'function'
);
}

_contentForClosure(closure) {
const content = this.document.markStructureContent(this.dictionary.data.S);

const prevStructElement = this.document._currentStructureElement;
this.document._currentStructureElement = this;

const wasEnded = this._ended;
this._ended = false;

closure();

this._ended = wasEnded;

this.document._currentStructureElement = prevStructElement;
this.document.endMarkedContent();

this._addContentToParentTree(content);
Expand Down Expand Up @@ -209,6 +235,16 @@ class PDFStructureElement {
}
});
}

if (child instanceof PDFAnnotationReference) {
const pageRef = this.document.page.dictionary;
const objr = {
Type: 'OBJR',
Obj: child.annotationRef,
Pg: pageRef,
};
this.dictionary.data.K.push(objr);
}
}
}

Expand Down
37 changes: 37 additions & 0 deletions tests/unit/annotations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,41 @@ describe('Annotations', () => {
]);
});
});

describe('annotations with structure parent', () => {
test('should add structParent to link annotations', () => {
document = new PDFDocument({
info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) },
compress: false,
tagged: true,
});

const docData = logData(document);

const linkElement = document.struct('Link');
document.addStructure(linkElement);

document.link(100, 100, 100, 20, 'http://example.com', {
structParent: linkElement,
});

linkElement.end();
document.end();

const dataStr = docData.join('\n');
expect(dataStr).toContain('/StructParent 0');
expect(dataStr).toContain('/Contents ()');
});

test('should work without structParent (backwards compatibility)', () => {
const docData = logData(document);

document.link(100, 100, 100, 20, 'http://example.com');
document.end();

const dataStr = docData.join('\n');
expect(dataStr).toContain('/Subtype /Link');
expect(dataStr).not.toContain('/StructParent');
});
});
});
17 changes: 17 additions & 0 deletions tests/unit/markings.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,23 @@ EMC
document.struct('Foo', [1]);
}).toThrow();
});

test('_currentStructureElement tracking with closures', () => {
const section = document.struct('Sect');
document.addStructure(section);

let capturedStructElement = null;

const paragraph = document.struct('P', () => {
capturedStructElement = document._currentStructureElement;
});

section.add(paragraph);
section.end();
document.end();

expect(capturedStructElement).toBe(paragraph);
});
});

describe('accessible document', () => {
Expand Down
Loading
Loading