From 0a433874f5119736af7e37a9f45a8628f8a44f2e Mon Sep 17 00:00:00 2001 From: andreiaugustin Date: Sat, 21 Mar 2026 12:17:19 +0200 Subject: [PATCH] add opacity to doc.image options --- CHANGELOG.md | 1 + docs/images.md | 1 + lib/mixins/images.js | 4 +++ tests/unit/image.spec.js | 54 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb05cb51..37b1a873d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ### [v0.18.0] - 2026-03-14 diff --git a/docs/images.md b/docs/images.md index 2e1723b78..8e9ec6ecd 100644 --- a/docs/images.md +++ b/docs/images.md @@ -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: diff --git a/lib/mixins/images.js b/lib/mixins/images.js index 8486b9b44..ff234a8ea 100644 --- a/lib/mixins/images.js +++ b/lib/mixins/images.js @@ -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], diff --git a/tests/unit/image.spec.js b/tests/unit/image.spec.js index af6e4cf1f..24a1ca5ea 100644 --- a/tests/unit/image.spec.js +++ b/tests/unit/image.spec.js @@ -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); + }); + }); });