Skip to content

Commit b4421b0

Browse files
committed
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
1 parent d5cad9a commit b4421b0

File tree

1 file changed

+71
-8
lines changed

1 file changed

+71
-8
lines changed

src/display/text_layer.js

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,80 @@ import {
5454
*/
5555
const renderTextLayer = (function renderTextLayerClosure() {
5656
const MAX_TEXT_DIVS_TO_RENDER = 100000;
57+
const DEFAULT_FONT_SIZE = 30;
58+
const DEFAULT_FONT_ASCENT = 0.8;
59+
const ascentCache = new Map();
5760

5861
const NonWhitespaceRegexp = /\S/;
5962

6063
function isAllWhitespace(str) {
6164
return !NonWhitespaceRegexp.test(str);
6265
}
6366

64-
function appendText(task, geom, styles) {
67+
function getAscent(fontFamily, ctx) {
68+
const cachedAscent = ascentCache.get(fontFamily);
69+
if (cachedAscent) {
70+
return cachedAscent;
71+
}
72+
73+
ctx.save();
74+
ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`;
75+
const metrics = ctx.measureText("");
76+
77+
// Both properties aren't available by default in Firefox.
78+
let ascent = metrics.fontBoundingBoxAscent;
79+
let descent = Math.abs(metrics.fontBoundingBoxDescent);
80+
if (ascent) {
81+
ctx.restore();
82+
const ratio = ascent / (ascent + descent);
83+
ascentCache.set(fontFamily, ratio);
84+
return ratio;
85+
}
86+
87+
// Try basic heuristic to guess ascent/descent.
88+
// Draw a g with baseline at 0,0 and then get the line
89+
// number where a pixel has non-null red component (starting
90+
// from bottom).
91+
ctx.strokeStyle = "red";
92+
ctx.clearRect(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
93+
ctx.strokeText("g", 0, 0);
94+
let pixels = ctx.getImageData(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
95+
.data;
96+
descent = 0;
97+
for (let i = pixels.length - 1 - 3; i >= 0; i -= 4) {
98+
if (pixels[i] > 0) {
99+
descent = Math.ceil(i / 4 / DEFAULT_FONT_SIZE);
100+
break;
101+
}
102+
}
103+
104+
// Draw an A with baseline at 0,DEFAULT_FONT_SIZE and then get the line
105+
// number where a pixel has non-null red component (starting
106+
// from top).
107+
ctx.clearRect(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE);
108+
ctx.strokeText("A", 0, DEFAULT_FONT_SIZE);
109+
pixels = ctx.getImageData(0, 0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE).data;
110+
ascent = 0;
111+
for (let i = 0, ii = pixels.length; i < ii; i += 4) {
112+
if (pixels[i] > 0) {
113+
ascent = DEFAULT_FONT_SIZE - Math.floor(i / 4 / DEFAULT_FONT_SIZE);
114+
break;
115+
}
116+
}
117+
118+
ctx.restore();
119+
120+
if (ascent) {
121+
const ratio = ascent / (ascent + descent);
122+
ascentCache.set(fontFamily, ratio);
123+
return ratio;
124+
}
125+
126+
ascentCache.set(fontFamily, DEFAULT_FONT_ASCENT);
127+
return DEFAULT_FONT_ASCENT;
128+
}
129+
130+
function appendText(task, geom, styles, ctx) {
65131
// Initialize all used properties to keep the caches monomorphic.
66132
const textDiv = document.createElement("span");
67133
const textDivProperties = {
@@ -90,12 +156,7 @@ const renderTextLayer = (function renderTextLayerClosure() {
90156
angle += Math.PI / 2;
91157
}
92158
const fontHeight = Math.hypot(tx[2], tx[3]);
93-
let fontAscent = fontHeight;
94-
if (style.ascent) {
95-
fontAscent = style.ascent * fontAscent;
96-
} else if (style.descent) {
97-
fontAscent = (1 + style.descent) * fontAscent;
98-
}
159+
const fontAscent = fontHeight * getAscent(style.fontFamily, ctx);
99160

100161
let left, top;
101162
if (angle === 0) {
@@ -578,7 +639,7 @@ const renderTextLayer = (function renderTextLayerClosure() {
578639
_processItems(items, styleCache) {
579640
for (let i = 0, len = items.length; i < len; i++) {
580641
this._textContentItemsStr.push(items[i].str);
581-
appendText(this, items[i], styleCache);
642+
appendText(this, items[i], styleCache, this._layoutTextCtx);
582643
}
583644
},
584645

@@ -628,6 +689,8 @@ const renderTextLayer = (function renderTextLayerClosure() {
628689

629690
// The temporary canvas is used to measure text length in the DOM.
630691
const canvas = this._document.createElement("canvas");
692+
canvas.height = canvas.width = DEFAULT_FONT_SIZE;
693+
631694
if (
632695
typeof PDFJSDev === "undefined" ||
633696
PDFJSDev.test("MOZCENTRAL || GENERIC")

0 commit comments

Comments
 (0)