Skip to content

Commit f233fa6

Browse files
committed
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.
1 parent 8135d7c commit f233fa6

File tree

9 files changed

+108
-13
lines changed

9 files changed

+108
-13
lines changed

extensions/chromium/preferences_schema.json

+12
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,18 @@
208208
2
209209
],
210210
"default": -1
211+
},
212+
"pageForegroundColor": {
213+
"title": "Color to use as a foreground color in constrat mode context",
214+
"description": "The color is a string as defined in CSS",
215+
"type": "string",
216+
"default": "CanvasText"
217+
},
218+
"pageBackgroundColor": {
219+
"title": "Color to use as a background color in constrat mode context",
220+
"description": "The color is a string as defined in CSS",
221+
"type": "string",
222+
"default": "Canvas"
211223
}
212224
}
213225
}

src/display/api.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,7 @@ class PDFPageProxy {
13931393
background = null,
13941394
optionalContentConfigPromise = null,
13951395
annotationCanvasMap = null,
1396+
pageColors = null,
13961397
}) {
13971398
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
13981399
if (arguments[0]?.renderInteractiveForms !== undefined) {
@@ -1516,6 +1517,7 @@ class PDFPageProxy {
15161517
canvasFactory: canvasFactoryInstance,
15171518
useRequestAnimationFrame: !intentPrint,
15181519
pdfBug: this._pdfBug,
1520+
pageColors,
15191521
});
15201522

15211523
(intentState.renderTasks ||= new Set()).add(internalRenderTask);
@@ -3219,6 +3221,7 @@ class InternalRenderTask {
32193221
canvasFactory,
32203222
useRequestAnimationFrame = false,
32213223
pdfBug = false,
3224+
pageColors = null,
32223225
}) {
32233226
this.callback = callback;
32243227
this.params = params;
@@ -3230,6 +3233,7 @@ class InternalRenderTask {
32303233
this._pageIndex = pageIndex;
32313234
this.canvasFactory = canvasFactory;
32323235
this._pdfBug = pdfBug;
3236+
this.pageColors = pageColors;
32333237

32343238
this.running = false;
32353239
this.graphicsReadyCallback = null;
@@ -3284,7 +3288,8 @@ class InternalRenderTask {
32843288
this.canvasFactory,
32853289
imageLayer,
32863290
optionalContentConfig,
3287-
this.annotationCanvasMap
3291+
this.annotationCanvasMap,
3292+
this.pageColors
32883293
);
32893294
this.gfx.beginDrawing({
32903295
transform,

src/display/canvas.js

+51-11
Original file line numberDiff line numberDiff line change
@@ -1042,9 +1042,8 @@ function copyCtxState(sourceCtx, destCtx) {
10421042
}
10431043
}
10441044

1045-
function resetCtxToDefault(ctx) {
1046-
ctx.strokeStyle = "#000000";
1047-
ctx.fillStyle = "#000000";
1045+
function resetCtxToDefault(ctx, foregroundColor) {
1046+
ctx.strokeStyle = ctx.fillStyle = foregroundColor || "#000000";
10481047
ctx.fillRule = "nonzero";
10491048
ctx.globalAlpha = 1;
10501049
ctx.lineWidth = 1;
@@ -1212,7 +1211,8 @@ class CanvasGraphics {
12121211
canvasFactory,
12131212
imageLayer,
12141213
optionalContentConfig,
1215-
annotationCanvasMap
1214+
annotationCanvasMap,
1215+
pageColors
12161216
) {
12171217
this.ctx = canvasCtx;
12181218
this.current = new CanvasExtraState(
@@ -1248,6 +1248,8 @@ class CanvasGraphics {
12481248
this.viewportScale = 1;
12491249
this.outputScaleX = 1;
12501250
this.outputScaleY = 1;
1251+
this.foregroundColor = pageColors?.foreground || null;
1252+
this.backgroundColor = pageColors?.background || null;
12511253
if (canvasCtx) {
12521254
// NOTE: if mozCurrentTransform is polyfilled, then the current state of
12531255
// the transformation must already be set in canvasCtx._transformMatrix.
@@ -1280,9 +1282,47 @@ class CanvasGraphics {
12801282
// transparent canvas when we have blend modes.
12811283
const width = this.ctx.canvas.width;
12821284
const height = this.ctx.canvas.height;
1283-
1285+
this.defaultBackgroundColor = background || "#ffffff";
12841286
this.ctx.save();
1285-
this.ctx.fillStyle = background || "rgb(255, 255, 255)";
1287+
1288+
if (this.foregroundColor && this.backgroundColor) {
1289+
this.ctx.fillStyle = this.foregroundColor;
1290+
const fg = (this.foregroundColor = this.ctx.fillStyle);
1291+
this.ctx.fillStyle = this.backgroundColor;
1292+
const bg = (this.backgroundColor = this.ctx.fillStyle);
1293+
1294+
if (fg === "#000000" && bg === "#ffffff") {
1295+
this.foregroundColor = this.backgroundColor = null;
1296+
} else {
1297+
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_Colors_and_Luminance
1298+
//
1299+
// Relative luminance:
1300+
// https://www.w3.org/TR/WCAG20/#relativeluminancedef
1301+
//
1302+
// We compute the rounded luminance of the default background color.
1303+
// Then for every color in the pdf, if its rounded luminance is the
1304+
// same as the background one then it's replaced by the new
1305+
// background color else by the foreground one.
1306+
const cB = parseInt(this.defaultBackgroundColor.slice(1), 16);
1307+
const rB = (cB && 0xff0000) >> 16;
1308+
const gB = (cB && 0x00ff00) >> 8;
1309+
const bB = cB && 0x0000ff;
1310+
const newComp = x => {
1311+
x /= 255;
1312+
return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
1313+
};
1314+
const lB = Math.round(
1315+
0.2126 * newComp(rB) + 0.7152 * newComp(gB) + 0.0722 * newComp(bB)
1316+
);
1317+
this.selectColor = (r, g, b) => {
1318+
const lC =
1319+
0.2126 * newComp(r) + 0.7152 * newComp(g) + 0.0722 * newComp(b);
1320+
return Math.round(lC) === lB ? bg : fg;
1321+
};
1322+
}
1323+
}
1324+
1325+
this.ctx.fillStyle = this.backgroundColor || this.defaultBackgroundColor;
12861326
this.ctx.fillRect(0, 0, width, height);
12871327
this.ctx.restore();
12881328

@@ -1303,7 +1343,7 @@ class CanvasGraphics {
13031343
}
13041344

13051345
this.ctx.save();
1306-
resetCtxToDefault(this.ctx);
1346+
resetCtxToDefault(this.ctx, this.foregroundColor);
13071347
if (transform) {
13081348
this.ctx.transform.apply(this.ctx, transform);
13091349
this.outputScaleX = transform[0];
@@ -2636,13 +2676,13 @@ class CanvasGraphics {
26362676
}
26372677

26382678
setStrokeRGBColor(r, g, b) {
2639-
const color = Util.makeHexColor(r, g, b);
2679+
const color = this.selectColor?.(r, g, b) || Util.makeHexColor(r, g, b);
26402680
this.ctx.strokeStyle = color;
26412681
this.current.strokeColor = color;
26422682
}
26432683

26442684
setFillRGBColor(r, g, b) {
2645-
const color = Util.makeHexColor(r, g, b);
2685+
const color = this.selectColor?.(r, g, b) || Util.makeHexColor(r, g, b);
26462686
this.ctx.fillStyle = color;
26472687
this.current.fillColor = color;
26482688
this.current.patternFill = false;
@@ -2964,9 +3004,9 @@ class CanvasGraphics {
29643004
this.ctx.setTransform(scaleX, 0, 0, -scaleY, 0, height * scaleY);
29653005
addContextCurrentTransform(this.ctx);
29663006

2967-
resetCtxToDefault(this.ctx);
3007+
resetCtxToDefault(this.ctx, this.foregroundColor);
29683008
} else {
2969-
resetCtxToDefault(this.ctx);
3009+
resetCtxToDefault(this.ctx, this.foregroundColor);
29703010

29713011
this.ctx.rect(rect[0], rect[1], width, height);
29723012
this.ctx.clip();

test/driver.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,9 @@ class Driver {
648648
renderForms = false,
649649
renderPrint = false,
650650
renderXfa = false,
651-
annotationCanvasMap = null;
651+
annotationCanvasMap = null,
652+
pageForegroundColor = null,
653+
pageBackgroundColor = null;
652654

653655
if (task.annotationStorage) {
654656
const entries = Object.entries(task.annotationStorage),
@@ -699,6 +701,8 @@ class Driver {
699701
renderForms = !!task.forms;
700702
renderPrint = !!task.print;
701703
renderXfa = !!task.enableXfa;
704+
pageForegroundColor = task.pageForegroundColor || null;
705+
pageBackgroundColor = task.pageBackgroundColor || null;
702706

703707
// Render the annotation layer if necessary.
704708
if (renderAnnotations || renderForms || renderXfa) {
@@ -746,6 +750,10 @@ class Driver {
746750
viewport,
747751
optionalContentConfigPromise: task.optionalContentConfigPromise,
748752
annotationCanvasMap,
753+
pageColors: {
754+
foreground: pageForegroundColor,
755+
background: pageBackgroundColor,
756+
},
749757
transform,
750758
};
751759
if (renderForms) {

test/test_manifest.json

+8
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@
6060
"enhance": true,
6161
"type": "text"
6262
},
63+
{ "id": "tracemonkey-forced-colors-eq",
64+
"file": "pdfs/tracemonkey.pdf",
65+
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
66+
"rounds": 1,
67+
"pageForegroundColor": "#00FF00",
68+
"pageBackgroundColor": "black",
69+
"type": "eq"
70+
},
6371
{ "id": "issue3925",
6472
"file": "pdfs/issue3925.pdf",
6573
"md5": "c5c895deecf7a7565393587e0d61be2b",

web/app.js

+4
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,10 @@ const PDFViewerApplication = {
525525
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
526526
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
527527
enablePermissions: AppOptions.get("enablePermissions"),
528+
pageColors: {
529+
foreground: AppOptions.get("pageForegroundColor"),
530+
background: AppOptions.get("pageBackgroundColor"),
531+
},
528532
});
529533
pdfRenderingQueue.setViewer(this.pdfViewer);
530534
pdfLinkService.setViewer(this.pdfViewer);

web/app_options.js

+10
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,16 @@ const defaultOptions = {
179179
value: 0,
180180
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
181181
},
182+
pageForegroundColor: {
183+
/** @type {string} */
184+
value: "CanvasText",
185+
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
186+
},
187+
pageBackgroundColor: {
188+
/** @type {string} */
189+
value: "Canvas",
190+
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
191+
},
182192

183193
cMapPacked: {
184194
/** @type {boolean} */

web/base_viewer.js

+4
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ const PagesCountLimit = {
116116
* @property {IL10n} l10n - Localization service.
117117
* @property {boolean} [enablePermissions] - Enables PDF document permissions,
118118
* when they exist. The default value is `false`.
119+
* @property {Object} [pageColors] - Overwrites background and foreground colors
120+
* with user defined ones.
119121
*/
120122

121123
class PDFPageViewBuffer {
@@ -262,6 +264,7 @@ class BaseViewer {
262264
this.maxCanvasPixels = options.maxCanvasPixels;
263265
this.l10n = options.l10n || NullL10n;
264266
this.#enablePermissions = options.enablePermissions || false;
267+
this.pageColors = options.pageColors || null;
265268

266269
this.defaultRenderingQueue = !options.renderingQueue;
267270
if (this.defaultRenderingQueue) {
@@ -698,6 +701,7 @@ class BaseViewer {
698701
renderer: this.renderer,
699702
useOnlyCssZoom: this.useOnlyCssZoom,
700703
maxCanvasPixels: this.maxCanvasPixels,
704+
pageColors: this.pageColors,
701705
l10n: this.l10n,
702706
});
703707
this._pages.push(pageView);

web/pdf_page_view.js

+4
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ import { NullL10n } from "./l10n_utils.js";
8282
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
8383
* total pixels, i.e. width * height. Use -1 for no limit. The default value
8484
* is 4096 * 4096 (16 mega-pixels).
85+
* @property {Object} [pageColors] - Overwrites background and foreground colors
86+
* with user defined ones.
8587
* @property {IL10n} l10n - Localization service.
8688
*/
8789

@@ -118,6 +120,7 @@ class PDFPageView {
118120
this.imageResourcesPath = options.imageResourcesPath || "";
119121
this.useOnlyCssZoom = options.useOnlyCssZoom || false;
120122
this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS;
123+
this.pageColors = options.pageColors || null;
121124

122125
this.eventBus = options.eventBus;
123126
this.renderingQueue = options.renderingQueue;
@@ -832,6 +835,7 @@ class PDFPageView {
832835
annotationMode: this.#annotationMode,
833836
optionalContentConfigPromise: this._optionalContentConfigPromise,
834837
annotationCanvasMap: this._annotationCanvasMap,
838+
pageColors: this.pageColors,
835839
};
836840
const renderTask = this.pdfPage.render(renderContext);
837841
renderTask.onContinue = function (cont) {

0 commit comments

Comments
 (0)