e82446fa5a
- when the CSS line-height property is set to 'normal' then the value depends of the user agent. So use a line height based on the font itself and if for any reasons this value is not available use 1.2 as default. - it's a partial fix for https://bugzilla.mozilla.org/show_bug.cgi?id=1717681.
228 lines
6.0 KiB
JavaScript
228 lines
6.0 KiB
JavaScript
/* Copyright 2021 Mozilla Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { selectFont } from "./fonts.js";
|
|
|
|
const WIDTH_FACTOR = 1.2;
|
|
const HEIGHT_FACTOR = 1.2;
|
|
|
|
class FontInfo {
|
|
constructor(xfaFont, fontFinder) {
|
|
if (!xfaFont) {
|
|
[this.pdfFont, this.xfaFont] = this.defaultFont(fontFinder);
|
|
return;
|
|
}
|
|
|
|
this.xfaFont = xfaFont;
|
|
const typeface = fontFinder.find(xfaFont.typeface);
|
|
if (!typeface) {
|
|
[this.pdfFont, this.xfaFont] = this.defaultFont(fontFinder);
|
|
return;
|
|
}
|
|
|
|
this.pdfFont = selectFont(xfaFont, typeface);
|
|
|
|
if (!this.pdfFont) {
|
|
[this.pdfFont, this.xfaFont] = this.defaultFont(fontFinder);
|
|
}
|
|
}
|
|
|
|
defaultFont(fontFinder) {
|
|
// TODO: Add a default font based on Liberation.
|
|
const font =
|
|
fontFinder.find("Helvetica", false) ||
|
|
fontFinder.find("Myriad Pro", false) ||
|
|
fontFinder.find("Arial", false) ||
|
|
fontFinder.getDefault();
|
|
if (font && font.regular) {
|
|
const pdfFont = font.regular;
|
|
const info = pdfFont.cssFontInfo;
|
|
const xfaFont = {
|
|
typeface: info.fontFamily,
|
|
posture: "normal",
|
|
weight: "normal",
|
|
size: 10,
|
|
};
|
|
return [pdfFont, xfaFont];
|
|
}
|
|
|
|
const xfaFont = {
|
|
typeface: "Courier",
|
|
posture: "normal",
|
|
weight: "normal",
|
|
size: 10,
|
|
};
|
|
return [null, xfaFont];
|
|
}
|
|
}
|
|
|
|
class FontSelector {
|
|
constructor(defaultXfaFont, fontFinder) {
|
|
this.fontFinder = fontFinder;
|
|
this.stack = [new FontInfo(defaultXfaFont, fontFinder)];
|
|
}
|
|
|
|
pushFont(xfaFont) {
|
|
const lastFont = this.stack[this.stack.length - 1];
|
|
for (const name of ["typeface", "posture", "weight", "size"]) {
|
|
if (!xfaFont[name]) {
|
|
xfaFont[name] = lastFont.xfaFont[name];
|
|
}
|
|
}
|
|
|
|
const fontInfo = new FontInfo(xfaFont, this.fontFinder);
|
|
if (!fontInfo.pdfFont) {
|
|
fontInfo.pdfFont = lastFont.pdfFont;
|
|
}
|
|
this.stack.push(fontInfo);
|
|
}
|
|
|
|
popFont() {
|
|
this.stack.pop();
|
|
}
|
|
|
|
topFont() {
|
|
return this.stack[this.stack.length - 1];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compute a text area dimensions based on font metrics.
|
|
*/
|
|
class TextMeasure {
|
|
constructor(defaultXfaFont, fonts) {
|
|
this.glyphs = [];
|
|
this.fontSelector = new FontSelector(defaultXfaFont, fonts);
|
|
}
|
|
|
|
pushFont(xfaFont) {
|
|
return this.fontSelector.pushFont(xfaFont);
|
|
}
|
|
|
|
popFont(xfaFont) {
|
|
return this.fontSelector.popFont();
|
|
}
|
|
|
|
addString(str) {
|
|
if (!str) {
|
|
return;
|
|
}
|
|
|
|
const lastFont = this.fontSelector.topFont();
|
|
const fontSize = lastFont.xfaFont.size;
|
|
if (lastFont.pdfFont) {
|
|
const pdfFont = lastFont.pdfFont;
|
|
const lineHeight = Math.round(Math.max(1, pdfFont.lineHeight) * fontSize);
|
|
const scale = fontSize / 1000;
|
|
|
|
for (const line of str.split(/[\u2029\n]/)) {
|
|
const encodedLine = pdfFont.encodeString(line).join("");
|
|
const glyphs = pdfFont.charsToGlyphs(encodedLine);
|
|
|
|
for (const glyph of glyphs) {
|
|
this.glyphs.push([
|
|
glyph.width * scale,
|
|
lineHeight,
|
|
glyph.unicode === " ",
|
|
false,
|
|
]);
|
|
}
|
|
|
|
this.glyphs.push([0, 0, false, true]);
|
|
}
|
|
this.glyphs.pop();
|
|
return;
|
|
}
|
|
|
|
// When we have no font in the pdf, just use the font size as default width.
|
|
for (const line of str.split(/[\u2029\n]/)) {
|
|
for (const char of line.split("")) {
|
|
this.glyphs.push([fontSize, fontSize, char === " ", false]);
|
|
}
|
|
|
|
this.glyphs.push([0, 0, false, true]);
|
|
}
|
|
this.glyphs.pop();
|
|
}
|
|
|
|
compute(maxWidth) {
|
|
let lastSpacePos = -1,
|
|
lastSpaceWidth = 0,
|
|
width = 0,
|
|
height = 0,
|
|
currentLineWidth = 0,
|
|
currentLineHeight = 0;
|
|
|
|
for (let i = 0, ii = this.glyphs.length; i < ii; i++) {
|
|
const [glyphWidth, glyphHeight, isSpace, isEOL] = this.glyphs[i];
|
|
if (isEOL) {
|
|
width = Math.max(width, currentLineWidth);
|
|
currentLineWidth = 0;
|
|
height += currentLineHeight;
|
|
currentLineHeight = glyphHeight;
|
|
lastSpacePos = -1;
|
|
lastSpaceWidth = 0;
|
|
continue;
|
|
}
|
|
|
|
if (isSpace) {
|
|
if (currentLineWidth + glyphWidth > maxWidth) {
|
|
// We can break here but the space is not taken into account.
|
|
width = Math.max(width, currentLineWidth);
|
|
currentLineWidth = 0;
|
|
height += currentLineHeight;
|
|
currentLineHeight = glyphHeight;
|
|
lastSpacePos = -1;
|
|
lastSpaceWidth = 0;
|
|
} else {
|
|
currentLineHeight = Math.max(glyphHeight, currentLineHeight);
|
|
lastSpaceWidth = currentLineWidth;
|
|
currentLineWidth += glyphWidth;
|
|
lastSpacePos = i;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (currentLineWidth + glyphWidth > maxWidth) {
|
|
// We must break to the last white position (if available)
|
|
height += currentLineHeight;
|
|
currentLineHeight = glyphHeight;
|
|
if (lastSpacePos !== -1) {
|
|
i = lastSpacePos;
|
|
width = Math.max(width, lastSpaceWidth);
|
|
currentLineWidth = 0;
|
|
lastSpacePos = -1;
|
|
lastSpaceWidth = 0;
|
|
} else {
|
|
// Just break in the middle of the word
|
|
width = Math.max(width, currentLineWidth);
|
|
currentLineWidth = glyphWidth;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
currentLineWidth += glyphWidth;
|
|
currentLineHeight = Math.max(glyphHeight, currentLineHeight);
|
|
}
|
|
|
|
width = Math.max(width, currentLineWidth);
|
|
height += currentLineHeight;
|
|
|
|
return { width: WIDTH_FACTOR * width, height: HEIGHT_FACTOR * height };
|
|
}
|
|
}
|
|
|
|
export { TextMeasure };
|