Merge pull request #21372 from calixteman/issue7998

Render gray transparency groups in grayscale
This commit is contained in:
Tim van der Meij 2026-06-02 20:10:18 +02:00 committed by GitHub
commit 744c1e6d7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 54 additions and 0 deletions

View File

@ -523,6 +523,7 @@ class PartialEvaluator {
isolated: false,
knockout: false,
needsIsolation: false,
isGray: false,
};
const groupSubtype = group.get("S");
@ -541,6 +542,10 @@ class PartialEvaluator {
}
}
// When the group color space is gray (a single component) the group's
// content must be rendered in grayscale, see issue 7998.
groupOptions.isGray = colorSpace?.numComps === 1;
if (smask?.backdrop) {
colorSpace ||= ColorSpaceUtils.rgb;
smask.backdrop = colorSpace.getRgbHex(smask.backdrop, 0);

View File

@ -3262,6 +3262,7 @@ class CanvasGraphics {
if (
!group.needsIsolation &&
!group.knockout &&
!group.isGray &&
this.#knockoutGroupLevel === 0 &&
currentCtx.globalAlpha === 1 &&
currentCtx.globalCompositeOperation === "source-over" &&
@ -3473,6 +3474,13 @@ class CanvasGraphics {
return;
}
if (group.isGray) {
// The group color space is gray (a single component), so its rendered
// content must be converted to grayscale before being composited onto
// the parent canvas, see issue 7998.
this.#convertGroupToGray(groupCtx);
}
this.ctx = ctx;
// Turn off image smoothing to avoid sub pixel interpolation which can
// look kind of blurry for some pdfs.
@ -3604,6 +3612,38 @@ class CanvasGraphics {
}
}
#convertGroupToGray(groupCtx) {
const { canvas } = groupCtx;
const { width, height } = canvas;
if (FeatureTest.isCanvasFilterSupported) {
// Draw the canvas onto itself with the grayscale filter applied (which
// preserves the alpha channel), using the "copy" composite operation so
// the filtered content fully replaces the original.
groupCtx.save();
groupCtx.setTransform(1, 0, 0, 1, 0, 0);
groupCtx.filter = "grayscale(1)";
groupCtx.globalAlpha = 1;
groupCtx.globalCompositeOperation = "copy";
groupCtx.drawImage(canvas, 0, 0);
groupCtx.restore();
return;
}
// Fallback when canvas filters aren't supported: convert each pixel to
// grayscale by hand, using the same luminance coefficients as the
// "grayscale(1)" filter while leaving the alpha channel untouched.
const imageData = groupCtx.getImageData(0, 0, width, height);
const { data } = imageData;
for (let i = 0, ii = data.length; i < ii; i += 4) {
const gray =
(data[i] * 0.2126 + data[i + 1] * 0.7152 + data[i + 2] * 0.0722 + 0.5) |
0;
data[i] = data[i + 1] = data[i + 2] = gray;
}
groupCtx.putImageData(imageData, 0, 0);
}
#destroyKnockoutPools(groupMeta) {
if (!groupMeta) {
return;

View File

@ -0,0 +1 @@
https://bugs.ghostscript.com/attachment.cgi?id=13156

View File

@ -3822,6 +3822,14 @@
"link": false,
"type": "eq"
},
{
"id": "issue7998",
"file": "pdfs/issue7998.pdf",
"md5": "6cf4622cbcb61fb07525dfb0ab23f9c3",
"rounds": 1,
"link": true,
"type": "eq"
},
{
"id": "issue11279",
"file": "pdfs/issue11279.pdf",