From fc055dbd80a3ed66d9996a80a84effd4658c7086 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 12 Mar 2023 15:47:01 +0100 Subject: [PATCH 1/2] [api-minor] Extend general transfer function support to browsers without `OffscreenCanvas` This patch extends PR 16115 to work in all browsers, regardless of their `OffscreenCanvas` support, such that transfer functions will be applied to general rendering (and not just image data). In order to do this we introduce the `BaseFilterFactory` that is then extended in browsers/Node.js environments, similar to all the other factories used in the API, such that we always have the necessary factory available in `src/display/canvas.js`. These changes help simplify the existing `putBinaryImageData` function, and the new method can easily be stubbed-out in the Firefox PDF Viewer. *Please note:* This patch removes the old *partial* transfer function support, which only applied to image data, from Node.js environments since the `node-canvas` package currently doesn't support filters. However, this should hopefully be fine given that: - Transfer functions are not very commonly used in PDF documents. - Browsers in general, and Firefox in particular, are the *primary* development target for the PDF.js library. - The FAQ only lists Node.js as *mostly* supported, see https://github.com/mozilla/pdf.js/wiki/Frequently-Asked-Questions#faq-support --- src/display/api.js | 17 ++++- src/display/base_factory.js | 15 ++++ src/display/canvas.js | 131 ++++++++++------------------------- src/display/display_utils.js | 6 +- src/display/node_utils.js | 4 ++ src/pdf.js | 2 - test/driver.js | 3 + test/test_manifest.json | 16 +++++ 8 files changed, 92 insertions(+), 102 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 20c78009e..19afd48a8 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -46,8 +46,8 @@ import { deprecated, DOMCanvasFactory, DOMCMapReaderFactory, + DOMFilterFactory, DOMStandardFontDataFactory, - FilterFactory, isDataScheme, isValidFetchUrl, loadScript, @@ -71,17 +71,20 @@ const DELAYED_CLEANUP_TIMEOUT = 5000; // ms let DefaultCanvasFactory = DOMCanvasFactory; let DefaultCMapReaderFactory = DOMCMapReaderFactory; +let DefaultFilterFactory = DOMFilterFactory; let DefaultStandardFontDataFactory = DOMStandardFontDataFactory; if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC") && isNodeJS) { const { NodeCanvasFactory, NodeCMapReaderFactory, + NodeFilterFactory, NodeStandardFontDataFactory, } = require("./node_utils.js"); DefaultCanvasFactory = NodeCanvasFactory; DefaultCMapReaderFactory = NodeCMapReaderFactory; + DefaultFilterFactory = NodeFilterFactory; DefaultStandardFontDataFactory = NodeStandardFontDataFactory; } @@ -342,7 +345,7 @@ function getDocument(src) { const canvasFactory = src.canvasFactory || new DefaultCanvasFactory({ ownerDocument }); const filterFactory = - src.filterFactory || new FilterFactory({ docId, ownerDocument }); + src.filterFactory || new DefaultFilterFactory({ docId, ownerDocument }); // Parameters only intended for development/testing purposes. const styleElement = @@ -784,6 +787,13 @@ class PDFDocumentProxy { return this._transport.annotationStorage; } + /** + * @type {Object} The filter factory instance. + */ + get filterFactory() { + return this._transport.filterFactory; + } + /** * @type {number} Total number of pages in the PDF file. */ @@ -3324,7 +3334,7 @@ class InternalRenderTask { this.commonObjs, this.objs, this.canvasFactory, - isOffscreenCanvasSupported ? this.filterFactory : null, + this.filterFactory, { optionalContentConfig }, this.annotationCanvasMap, this.pageColors @@ -3429,6 +3439,7 @@ export { build, DefaultCanvasFactory, DefaultCMapReaderFactory, + DefaultFilterFactory, DefaultStandardFontDataFactory, getDocument, LoopbackPort, diff --git a/src/display/base_factory.js b/src/display/base_factory.js index 148b978fd..181289f1c 100644 --- a/src/display/base_factory.js +++ b/src/display/base_factory.js @@ -15,6 +15,20 @@ import { CMapCompressionType, unreachable } from "../shared/util.js"; +class BaseFilterFactory { + constructor() { + if (this.constructor === BaseFilterFactory) { + unreachable("Cannot initialize BaseFilterFactory."); + } + } + + addFilter(maps) { + return "none"; + } + + destroy() {} +} + class BaseCanvasFactory { constructor() { if (this.constructor === BaseCanvasFactory) { @@ -179,6 +193,7 @@ class BaseSVGFactory { export { BaseCanvasFactory, BaseCMapReaderFactory, + BaseFilterFactory, BaseStandardFontDataFactory, BaseSVGFactory, }; diff --git a/src/display/canvas.js b/src/display/canvas.js index 8cb0819f5..7e758fbf7 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -491,7 +491,7 @@ class CanvasExtraState { this.strokeAlpha = 1; this.lineWidth = 1; this.activeSMask = null; - this.transferMaps = null; + this.transferMaps = "none"; this.startNewPathAndClipBox([0, 0, width, height]); } @@ -588,7 +588,7 @@ class CanvasExtraState { } } -function putBinaryImageData(ctx, imgData, transferMaps = null) { +function putBinaryImageData(ctx, imgData) { if (typeof ImageData !== "undefined" && imgData instanceof ImageData) { ctx.putImageData(imgData, 0, 0); return; @@ -618,24 +618,6 @@ function putBinaryImageData(ctx, imgData, transferMaps = null) { const dest = chunkImgData.data; let i, j, thisChunkHeight, elemsInThisChunk; - let transferMapRed, transferMapGreen, transferMapBlue, transferMapGray; - if (transferMaps) { - switch (transferMaps.length) { - case 1: - transferMapRed = transferMaps[0]; - transferMapGreen = transferMaps[0]; - transferMapBlue = transferMaps[0]; - transferMapGray = transferMaps[0]; - break; - case 4: - transferMapRed = transferMaps[0]; - transferMapGreen = transferMaps[1]; - transferMapBlue = transferMaps[2]; - transferMapGray = transferMaps[3]; - break; - } - } - // There are multiple forms in which the pixel data can be passed, and // imgData.kind tells us which one this is. if (imgData.kind === ImageKind.GRAYSCALE_1BPP) { @@ -644,14 +626,8 @@ function putBinaryImageData(ctx, imgData, transferMaps = null) { const dest32 = new Uint32Array(dest.buffer, 0, dest.byteLength >> 2); const dest32DataLength = dest32.length; const fullSrcDiff = (width + 7) >> 3; - let white = 0xffffffff; - let black = FeatureTest.isLittleEndian ? 0xff000000 : 0x000000ff; - - if (transferMapGray) { - if (transferMapGray[0] === 0xff && transferMapGray[0xff] === 0) { - [white, black] = [black, white]; - } - } + const white = 0xffffffff; + const black = FeatureTest.isLittleEndian ? 0xff000000 : 0x000000ff; for (i = 0; i < totalChunks; i++) { thisChunkHeight = i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight; @@ -693,32 +669,12 @@ function putBinaryImageData(ctx, imgData, transferMaps = null) { } } else if (imgData.kind === ImageKind.RGBA_32BPP) { // RGBA, 32-bits per pixel. - const hasTransferMaps = !!( - transferMapRed || - transferMapGreen || - transferMapBlue - ); - j = 0; elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4; for (i = 0; i < fullChunks; i++) { dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); srcPos += elemsInThisChunk; - if (hasTransferMaps) { - for (let k = 0; k < elemsInThisChunk; k += 4) { - if (transferMapRed) { - dest[k + 0] = transferMapRed[dest[k + 0]]; - } - if (transferMapGreen) { - dest[k + 1] = transferMapGreen[dest[k + 1]]; - } - if (transferMapBlue) { - dest[k + 2] = transferMapBlue[dest[k + 2]]; - } - } - } - ctx.putImageData(chunkImgData, 0, j); j += FULL_CHUNK_HEIGHT; } @@ -726,30 +682,10 @@ function putBinaryImageData(ctx, imgData, transferMaps = null) { elemsInThisChunk = width * partialChunkHeight * 4; dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); - if (hasTransferMaps) { - for (let k = 0; k < elemsInThisChunk; k += 4) { - if (transferMapRed) { - dest[k + 0] = transferMapRed[dest[k + 0]]; - } - if (transferMapGreen) { - dest[k + 1] = transferMapGreen[dest[k + 1]]; - } - if (transferMapBlue) { - dest[k + 2] = transferMapBlue[dest[k + 2]]; - } - } - } - ctx.putImageData(chunkImgData, 0, j); } } else if (imgData.kind === ImageKind.RGB_24BPP) { // RGB, 24-bits per pixel. - const hasTransferMaps = !!( - transferMapRed || - transferMapGreen || - transferMapBlue - ); - thisChunkHeight = FULL_CHUNK_HEIGHT; elemsInThisChunk = width * thisChunkHeight; for (i = 0; i < totalChunks; i++) { @@ -766,20 +702,6 @@ function putBinaryImageData(ctx, imgData, transferMaps = null) { dest[destPos++] = 255; } - if (hasTransferMaps) { - for (let k = 0; k < destPos; k += 4) { - if (transferMapRed) { - dest[k + 0] = transferMapRed[dest[k + 0]]; - } - if (transferMapGreen) { - dest[k + 1] = transferMapGreen[dest[k + 1]]; - } - if (transferMapBlue) { - dest[k + 2] = transferMapBlue[dest[k + 2]]; - } - } - } - ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT); } } else { @@ -865,7 +787,10 @@ function resetCtxToDefault(ctx, foregroundColor) { ctx.setLineDash([]); ctx.lineDashOffset = 0; } - if (!isNodeJS) { + if ( + (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) || + !isNodeJS + ) { ctx.filter = "none"; } } @@ -1591,12 +1516,8 @@ class CanvasGraphics { this.checkSMaskState(); break; case "TR": - if (this.filterFactory) { - this.ctx.filter = this.current.transferMaps = - this.filterFactory.addFilter(value); - } else { - this.current.transferMaps = value; - } + this.ctx.filter = this.current.transferMaps = + this.filterFactory.addFilter(value); break; } } @@ -3042,8 +2963,25 @@ class CanvasGraphics { this.paintInlineImageXObjectGroup(imgData, map); } + applyTransferMapsToCanvas(ctx) { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + if (this.current.transferMaps !== "none") { + warn("Ignoring transferMaps - `OffscreenCanvas` support is disabled."); + } + return ctx.canvas; + } + if (this.current.transferMaps === "none") { + return ctx.canvas; + } + ctx.filter = this.current.transferMaps; + ctx.drawImage(ctx.canvas, 0, 0); + ctx.filter = "none"; + + return ctx.canvas; + } + applyTransferMapsToBitmap(imgData) { - if (!this.current.transferMaps || this.current.transferMaps === "none") { + if (this.current.transferMaps === "none") { return imgData.bitmap; } const { bitmap, width, height } = imgData; @@ -3070,7 +3008,10 @@ class CanvasGraphics { this.save(); - if (!isNodeJS) { + if ( + (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) || + !isNodeJS + ) { // The filter, if any, will be applied in applyTransferMapsToBitmap. // It must be applied to the image before rescaling else some artifacts // could appear. @@ -3097,8 +3038,8 @@ class CanvasGraphics { height ); const tmpCtx = tmpCanvas.context; - putBinaryImageData(tmpCtx, imgData, this.current.transferMaps); - imgToPaint = tmpCanvas.canvas; + putBinaryImageData(tmpCtx, imgData); + imgToPaint = this.applyTransferMapsToCanvas(tmpCtx); } const scaled = this._scaleImage( @@ -3140,8 +3081,8 @@ class CanvasGraphics { const tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h); const tmpCtx = tmpCanvas.context; - putBinaryImageData(tmpCtx, imgData, this.current.transferMaps); - imgToPaint = tmpCanvas.canvas; + putBinaryImageData(tmpCtx, imgData); + imgToPaint = this.applyTransferMapsToCanvas(tmpCtx); } for (const entry of map) { diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 103f6ee49..945e0db70 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -16,6 +16,7 @@ import { BaseCanvasFactory, BaseCMapReaderFactory, + BaseFilterFactory, BaseStandardFontDataFactory, BaseSVGFactory, } from "./base_factory.js"; @@ -48,7 +49,7 @@ class PixelsPerInch { * an image without the need to apply them on the pixel arrays: the renderer * does the magic for us. */ -class FilterFactory { +class DOMFilterFactory extends BaseFilterFactory { #_cache; #_defs; @@ -60,6 +61,7 @@ class FilterFactory { #id = 0; constructor({ docId, ownerDocument = globalThis.document } = {}) { + super(); this.#docId = docId; this.#document = ownerDocument; } @@ -823,9 +825,9 @@ export { deprecated, DOMCanvasFactory, DOMCMapReaderFactory, + DOMFilterFactory, DOMStandardFontDataFactory, DOMSVGFactory, - FilterFactory, getColorValues, getCurrentTransform, getCurrentTransformInverse, diff --git a/src/display/node_utils.js b/src/display/node_utils.js index 337dce853..e848ea543 100644 --- a/src/display/node_utils.js +++ b/src/display/node_utils.js @@ -17,6 +17,7 @@ import { BaseCanvasFactory, BaseCMapReaderFactory, + BaseFilterFactory, BaseStandardFontDataFactory, } from "./base_factory.js"; @@ -39,6 +40,8 @@ const fetchData = function (url) { }); }; +class NodeFilterFactory extends BaseFilterFactory {} + class NodeCanvasFactory extends BaseCanvasFactory { /** * @ignore @@ -72,5 +75,6 @@ class NodeStandardFontDataFactory extends BaseStandardFontDataFactory { export { NodeCanvasFactory, NodeCMapReaderFactory, + NodeFilterFactory, NodeStandardFontDataFactory, }; diff --git a/src/pdf.js b/src/pdf.js index f0ecd4a50..dcad1a231 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -51,7 +51,6 @@ import { version, } from "./display/api.js"; import { - FilterFactory, getFilenameFromUrl, getPdfFilenameFromUrl, getXfaPageViewport, @@ -91,7 +90,6 @@ export { createPromiseCapability, createValidAbsoluteUrl, FeatureTest, - FilterFactory, getDocument, getFilenameFromUrl, getPdfFilenameFromUrl, diff --git a/test/driver.js b/test/driver.js index e722ca7bb..56d507dde 100644 --- a/test/driver.js +++ b/test/driver.js @@ -469,6 +469,8 @@ class Driver { .getElementsByTagName("head")[0] .append(xfaStyleElement); } + const isOffscreenCanvasSupported = + task.isOffscreenCanvasSupported === false ? false : undefined; const loadingTask = getDocument({ url: new URL(task.file, window.location), @@ -480,6 +482,7 @@ class Driver { useSystemFonts: task.useSystemFonts, useWorkerFetch: task.useWorkerFetch, enableXfa: task.enableXfa, + isOffscreenCanvasSupported, styleElement: xfaStyleElement, }); let promise = loadingTask.promise; diff --git a/test/test_manifest.json b/test/test_manifest.json index 62552aabe..eb14cf6e9 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -6281,6 +6281,13 @@ "rounds": 1, "type": "eq" }, + { "id": "issue6931-disable-isOffscreenCanvasSupported", + "file": "pdfs/issue6931_reduced.pdf", + "md5": "e61388913821a5e044bf85a5846d6d9a", + "rounds": 1, + "type": "eq", + "isOffscreenCanvasSupported": false + }, { "id": "annotation-button-widget-annotations", "file": "pdfs/annotation-button-widget.pdf", "md5": "5cf23adfff84256d9cfe261bea96dade", @@ -7474,6 +7481,15 @@ "link": true, "type": "eq" }, + { + "id": "issue16114-disable-isOffscreenCanvasSupported", + "file": "pdfs/issue16114.pdf", + "md5": "c04827ea33692e0f94a5e51716d9aa2e", + "rounds": 1, + "link": true, + "type": "eq", + "isOffscreenCanvasSupported": false + }, { "id": "bug1820909", "file": "pdfs/bug1820909.pdf", From 50c844c5b84cf6f8b8fb5cc95246d1975e3cccd4 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 12 Mar 2023 22:53:42 +0100 Subject: [PATCH 2/2] Stop including `isOffscreenCanvasSupported` in the "StartRenderPage" message With the previous commit this is now completely unused in API, hence it can be removed. This is done in a separate commit to make it easier to re-instate it, would the need ever arise. --- src/core/document.js | 2 -- src/display/api.js | 49 +++++++++++++++----------------------------- 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/src/core/document.js b/src/core/document.js index a1d3c6c06..b66f4d56c 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -417,8 +417,6 @@ class Page { this.resources, this.nonBlendModesSet ), - isOffscreenCanvasSupported: - this.evaluatorOptions.isOffscreenCanvasSupported, pageIndex: this.pageIndex, cacheKey, }); diff --git a/src/display/api.js b/src/display/api.js index 19afd48a8..d13e223dc 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1521,25 +1521,19 @@ class PDFPageProxy { intentState.displayReadyCapability.promise, optionalContentConfigPromise, ]) - .then( - ([ - { transparency, isOffscreenCanvasSupported }, - optionalContentConfig, - ]) => { - if (this.#pendingCleanup) { - complete(); - return; - } - this._stats?.time("Rendering"); - - internalRenderTask.initializeGraphics({ - transparency, - isOffscreenCanvasSupported, - optionalContentConfig, - }); - internalRenderTask.operatorListChanged(); + .then(([transparency, optionalContentConfig]) => { + if (this.#pendingCleanup) { + complete(); + return; } - ) + this._stats?.time("Rendering"); + + internalRenderTask.initializeGraphics({ + transparency, + optionalContentConfig, + }); + internalRenderTask.operatorListChanged(); + }) .catch(complete); return renderTask; @@ -1763,7 +1757,7 @@ class PDFPageProxy { /** * @private */ - _startRenderPage(transparency, isOffscreenCanvasSupported, cacheKey) { + _startRenderPage(transparency, cacheKey) { const intentState = this._intentStates.get(cacheKey); if (!intentState) { return; // Rendering was cancelled. @@ -1772,10 +1766,7 @@ class PDFPageProxy { // TODO Refactor RenderPageRequest to separate rendering // and operator list logic - intentState.displayReadyCapability?.resolve({ - transparency, - isOffscreenCanvasSupported, - }); + intentState.displayReadyCapability?.resolve(transparency); } /** @@ -2737,11 +2728,7 @@ class WorkerTransport { } const page = this.#pageCache.get(data.pageIndex); - page._startRenderPage( - data.transparency, - data.isOffscreenCanvasSupported, - data.cacheKey - ); + page._startRenderPage(data.transparency, data.cacheKey); }); messageHandler.on("commonobj", ([id, type, exportedData]) => { @@ -3303,11 +3290,7 @@ class InternalRenderTask { }); } - initializeGraphics({ - transparency = false, - isOffscreenCanvasSupported = false, - optionalContentConfig, - }) { + initializeGraphics({ transparency = false, optionalContentConfig }) { if (this.cancelled) { return; }