Skip to content

Commit f1db713

Browse files
committed
Stabilize light-theme blending for full-page scans
1 parent b5f23b8 commit f1db713

1 file changed

Lines changed: 59 additions & 4 deletions

File tree

src/display/blender/blender.js

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,25 @@ class Blender {
200200
return colorSet.size;
201201
}
202202

203+
neutralRatio(imageData, deviation = 12, step = 16) {
204+
const { data } = imageData;
205+
let neutral = 0;
206+
let total = 0;
207+
208+
for (let i = 0; i < data.length; i += 4 * step) {
209+
const alpha = data[i + 3];
210+
if (alpha < 32) {
211+
continue;
212+
}
213+
total++;
214+
if (this.isColorNeutral(data[i], data[i + 1], data[i + 2], deviation)) {
215+
neutral++;
216+
}
217+
}
218+
219+
return total ? neutral / total : 0;
220+
}
221+
203222
averageLightness(imageData) {
204223
const { data } = imageData;
205224
let luminanceSum = 0;
@@ -309,14 +328,17 @@ class Blender {
309328
type = "invert";
310329
} else {
311330
if (entirePage) {
331+
const lightness = this.averageLightness(imageData);
312332
if (!this.fullPageImageDetected) {
313333
this.fullPageImageDetected = true;
314-
const lightness = this.averageLightness(imageData);
315334
if (this.dark && lightness >= 150) {
316335
type = "invert";
317336
this.forceInversion = true;
318337
}
319338
}
339+
if (!this.dark && lightness >= 150 && this.neutralRatio(imageData) >= 0.9) {
340+
type = "gradient";
341+
}
320342
}
321343
// This is mainly necessary for IEEE TRANSACTIONS papers because they
322344
// use formulas as images instead of glyphs or vector graphics
@@ -330,7 +352,39 @@ class Blender {
330352

331353
const data = imageData.data;
332354

333-
if (type === "replace") {
355+
if (type === "gradient") {
356+
const colorDeviation = 12;
357+
const lumaR = 0.299;
358+
const lumaG = 0.587;
359+
const lumaB = 0.114;
360+
361+
for (let i = 0; i < data.length; i += 4) {
362+
const alpha = data[i + 3];
363+
if (!alpha) {
364+
continue;
365+
}
366+
const r = data[i];
367+
const g = data[i + 1];
368+
const b = data[i + 2];
369+
if (!this.isColorNeutral(r, g, b, colorDeviation)) {
370+
continue;
371+
}
372+
373+
const brightness = (r * lumaR + g * lumaG + b * lumaB) / 255;
374+
data[i] = Math.round(bgR + (fgR - bgR) * (1 - brightness));
375+
data[i + 1] = Math.round(bgG + (fgG - bgG) * (1 - brightness));
376+
data[i + 2] = Math.round(bgB + (fgB - bgB) * (1 - brightness));
377+
}
378+
379+
offCtx.putImageData(imageData, 0, 0);
380+
if (args.length === 3) {
381+
this.origDrawImage(offCanvas, dx, dy);
382+
} else if (args.length === 5) {
383+
this.origDrawImage(offCanvas, dx, dy, dWidth, dHeight);
384+
} else {
385+
this.origDrawImage(offCanvas, 0, 0, sWidth, sHeight, dx, dy, dWidth, dHeight);
386+
}
387+
} else if (type === "replace") {
334388
const whiteThreshold = 200;
335389
const blackThreshold = 50;
336390
const colorDeviation = 1;
@@ -417,8 +471,9 @@ class Blender {
417471
this.origDrawImage(offCanvas, 0, 0, sWidth, sHeight, dx, dy, dWidth, dHeight);
418472
}
419473
} else if (type === "overlay") {
420-
// Full-page scans often cover OCR text drawn earlier in the operator
421-
// list, so they must remain opaque or the hidden text bleeds through.
474+
// Full-page scans must remain opaque, otherwise OCR/underlying content
475+
// can bleed through in themed modes. Keep partial opacity only for
476+
// smaller overlays where the theme should still influence the page.
422477
this.ctx.globalCompositeOperation = "source-over";
423478
this.ctx.globalAlpha = entirePage ? 1 : 0.8;
424479
this.origDrawImage(...args);

0 commit comments

Comments
 (0)