Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- Bump node version requirement to 20+
- Bump minimum supported browsers to Firefox 115, iOS/Safari 16
- Fix text with input x as null
- Fix PDF/UA compliance issues in kitchen-sink-accessible example
- Add bbox and placement options to PDFStructureElement for PDF/UA compliance

### [v0.18.0] - 2026-03-14

Expand Down
19 changes: 16 additions & 3 deletions docs/accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ When creating a structure element, you can provide options:
* `alt` - alternative text for an image or other visual content
* `expanded` - the expanded form of an abbreviation or acronym
* `actual` - the actual text the content represents (e.g. if it is rendered as vector graphics)
* `bbox` - bounding box of the element's content in PDFKit coordinates `[left, top, right, bottom]`, required for `Figure` elements in PDF/UA
* `placement` - layout placement of the element: `'Block'` (the default when `bbox` is set) or `'Inline'`

Example of a structure tree with options specified:

Expand All @@ -256,7 +258,8 @@ Example of a structure tree with options specified:
]),
]),
doc.struct('Figure', {
alt: 'photo of a concrete path with tactile paving'
alt: 'photo of a concrete path with tactile paving',
bbox: [100, 200, 500, 600]
}, [
photoStructureContent
])
Expand Down Expand Up @@ -359,7 +362,7 @@ Non-structure tags:
* `Reference` - content in a document that refers to other content (e.g. page number in an index)
* `BibEntry` - bibliography entry; may have a `Lbl` (see "block" elements)
* `Code` - code
* `Link` - hyperlink; should contain a link annotation
* `Link` - hyperlink
* `Annot` - annotation (other than a link)
* `Ruby` - Chinese/Japanese pronunciation/explanation
* `RB` - Ruby base text
Expand All @@ -371,6 +374,16 @@ Non-structure tags:

"Illustration" elements (should have `alt` and/or `actualtext` set):

* `Figure` - figure
* `Figure` - figure, should also have `bbox` set
* `Formula` - formula
* `Form` - form widget

## Limitations

### Built-in fonts

PDFKit ships with the 14 standard PDF fonts (Helvetica, Times-Roman, Courier, etc.) as AFM metric files only.
Because of this, these fonts cannot be embedded in the PDF output. Both PDF/UA and PDF/A require all fonts to
be embedded, so using any of the built-in fonts will result in a non-compliant document.
If you need to produce a compliant PDF, use embedded TrueType or OpenType fonts instead by loading them from
a file with `doc.font('path/to/font.ttf')`.
Binary file modified docs/guide.pdf
Binary file not shown.
20 changes: 20 additions & 0 deletions docs/vector.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,26 @@ that you don't have to call `fillColor` or `strokeColor` beforehand. The
.fillOpacity(0.8)
.fillAndStroke("red", "#900")

Note that if you are producing a PDF/UA-compliant PDF, `fillColor` and `strokeColor`
must be called before beginning path construction (i.e. before `moveTo`, `path`, `rect`,
`circle` and similar methods). The PDF spec (ISO 32000-2) does not allow color space operators
to be emitted during path construction, and passing a color directly to `fill`, `stroke` or
`fillAndStroke` can produce a non-compliant PDF. The safest approach is to always set
colors before defining the path:

// good: emits color operators before path
doc.fillColor('red')
.moveTo(100, 150)
.lineTo(100, 250)
.lineTo(200, 250)
.fill();

// not good: may emit color operators throughout path construction
doc.moveTo(100, 150)
.lineTo(100, 250)
.lineTo(200, 250)
.fill('red');

This example produces the following output:

![5](images/color.png "100")
Expand Down
58 changes: 43 additions & 15 deletions examples/kitchen-sink-accessible.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct.add(
var imageSection = doc.struct('Sect');
struct.add(imageSection);

doc.outline.addItem('PNG and JPEG images:'); // add a bookmark for the image section's H1
imageSection.add(
doc.struct('H1', () => {
doc.fontSize(18).text('PNG and JPEG images: ');
Expand All @@ -49,7 +50,8 @@ imageSection.add(
doc.struct(
'Figure',
{
alt: 'Promotional image of an Apple laptop. '
alt: 'Promotional image of an Apple laptop. ',
bbox: [100, 160, 512, 387]
},
() => {
doc.image('images/test.png', 100, 160, {
Expand All @@ -64,7 +66,8 @@ imageSection.add(
'Figure',
{
alt:
'Photograph of a path flanked by blossoming trees with surrounding hedges. '
'Photograph of a path flanked by blossoming trees with surrounding hedges. ',
bbox: [190, 400, 415, 700]
},
() => {
doc.image('images/test.jpeg', 190, 400, {
Expand All @@ -83,6 +86,7 @@ doc.addPage();
var vectorSection = doc.struct('Sect');
struct.add(vectorSection);

doc.outline.addItem('Vector graphics:'); // add a bookmark for the vector graphics section's H1
vectorSection.add(
doc.struct('H1', () => {
doc.fontSize(25).text('Here are some vector graphics... ', 100, 100);
Expand All @@ -93,15 +97,19 @@ vectorSection.add(
doc.struct(
'Figure',
{
alt: 'Orange triangle. '
alt: 'Orange triangle. ',
bbox: [100, 150, 200, 250]
},
() => {
// we set fill color before path construction to comply with ISO 32000-2 Figure 9
doc
.save()
.fillColor('#FF8800')
.moveTo(100, 150)
.lineTo(100, 250)
.lineTo(200, 250)
.fill('#FF8800');
.fill()
.restore();
}
)
);
Expand All @@ -110,10 +118,12 @@ vectorSection.add(
doc.struct(
'Figure',
{
alt: 'Purple circle. '
alt: 'Purple circle. ',
bbox: [230, 150, 330, 250]
},
() => {
doc.circle(280, 200, 50).fill('#7722FF');
// we set fill color before path construction to comply with ISO 32000-2 Figure 9
doc.save().fillColor('#7722FF').circle(280, 200, 50).fill().restore();
}
)
);
Expand All @@ -122,16 +132,20 @@ vectorSection.add(
doc.struct(
'Figure',
{
alt: 'Red star with hollow center. '
alt: 'Red star with hollow center. ',
bbox: [360, 128, 504, 266]
},
() => {
// we set fill color before path construction to comply with ISO 32000-2 Figure 9
doc
.save()
.fillColor('red')
.scale(0.6)
.translate(470, 140)
// render an SVG path
.path('M 250,75 L 323,301 131,161 369,161 177,301 z')
// fill using the even-odd winding rule
.fill('red', 'even-odd')
.fill('even-odd')
.restore();
}
)
Expand All @@ -143,6 +157,7 @@ vectorSection.end();
var wrappedSection = doc.struct('Sect');
struct.add(wrappedSection);

doc.outline.addItem('PNG and JPEG images:'); // add a bookmark for the wrapped text section's H1
wrappedSection.add(
doc.struct('H1', () => {
doc
Expand All @@ -155,7 +170,7 @@ wrappedSection.add(

var loremIpsum =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam in suscipit purus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vivamus nec hendrerit felis. Morbi aliquam facilisis risus eu lacinia. Sed eu leo in turpis fringilla hendrerit. Ut nec accumsan nisl. Suspendisse rhoncus nisl posuere tortor tempus et dapibus elit porta. Cras leo neque, elementum a rhoncus ut, vestibulum non nibh. Phasellus pretium justo turpis. Etiam vulputate, odio vitae tincidunt ultricies, eros odio dapibus nisi, ut tincidunt lacus arcu eu elit. Aenean velit erat, vehicula eget lacinia ut, dignissim non tellus. Aliquam nec lacus mi, sed vestibulum nunc. Suspendisse potenti. Curabitur vitae sem turpis. Vestibulum sed neque eget dolor dapibus porttitor at sit amet sem. Fusce a turpis lorem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;\nMauris at ante tellus. Vestibulum a metus lectus. Praesent tempor purus a lacus blandit eget gravida ante hendrerit. Cras et eros metus. Sed commodo malesuada eros, vitae interdum augue semper quis. Fusce id magna nunc. Curabitur sollicitudin placerat semper. Cras et mi neque, a dignissim risus. Nulla venenatis porta lacus, vel rhoncus lectus tempor vitae. Duis sagittis venenatis rutrum. Curabitur tempor massa tortor.';
doc.text(loremIpsum, {
doc.font('Palatino').text(loremIpsum, {
width: 412,
align: 'justify',
indent: 30,
Expand All @@ -172,6 +187,7 @@ doc.addPage();
var tigerSection = doc.struct('Sect');
struct.add(tigerSection);

doc.outline.addItem('Tiger line art:'); // add a bookmark for the tiger section's H1
tigerSection.add(
doc.struct('H1', () => {
doc
Expand All @@ -185,26 +201,34 @@ tigerSection.add(
doc.struct(
'Figure',
{
alt: 'Tiger line art. '
alt: 'Tiger line art. ',
bbox: [30, 140, 540, 680]
},
() => {
var i, len, part;
// Render each path that makes up the tiger image
for (i = 0, len = tiger.length; i < len; i++) {
part = tiger[i];
doc.save();
doc.path(part.path); // render an SVG path
// we set fill color before path construction to comply with ISO 32000-2 Figure 9
if (part.fill !== 'none') {
doc.fillColor(part.fill);
}
if (part.stroke !== 'none') {
doc.strokeColor(part.stroke);
}
if (part['stroke-width']) {
doc.lineWidth(part['stroke-width']);
}
doc.path(part.path); // render an SVG path
if (part.fill !== 'none' && part.stroke !== 'none') {
doc.fillAndStroke(part.fill, part.stroke);
doc.fillAndStroke();
} else {
if (part.fill !== 'none') {
doc.fill(part.fill);
doc.fill();
}
if (part.stroke !== 'none') {
doc.stroke(part.stroke);
doc.stroke();
}
}
doc.restore();
Expand All @@ -222,7 +246,10 @@ doc.addPage();
var linkSection = doc.struct('Sect');
struct.add(linkSection);

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

linkParagraph.add(
doc.struct(
'Link',
{
Expand All @@ -237,6 +264,7 @@ linkSection.add(
)
);

linkParagraph.end();
linkSection.end();

// Add a list with a font loaded from a TrueType collection file
Expand Down
Binary file modified examples/kitchen-sink-accessible.pdf
Binary file not shown.
18 changes: 18 additions & 0 deletions lib/structure_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ class PDFStructureElement {
if (typeof options.actual !== 'undefined') {
data.ActualText = new String(options.actual);
}
if (
typeof options.bbox !== 'undefined' ||
typeof options.placement !== 'undefined'
) {
const attrs = { O: 'Layout' };
attrs.Placement =
typeof options.placement !== 'undefined' ? options.placement : 'Block';
if (typeof options.bbox !== 'undefined') {
const height = this.document.page.height;
attrs.BBox = [
options.bbox[0],
height - options.bbox[3],
options.bbox[2],
height - options.bbox[1],
];
}
data.A = attrs;
}

this._children = [];

Expand Down
Loading
Loading