From 412d942bddbebe27716500611178658ffe053260 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 4 May 2022 15:37:13 +0200 Subject: [PATCH] Improve pdf reading in high contrast mode - Use Canvas & CanvasText color when they don't have their default value as background and foreground colors. - The colors used to draw (stroke/fill) in a pdf are replaced by the bg/fg ones according to their luminance. --- extensions/chromium/preferences_schema.json | 12 ++++ src/display/api.js | 9 ++- src/display/canvas.js | 62 +++++++++++++++++---- src/pdf.js | 2 + src/shared/util.js | 11 ++++ test/driver.js | 10 +++- test/test_manifest.json | 8 +++ web/app.js | 4 ++ web/app_options.js | 10 ++++ web/base_viewer.js | 16 ++++++ web/pdf_page_view.js | 4 ++ 11 files changed, 135 insertions(+), 13 deletions(-) diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index c8bd5e0c19b720..198bc07abd40c7 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -208,6 +208,18 @@ 2 ], "default": -1 + }, + "pageForegroundColor": { + "title": "Color to use as a foreground color in constrat mode context", + "description": "The color is a string as defined in CSS", + "type": "string", + "default": "CanvasText" + }, + "pageBackgroundColor": { + "title": "Color to use as a background color in constrat mode context", + "description": "The color is a string as defined in CSS", + "type": "string", + "default": "Canvas" } } } diff --git a/src/display/api.js b/src/display/api.js index 9c4c5f0b564289..8ff80b04dbea97 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1176,6 +1176,8 @@ class PDFDocumentProxy { * states set. * @property {Map} [annotationCanvasMap] - Map some * annotation ids with canvases used to render them. + * @property {Object} [pageColors] - Overwrites background and foreground colors + * with user defined ones. */ /** @@ -1393,6 +1395,7 @@ class PDFPageProxy { background = null, optionalContentConfigPromise = null, annotationCanvasMap = null, + pageColors = null, }) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) { if (arguments[0]?.renderInteractiveForms !== undefined) { @@ -1516,6 +1519,7 @@ class PDFPageProxy { canvasFactory: canvasFactoryInstance, useRequestAnimationFrame: !intentPrint, pdfBug: this._pdfBug, + pageColors, }); (intentState.renderTasks ||= new Set()).add(internalRenderTask); @@ -3219,6 +3223,7 @@ class InternalRenderTask { canvasFactory, useRequestAnimationFrame = false, pdfBug = false, + pageColors = null, }) { this.callback = callback; this.params = params; @@ -3230,6 +3235,7 @@ class InternalRenderTask { this._pageIndex = pageIndex; this.canvasFactory = canvasFactory; this._pdfBug = pdfBug; + this.pageColors = pageColors; this.running = false; this.graphicsReadyCallback = null; @@ -3284,7 +3290,8 @@ class InternalRenderTask { this.canvasFactory, imageLayer, optionalContentConfig, - this.annotationCanvasMap + this.annotationCanvasMap, + this.pageColors ); this.gfx.beginDrawing({ transform, diff --git a/src/display/canvas.js b/src/display/canvas.js index c56dff19b18685..1275b4572f8f2a 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -1042,9 +1042,8 @@ function copyCtxState(sourceCtx, destCtx) { } } -function resetCtxToDefault(ctx) { - ctx.strokeStyle = "#000000"; - ctx.fillStyle = "#000000"; +function resetCtxToDefault(ctx, foregroundColor) { + ctx.strokeStyle = ctx.fillStyle = foregroundColor || "#000000"; ctx.fillRule = "nonzero"; ctx.globalAlpha = 1; ctx.lineWidth = 1; @@ -1212,7 +1211,8 @@ class CanvasGraphics { canvasFactory, imageLayer, optionalContentConfig, - annotationCanvasMap + annotationCanvasMap, + pageColors ) { this.ctx = canvasCtx; this.current = new CanvasExtraState( @@ -1248,6 +1248,8 @@ class CanvasGraphics { this.viewportScale = 1; this.outputScaleX = 1; this.outputScaleY = 1; + this.foregroundColor = pageColors?.foreground || null; + this.backgroundColor = pageColors?.background || null; if (canvasCtx) { // NOTE: if mozCurrentTransform is polyfilled, then the current state of // the transformation must already be set in canvasCtx._transformMatrix. @@ -1280,9 +1282,47 @@ class CanvasGraphics { // transparent canvas when we have blend modes. const width = this.ctx.canvas.width; const height = this.ctx.canvas.height; - + this.defaultBackgroundColor = background || "#ffffff"; this.ctx.save(); - this.ctx.fillStyle = background || "rgb(255, 255, 255)"; + + if (this.foregroundColor && this.backgroundColor) { + this.ctx.fillStyle = this.foregroundColor; + const fg = (this.foregroundColor = this.ctx.fillStyle); + this.ctx.fillStyle = this.backgroundColor; + const bg = (this.backgroundColor = this.ctx.fillStyle); + + if (fg === "#000000" && bg === "#ffffff") { + this.foregroundColor = this.backgroundColor = null; + } else { + // https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_Colors_and_Luminance + // + // Relative luminance: + // https://www.w3.org/TR/WCAG20/#relativeluminancedef + // + // We compute the rounded luminance of the default background color. + // Then for every color in the pdf, if its rounded luminance is the + // same as the background one then it's replaced by the new + // background color else by the foreground one. + const cB = parseInt(this.defaultBackgroundColor.slice(1), 16); + const rB = (cB && 0xff0000) >> 16; + const gB = (cB && 0x00ff00) >> 8; + const bB = cB && 0x0000ff; + const newComp = x => { + x /= 255; + return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; + }; + const lB = Math.round( + 0.2126 * newComp(rB) + 0.7152 * newComp(gB) + 0.0722 * newComp(bB) + ); + this.selectColor = (r, g, b) => { + const lC = + 0.2126 * newComp(r) + 0.7152 * newComp(g) + 0.0722 * newComp(b); + return Math.round(lC) === lB ? bg : fg; + }; + } + } + + this.ctx.fillStyle = this.backgroundColor || this.defaultBackgroundColor; this.ctx.fillRect(0, 0, width, height); this.ctx.restore(); @@ -1303,7 +1343,7 @@ class CanvasGraphics { } this.ctx.save(); - resetCtxToDefault(this.ctx); + resetCtxToDefault(this.ctx, this.foregroundColor); if (transform) { this.ctx.transform.apply(this.ctx, transform); this.outputScaleX = transform[0]; @@ -2636,13 +2676,13 @@ class CanvasGraphics { } setStrokeRGBColor(r, g, b) { - const color = Util.makeHexColor(r, g, b); + const color = this.selectColor?.(r, g, b) || Util.makeHexColor(r, g, b); this.ctx.strokeStyle = color; this.current.strokeColor = color; } setFillRGBColor(r, g, b) { - const color = Util.makeHexColor(r, g, b); + const color = this.selectColor?.(r, g, b) || Util.makeHexColor(r, g, b); this.ctx.fillStyle = color; this.current.fillColor = color; this.current.patternFill = false; @@ -2964,9 +3004,9 @@ class CanvasGraphics { this.ctx.setTransform(scaleX, 0, 0, -scaleY, 0, height * scaleY); addContextCurrentTransform(this.ctx); - resetCtxToDefault(this.ctx); + resetCtxToDefault(this.ctx, this.foregroundColor); } else { - resetCtxToDefault(this.ctx); + resetCtxToDefault(this.ctx, this.foregroundColor); this.ctx.rect(rect[0], rect[1], width, height); this.ctx.clip(); diff --git a/src/pdf.js b/src/pdf.js index 7c38367d120055..45c44014d235b6 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -25,6 +25,7 @@ import { CMapCompressionType, createPromiseCapability, createValidAbsoluteUrl, + FeatureTest, InvalidPDFException, MissingPDFException, OPS, @@ -110,6 +111,7 @@ export { CMapCompressionType, createPromiseCapability, createValidAbsoluteUrl, + FeatureTest, getDocument, getFilenameFromUrl, getPdfFilenameFromUrl, diff --git a/src/shared/util.js b/src/shared/util.js index 5e5529e4150e0d..d4d961bed99905 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -709,6 +709,17 @@ class FeatureTest { typeof OffscreenCanvas !== "undefined" ); } + + static get isCSSColorCanvasTextSupported() { + return shadow( + this, + "isCSSColorCanvasTextSupported", + typeof CSS !== "undefined" && + typeof CSS.supports === "function" && + CSS.supports("color", "CanvasText") && + CSS.supports("color", "Canvas") + ); + } } const hexNumbers = [...Array(256).keys()].map(n => diff --git a/test/driver.js b/test/driver.js index 50a6110c1c54b5..904edb975d0b1a 100644 --- a/test/driver.js +++ b/test/driver.js @@ -648,7 +648,9 @@ class Driver { renderForms = false, renderPrint = false, renderXfa = false, - annotationCanvasMap = null; + annotationCanvasMap = null, + pageForegroundColor = null, + pageBackgroundColor = null; if (task.annotationStorage) { const entries = Object.entries(task.annotationStorage), @@ -699,6 +701,8 @@ class Driver { renderForms = !!task.forms; renderPrint = !!task.print; renderXfa = !!task.enableXfa; + pageForegroundColor = task.pageForegroundColor || null; + pageBackgroundColor = task.pageBackgroundColor || null; // Render the annotation layer if necessary. if (renderAnnotations || renderForms || renderXfa) { @@ -746,6 +750,10 @@ class Driver { viewport, optionalContentConfigPromise: task.optionalContentConfigPromise, annotationCanvasMap, + pageColors: { + foreground: pageForegroundColor, + background: pageBackgroundColor, + }, transform, }; if (renderForms) { diff --git a/test/test_manifest.json b/test/test_manifest.json index 10de596d10f715..443dc23b7ea5fd 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -60,6 +60,14 @@ "enhance": true, "type": "text" }, + { "id": "tracemonkey-forced-colors-eq", + "file": "pdfs/tracemonkey.pdf", + "md5": "9a192d8b1a7dc652a19835f6f08098bd", + "rounds": 1, + "pageForegroundColor": "#00FF00", + "pageBackgroundColor": "black", + "type": "eq" + }, { "id": "issue3925", "file": "pdfs/issue3925.pdf", "md5": "c5c895deecf7a7565393587e0d61be2b", diff --git a/web/app.js b/web/app.js index a34449fe81f01c..56d03558b6b95f 100644 --- a/web/app.js +++ b/web/app.js @@ -525,6 +525,10 @@ const PDFViewerApplication = { useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"), enablePermissions: AppOptions.get("enablePermissions"), + pageColors: { + foreground: AppOptions.get("pageForegroundColor"), + background: AppOptions.get("pageBackgroundColor"), + }, }); pdfRenderingQueue.setViewer(this.pdfViewer); pdfLinkService.setViewer(this.pdfViewer); diff --git a/web/app_options.js b/web/app_options.js index dfb83a01aa7b99..2667b4f124d718 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -179,6 +179,16 @@ const defaultOptions = { value: 0, kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + pageForegroundColor: { + /** @type {string} */ + value: "CanvasText", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, + pageBackgroundColor: { + /** @type {string} */ + value: "Canvas", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, cMapPacked: { /** @type {boolean} */ diff --git a/web/base_viewer.js b/web/base_viewer.js index 7db0d193fa544a..6d8a50024dabde 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -32,6 +32,7 @@ import { AnnotationMode, createPromiseCapability, + FeatureTest, PermissionFlag, PixelsPerInch, version, @@ -116,6 +117,8 @@ const PagesCountLimit = { * @property {IL10n} l10n - Localization service. * @property {boolean} [enablePermissions] - Enables PDF document permissions, * when they exist. The default value is `false`. + * @property {Object} [pageColors] - Overwrites background and foreground colors + * with user defined ones. */ class PDFPageViewBuffer { @@ -262,6 +265,18 @@ class BaseViewer { this.maxCanvasPixels = options.maxCanvasPixels; this.l10n = options.l10n || NullL10n; this.#enablePermissions = options.enablePermissions || false; + this.pageColors = options.pageColors || null; + + if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { + if ( + options.pageColors && + options.pageColors.foreground === "CanvasText" && + options.pageColors.background === "Canvas" && + !FeatureTest.isCSSColorCanvasTextSupported + ) { + this.pageColors = null; + } + } this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { @@ -698,6 +713,7 @@ class BaseViewer { renderer: this.renderer, useOnlyCssZoom: this.useOnlyCssZoom, maxCanvasPixels: this.maxCanvasPixels, + pageColors: this.pageColors, l10n: this.l10n, }); this._pages.push(pageView); diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index e769077f5eed25..0820a81beb3931 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -82,6 +82,8 @@ import { NullL10n } from "./l10n_utils.js"; * @property {number} [maxCanvasPixels] - The maximum supported canvas size in * total pixels, i.e. width * height. Use -1 for no limit. The default value * is 4096 * 4096 (16 mega-pixels). + * @property {Object} [pageColors] - Overwrites background and foreground colors + * with user defined ones. * @property {IL10n} l10n - Localization service. */ @@ -118,6 +120,7 @@ class PDFPageView { this.imageResourcesPath = options.imageResourcesPath || ""; this.useOnlyCssZoom = options.useOnlyCssZoom || false; this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS; + this.pageColors = options.pageColors || null; this.eventBus = options.eventBus; this.renderingQueue = options.renderingQueue; @@ -832,6 +835,7 @@ class PDFPageView { annotationMode: this.#annotationMode, optionalContentConfigPromise: this._optionalContentConfigPromise, annotationCanvasMap: this._annotationCanvasMap, + pageColors: this.pageColors, }; const renderTask = this.pdfPage.render(renderContext); renderTask.onContinue = function (cont) {