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
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
- Add robust handling of null byte padding in JPEG images
- Replace outdated jpeg-exif with minimal implementation
- Replace outdated crypto-js with maintained small alternatives
- Fix issue with indentation with `indentAllLines: true` when a new page is created
- Fix issue with indentation with `indentAllLines: true` when a new page is created
- Extend cornerRadius param to accept an array of radii

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

Expand All @@ -25,7 +26,7 @@
- Fix null values in table cells rendering as `[object Object]`
- Fix further LineWrapper precision issues
- Optmize standard font handling. Less code, less memory usage

### [v0.17.0] - 2025-04-12

- Fix precision rounding issues in LineWrapper
Expand Down
24 changes: 14 additions & 10 deletions docs/vector.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ easier. Here is a list of the helpers.
* `circle(centerX, centerY, radius)`
* `polygon(points...)`

`roundedRect` `cornerRadius` param accepts a single radius (number) or per-corner radii (array).
If an array, the order is: `[topRight, bottomRight, bottomLeft, topLeft]`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you adjust the order to match the css?

/* top-left | top-right | bottom-right | bottom-left */

https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/border-radius

Also please rename cornerRadius to borderRadius

For example, to round only the right side corners: `roundedRect(x, y, w, h, [20, 20, 0, 0])`.

The last one, `polygon`, allows you to pass in a list of points (arrays of x,y
pairs), and it will create the shape by moving to the first point, and then
drawing lines to each consecutive point. Here is how you'd draw a triangle
Expand All @@ -86,16 +90,16 @@ path.
In order to make our drawings interesting, we really need to give them some
style. PDFKit has many methods designed to do just that.

* `lineWidth`
* `lineCap`
* `lineJoin`
* `miterLimit`
* `dash`
* `fillColor`
* `strokeColor`
* `opacity`
* `fillOpacity`
* `strokeOpacity`
* `lineWidth`
* `lineCap`
* `lineJoin`
* `miterLimit`
* `dash`
* `fillColor`
* `strokeColor`
* `opacity`
* `fillOpacity`
* `strokeOpacity`

Some of these are pretty self explanatory, but let's go through a few of them.

Expand Down
82 changes: 65 additions & 17 deletions lib/mixins/vector.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,24 +112,72 @@ export default {
);
},

roundedRect(x, y, w, h, r) {
if (r == null) {
r = 0;
/**
* @param {number|number[]} cornerRadius - Corner radius (number) or per-corner radii (array).
* If an array, order is: top-right, bottom-right, bottom-left, top-left.
*/
roundedRect(x, y, w, h, cornerRadius) {
if (cornerRadius == null) {
cornerRadius = 0;
}
r = Math.min(r, 0.5 * w, 0.5 * h);

// amount to inset control points from corners (see `ellipse`)
const c = r * (1.0 - KAPPA);

this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.bezierCurveTo(x + w - c, y, x + w, y + c, x + w, y + r);
this.lineTo(x + w, y + h - r);
this.bezierCurveTo(x + w, y + h - c, x + w - c, y + h, x + w - r, y + h);
this.lineTo(x + r, y + h);
this.bezierCurveTo(x + c, y + h, x, y + h - c, x, y + h - r);
this.lineTo(x, y + r);
this.bezierCurveTo(x, y + c, x + c, y, x + r, y);

let radii;
if (Array.isArray(cornerRadius)) {
radii = cornerRadius.slice(0, 4);
} else {
radii = [
cornerRadius, // top-right
cornerRadius, // bottom-right
cornerRadius, // bottom-left
cornerRadius, // top-left
];
}

const limit = Math.min(0.5 * w, 0.5 * h);
const rTR = Math.max(0, Math.min(radii[0] || 0, limit));
const rBR = Math.max(0, Math.min(radii[1] || 0, limit));
const rBL = Math.max(0, Math.min(radii[2] || 0, limit));
const rTL = Math.max(0, Math.min(radii[3] || 0, limit));

const cpTR = rTR * (1.0 - KAPPA);
const cpBR = rBR * (1.0 - KAPPA);
const cpBL = rBL * (1.0 - KAPPA);
const cpTL = rTL * (1.0 - KAPPA);

// Start at the top edge, inset by top-left radius.
this.moveTo(x + rTL, y);

// Top edge to top-right.
this.lineTo(x + w - rTR, y);
if (rTR > 0) {
this.bezierCurveTo(x + w - cpTR, y, x + w, y + cpTR, x + w, y + rTR);
}

// Right edge to bottom-right.
this.lineTo(x + w, y + h - rBR);
if (rBR > 0) {
this.bezierCurveTo(
x + w,
y + h - cpBR,
x + w - cpBR,
y + h,
x + w - rBR,
y + h,
);
}

// Bottom edge to bottom-left.
this.lineTo(x + rBL, y + h);
if (rBL > 0) {
this.bezierCurveTo(x + cpBL, y + h, x, y + h - cpBL, x, y + h - rBL);
}

// Left edge to top-left.
this.lineTo(x, y + rTL);
if (rTL > 0) {
this.bezierCurveTo(x, y + cpTL, x + cpTL, y, x + rTL, y);
}

return this.closePath();
},

Expand Down
75 changes: 74 additions & 1 deletion tests/unit/vector.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PDFDocument from '../../lib/document';
import { logData } from './helpers';
import { logData, getObjects } from './helpers';

describe('Vector Graphics', () => {
let document;
Expand Down Expand Up @@ -176,4 +176,77 @@ describe('Vector Graphics', () => {
]);
});
});

describe('roundedRect', () => {
test('uses cornerRadius to draw rounded corners by default', () => {
const docData = logData(document);

document.roundedRect(50, 50, 100, 80, 20).stroke();
document.end();

const objects = getObjects(docData);
const vectorObject = objects.find((obj) =>
obj.items.some((item) => item instanceof Buffer),
);
const streamBuffer =
vectorObject &&
vectorObject.items.find((item) => item instanceof Buffer);
const streamString = streamBuffer?.toString('ascii') || '';

// Expect at least one Bezier curve command (`c`) in the vector stream
expect(streamString).toMatch(/\sc[\s\n]/);
});

test('cornerRadius array can disable rounded corners', () => {
const docData = logData(document);

document.roundedRect(50, 50, 100, 80, [0, 0, 0, 0]).stroke();
document.end();

const objects = getObjects(docData);
const vectorObject = objects.find((obj) =>
obj.items.some((item) => item instanceof Buffer),
);
const streamBuffer =
vectorObject &&
vectorObject.items.find((item) => item instanceof Buffer);
const streamString = streamBuffer?.toString('ascii') || '';

// No Bezier curve command (`c`) should be present when all corners are disabled
expect(streamString).not.toMatch(/\sc[\s\n]/);
});

test('top-right corner ends at expected point', () => {
const docData = logData(document);

const x = 10;
const y = 20;
const w = 30;
const r = 5;

// Only the top-right corner is rounded
document.roundedRect(x, y, w, 40, [r, 0, 0, 0]).stroke();
document.end();

const objects = getObjects(docData);
const vectorObject = objects.find((obj) =>
obj.items.some((item) => item instanceof Buffer),
);
const streamBuffer =
vectorObject &&
vectorObject.items.find((item) => item instanceof Buffer);
const streamString = streamBuffer?.toString('ascii') || '';

const expectedX = x + w; // 40
const expectedY = y + r; // 25

// Look for a cubic Bezier segment whose end point is (expectedX, expectedY)
// Numbers are written using PDFObject.number, but these coordinates are integers.
const endPointPattern = new RegExp(
`\\b${expectedX}(?:\\.0+)?\\s+${expectedY}(?:\\.0+)?\\s+c\\b`,
);

expect(streamString).toMatch(endPointPattern);
});
});
});
Loading