diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 0bcdab80b..6787040ca 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -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); diff --git a/src/display/canvas.js b/src/display/canvas.js index e0c6ac8b7..9e7ddd334 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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; diff --git a/test/pdfs/issue7998.pdf.link b/test/pdfs/issue7998.pdf.link new file mode 100644 index 000000000..7e6f3d875 --- /dev/null +++ b/test/pdfs/issue7998.pdf.link @@ -0,0 +1 @@ +https://bugs.ghostscript.com/attachment.cgi?id=13156 diff --git a/test/test_manifest.json b/test/test_manifest.json index 2e40a9204..49db65129 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -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",