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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Bump node version requirement to 20+
- Bump minimum supported browsers to Firefox 115, iOS/Safari 16
- Fix text with input x as null
- Add opacity option to `doc.image()` to control image transparency
- Fix PDF/UA compliance issues in kitchen-sink-accessible example
- Add bbox and placement options to PDFStructureElement for PDF/UA compliance

Expand Down
1 change: 1 addition & 0 deletions docs/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ be scaled according to the following options.
- `goTo` - go to anchor (shortcut to create an annotation)
- `destination` - create anchor to this image
- `ignoreOrientation` - (true/false) ignore JPEG EXIF orientation. By default, images with JPEG EXIF orientation are properly rotated and/or flipped. Defaults to `false`, unless `ignoreOrientation` option set to `true` when creating the `PDFDocument` object (e.g. `new PDFDocument({ignoreOrientation: true})`)
- `opacity` - a value between `0` (fully transparent) and `1` (fully opaque). For PNGs that already have an alpha channel, this compounds with the existing transparency

When a `fit` or `cover` array is provided, PDFKit accepts these additional options:

Expand Down
4 changes: 4 additions & 0 deletions lib/mixins/images.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ export default {

this.save();

if (options.opacity != null) {
this._doOpacity(options.opacity, null);
}

if (rotateAngle) {
this.rotate(rotateAngle, {
origin: [originX, originY],
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/image.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,58 @@ describe('Image', function () {
expect(jpeg.height).toBe(500);
expect(jpeg.orientation).toBe(1);
});

describe('opacity', function () {
test('adds an ExtGState with the correct ca value', () => {
document.image('./tests/images/bee.png', 0, 0, { opacity: 0.5 });

const gstates = document.page.ext_gstates;
const entry = Object.values(gstates)[0];
expect(entry.data.ca).toBe(0.5);
});

test('registers the ExtGState on the page resources', () => {
document.image('./tests/images/bee.png', 0, 0, { opacity: 0.5 });

expect(Object.keys(document.page.ext_gstates).length).toBe(1);
});

test('clamps opacity below 0 to 0', () => {
document.image('./tests/images/bee.png', 0, 0, { opacity: -0.5 });

const entry = Object.values(document.page.ext_gstates)[0];
expect(entry.data.ca).toBe(0);
});

test('clamps opacity above 1 to 1', () => {
document.image('./tests/images/bee.png', 0, 0, { opacity: 1.5 });

const entry = Object.values(document.page.ext_gstates)[0];
expect(entry.data.ca).toBe(1);
});

test('reuses the same ExtGState for the same opacity value', () => {
document.image('./tests/images/bee.png', 0, 0, { opacity: 0.5 });
document.image('./tests/images/bee.png', 100, 0, { opacity: 0.5 });

// both calls share one entry, not two
expect(Object.keys(document.page.ext_gstates).length).toBe(1);
});

test('does not add an ExtGState when no opacity is specified', () => {
document.image('./tests/images/bee.png', 0, 0);

expect(Object.keys(document.page.ext_gstates).length).toBe(0);
});

test('links the ExtGState into the page resources', () => {
document.image('./tests/images/bee.png', 0, 0, { opacity: 0.5 });
document.end();

const gstates = document.page.ext_gstates;
const [name, ref] = Object.entries(gstates)[0];
expect(name).toMatch(/^Gs\d+$/);
expect(ref.data.ca).toBe(0.5);
});
});
});
Loading