Skip to content

Commit

Permalink
Improve pdf reading in high contrast mode
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
calixteman committed May 5, 2022
1 parent 8135d7c commit 0301f06
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 13 deletions.
10 changes: 10 additions & 0 deletions extensions/chromium/preferences_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@
2
],
"default": -1
},
"pageBackgroundColor": {
"description": "The color is a string as defined in CSS",
"type": "string",
"default": "Canvas"
},
"pageForegroundColor": {
"description": "The color is a string as defined in CSS",
"type": "string",
"default": "CanvasText"
}
}
}
9 changes: 8 additions & 1 deletion src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,8 @@ class PDFDocumentProxy {
* states set.
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
* annotation ids with canvases used to render them.
* @property {Object} [pageColors] - Overwrites background and foreground colors
* with user defined ones.
*/

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1516,6 +1519,7 @@ class PDFPageProxy {
canvasFactory: canvasFactoryInstance,
useRequestAnimationFrame: !intentPrint,
pdfBug: this._pdfBug,
pageColors,
});

(intentState.renderTasks ||= new Set()).add(internalRenderTask);
Expand Down Expand Up @@ -3219,6 +3223,7 @@ class InternalRenderTask {
canvasFactory,
useRequestAnimationFrame = false,
pdfBug = false,
pageColors = null,
}) {
this.callback = callback;
this.params = params;
Expand All @@ -3230,6 +3235,7 @@ class InternalRenderTask {
this._pageIndex = pageIndex;
this.canvasFactory = canvasFactory;
this._pdfBug = pdfBug;
this.pageColors = pageColors;

this.running = false;
this.graphicsReadyCallback = null;
Expand Down Expand Up @@ -3284,7 +3290,8 @@ class InternalRenderTask {
this.canvasFactory,
imageLayer,
optionalContentConfig,
this.annotationCanvasMap
this.annotationCanvasMap,
this.pageColors
);
this.gfx.beginDrawing({
transform,
Expand Down
64 changes: 53 additions & 11 deletions src/display/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1212,7 +1211,8 @@ class CanvasGraphics {
canvasFactory,
imageLayer,
optionalContentConfig,
annotationCanvasMap
annotationCanvasMap,
pageColors
) {
this.ctx = canvasCtx;
this.current = new CanvasExtraState(
Expand Down Expand Up @@ -1248,6 +1248,8 @@ class CanvasGraphics {
this.viewportScale = 1;
this.outputScaleX = 1;
this.outputScaleY = 1;
this.backgroundColor = pageColors?.background || null;
this.foregroundColor = pageColors?.foreground || null;
if (canvasCtx) {
// NOTE: if mozCurrentTransform is polyfilled, then the current state of
// the transformation must already be set in canvasCtx._transformMatrix.
Expand Down Expand Up @@ -1280,9 +1282,49 @@ 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) {
// Get the #RRGGBB value of the color. If it's a name (e.g. CanvasText)
// then it'll be converted to it's rgb value.
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 lumB = Math.round(
0.2126 * newComp(rB) + 0.7152 * newComp(gB) + 0.0722 * newComp(bB)
);
this.selectColor = (r, g, b) => {
const lumC =
0.2126 * newComp(r) + 0.7152 * newComp(g) + 0.0722 * newComp(b);
return Math.round(lumC) === lumB ? bg : fg;
};
}
}

this.ctx.fillStyle = this.backgroundColor || this.defaultBackgroundColor;
this.ctx.fillRect(0, 0, width, height);
this.ctx.restore();

Expand All @@ -1303,7 +1345,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];
Expand Down Expand Up @@ -2636,13 +2678,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;
Expand Down Expand Up @@ -2964,9 +3006,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();
Expand Down
5 changes: 4 additions & 1 deletion test/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,8 @@ class Driver {
renderForms = false,
renderPrint = false,
renderXfa = false,
annotationCanvasMap = null;
annotationCanvasMap = null,
pageColors = null;

if (task.annotationStorage) {
const entries = Object.entries(task.annotationStorage),
Expand Down Expand Up @@ -699,6 +700,7 @@ class Driver {
renderForms = !!task.forms;
renderPrint = !!task.print;
renderXfa = !!task.enableXfa;
pageColors = task.pageColors || null;

// Render the annotation layer if necessary.
if (renderAnnotations || renderForms || renderXfa) {
Expand Down Expand Up @@ -746,6 +748,7 @@ class Driver {
viewport,
optionalContentConfigPromise: task.optionalContentConfigPromise,
annotationCanvasMap,
pageColors,
transform,
};
if (renderForms) {
Expand Down
10 changes: 10 additions & 0 deletions test/test_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@
"enhance": true,
"type": "text"
},
{ "id": "tracemonkey-forced-colors-eq",
"file": "pdfs/tracemonkey.pdf",
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
"rounds": 1,
"pageColors": {
"background": "black",
"foreground": "#00FF00"
},
"type": "eq"
},
{ "id": "issue3925",
"file": "pdfs/issue3925.pdf",
"md5": "c5c895deecf7a7565393587e0d61be2b",
Expand Down
4 changes: 4 additions & 0 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,10 @@ const PDFViewerApplication = {
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
enablePermissions: AppOptions.get("enablePermissions"),
pageColors: {
background: AppOptions.get("pageBackgroundColor"),
foreground: AppOptions.get("pageForegroundColor"),
},
});
pdfRenderingQueue.setViewer(this.pdfViewer);
pdfLinkService.setViewer(this.pdfViewer);
Expand Down
10 changes: 10 additions & 0 deletions web/app_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ const defaultOptions = {
compatibility: compatibilityParams.maxCanvasPixels,
kind: OptionKind.VIEWER,
},
pageBackgroundColor: {
/** @type {string} */
value: "Canvas",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
pageForegroundColor: {
/** @type {string} */
value: "CanvasText",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
pdfBugEnabled: {
/** @type {boolean} */
value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION"),
Expand Down
19 changes: 19 additions & 0 deletions web/base_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,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 {
Expand Down Expand Up @@ -262,6 +264,22 @@ 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 &&
(!CSS.supports("color", options.pageColors.background) ||
!CSS.supports("color", options.pageColors.foreground))
) {
if (options.pageColors.background || options.pageColors.foreground) {
console.warn(
"Ignoring `pageColors`-option, since the browser doesn't support the values used."
);
}
this.pageColors = null;
}
}

this.defaultRenderingQueue = !options.renderingQueue;
if (this.defaultRenderingQueue) {
Expand Down Expand Up @@ -698,6 +716,7 @@ class BaseViewer {
renderer: this.renderer,
useOnlyCssZoom: this.useOnlyCssZoom,
maxCanvasPixels: this.maxCanvasPixels,
pageColors: this.pageColors,
l10n: this.l10n,
});
this._pages.push(pageView);
Expand Down
4 changes: 4 additions & 0 deletions web/pdf_page_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 0301f06

Please sign in to comment.