@@ -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