From b4421b076aa6eda4490b34dfbf5b3e635be49aea Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sat, 23 Jan 2021 18:21:48 +0100 Subject: [PATCH] Modifiy the way to compute baseline to have a better match between canvas and text layer - use ascent of the fallback font instead of the one from pdf to position spans - use TextMetrics.fontBoundingBoxAscent if available or - use a basic heuristic to guess ascent in drawing char on a canvas - compute ascent as a ratio of font height --- src/display/text_layer.js | 79 +++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/src/display/text_layer.js b/src/display/text_layer.js index 19e750729..6903b1804 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -54,6 +54,9 @@ import { */ const renderTextLayer = (function renderTextLayerClosure() { const MAX_TEXT_DIVS_TO_RENDER = 100000; + const DEFAULT_FONT_SIZE = 30; + const DEFAULT_FONT_ASCENT = 0.8; + const ascentCache = new Map(); const NonWhitespaceRegexp = /\S/; @@ -61,7 +64,70 @@ const renderTextLayer = (function renderTextLayerClosure() { return !NonWhitespaceRegexp.test(str); } - function appendText(task, geom, styles) { + function getAscent(fontFamily, ctx) { + const cachedAscent = ascentCache.get(fontFamily); + if (cachedAscent) { + return cachedAscent; + } + + ctx.save(); + ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`; + const metrics = ctx.measureText(""); + + // Both properties aren't available by default in Firefox. + let ascent = metrics.fontBoundingBoxAscent; + let descent = Math.abs(metrics.fontBoundingBoxDescent); + if (ascent) { + ctx.restore(); + const ratio = ascent / (ascent + descent); + ascentCache.set(fontFamily, ratio); + return ratio; + } + + // Try basic heuristic to guess ascent/descent. + // Draw a g with baseline at 0,0 and then get the line + // number where a pixel has non-null red component (starting + // from bottom). + ctx.strokeStyle = "red"; + ctx.clearRect(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE); + ctx.strokeText("g", 0, 0); + let pixels = ctx.getImageData(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE) + .data; + descent = 0; + for (let i = pixels.length - 1 - 3; i >= 0; i -= 4) { + if (pixels[i] > 0) { + descent = Math.ceil(i / 4 / DEFAULT_FONT_SIZE); + break; + } + } + + // Draw an A with baseline at 0,DEFAULT_FONT_SIZE and then get the line + // number where a pixel has non-null red component (starting + // from top). + ctx.clearRect(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE); + ctx.strokeText("A", 0, DEFAULT_FONT_SIZE); + pixels = ctx.getImageData(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE).data; + ascent = 0; + for (let i = 0, ii = pixels.length; i < ii; i += 4) { + if (pixels[i] > 0) { + ascent = DEFAULT_FONT_SIZE - Math.floor(i / 4 / DEFAULT_FONT_SIZE); + break; + } + } + + ctx.restore(); + + if (ascent) { + const ratio = ascent / (ascent + descent); + ascentCache.set(fontFamily, ratio); + return ratio; + } + + ascentCache.set(fontFamily, DEFAULT_FONT_ASCENT); + return DEFAULT_FONT_ASCENT; + } + + function appendText(task, geom, styles, ctx) { // Initialize all used properties to keep the caches monomorphic. const textDiv = document.createElement("span"); const textDivProperties = { @@ -90,12 +156,7 @@ const renderTextLayer = (function renderTextLayerClosure() { angle += Math.PI / 2; } const fontHeight = Math.hypot(tx[2], tx[3]); - let fontAscent = fontHeight; - if (style.ascent) { - fontAscent = style.ascent * fontAscent; - } else if (style.descent) { - fontAscent = (1 + style.descent) * fontAscent; - } + const fontAscent = fontHeight * getAscent(style.fontFamily, ctx); let left, top; if (angle === 0) { @@ -578,7 +639,7 @@ const renderTextLayer = (function renderTextLayerClosure() { _processItems(items, styleCache) { for (let i = 0, len = items.length; i < len; i++) { this._textContentItemsStr.push(items[i].str); - appendText(this, items[i], styleCache); + appendText(this, items[i], styleCache, this._layoutTextCtx); } }, @@ -628,6 +689,8 @@ const renderTextLayer = (function renderTextLayerClosure() { // The temporary canvas is used to measure text length in the DOM. const canvas = this._document.createElement("canvas"); + canvas.height = canvas.width = DEFAULT_FONT_SIZE; + if ( typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL || GENERIC")