1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-28 23:28:16 +02:00

XFA - Fix text positions (bug 1718741)

- font line height is taken into account by acrobat when it isn't with masterpdfeditor: I extracted a font from a pdf, modified some ascent/descent properties thanks to ttx and the reinjected the font in the pdf: only Acrobat is taken it into account. So in this patch, line heights for some substituted fonts are added.
  - it seems that Acrobat is using a line height of 1.2 when the line height in the font is not enough (it's the only way I found to fix correctly bug 1718741).
   - don't use flex in wrapper container (which was causing an horizontal overflow in the above bug).
   - consequently, the above fixes introduced a lot of small regressions, so in order to see real improvements on reftests, I fixed the regressions in this patch:
     - replace margin by padding in some case where padding is a part of a container dimensions;
     - remove some flex display: some containers are wrongly sized when rendered;
     - set letter-spacing to 0.01px: it helps to be sure that text is not broken because of not enough width in Firefox.
This commit is contained in:
Calixte Denizet 2021-07-09 17:29:21 +02:00
parent 0afc785c7d
commit 58e1f51688
15 changed files with 220 additions and 103 deletions

View file

@ -396,6 +396,11 @@ function toStyle(node, ...names) {
if (value === null) {
continue;
}
if (converters.hasOwnProperty(name)) {
converters[name](node, style);
continue;
}
if (value instanceof XFAObject) {
const newStyle = value[$toStyle]();
if (newStyle) {
@ -403,11 +408,6 @@ function toStyle(node, ...names) {
} else {
warn(`(DEBUG) - XFA - style for ${name} not implemented yet`);
}
continue;
}
if (converters.hasOwnProperty(name)) {
converters[name](node, style);
}
}
return style;
@ -560,6 +560,44 @@ function isPrintOnly(node) {
);
}
function setPara(node, nodeStyle, value) {
if (value.attributes.class && value.attributes.class.includes("xfaRich")) {
if (nodeStyle) {
if (node.h === "") {
nodeStyle.height = "auto";
}
if (node.w === "") {
nodeStyle.width = "auto";
}
}
if (node.para) {
// By definition exData are external data so para
// has no effect on it.
const valueStyle = value.attributes.style;
valueStyle.display = "flex";
valueStyle.flexDirection = "column";
switch (node.para.vAlign) {
case "top":
valueStyle.justifyContent = "start";
break;
case "bottom":
valueStyle.justifyContent = "end";
break;
case "middle":
valueStyle.justifyContent = "center";
break;
}
const paraStyle = node.para[$toStyle]();
for (const [key, val] of Object.entries(paraStyle)) {
if (!(key in valueStyle)) {
valueStyle[key] = val;
}
}
}
}
}
function setFontFamily(xfaFont, fontFinder, style) {
const name = stripQuotes(xfaFont.typeface);
const typeface = fontFinder.find(name);
@ -574,9 +612,12 @@ function setFontFamily(xfaFont, fontFinder, style) {
// Already something so don't overwrite.
return;
}
const pdfFont = selectFont(xfaFont, typeface);
if (pdfFont && pdfFont.lineHeight > 0) {
style.lineHeight = pdfFont.lineHeight;
style.lineHeight = Math.max(1.2, pdfFont.lineHeight);
} else {
style.lineHeight = 1.2;
}
}
}
@ -593,5 +634,6 @@ export {
setAccess,
setFontFamily,
setMinMaxDimensions,
setPara,
toStyle,
};

View file

@ -80,6 +80,7 @@ import {
setAccess,
setFontFamily,
setMinMaxDimensions,
setPara,
toStyle,
} from "./html_utils.js";
import {
@ -134,12 +135,20 @@ function* getContainedChildren(node) {
function valueToHtml(value) {
return HTMLResult.success({
name: "span",
name: "div",
attributes: {
class: ["xfaRich"],
style: Object.create(null),
},
value,
children: [
{
name: "span",
attributes: {
style: Object.create(null),
},
value,
},
],
});
}
@ -225,6 +234,15 @@ function handleBreak(node) {
return true;
}
function handleOverflow(node, extraNode, space) {
const root = node[$getTemplateRoot]();
const saved = root[$extra].noLayoutFailure;
root[$extra].noLayoutFailure = true;
const res = extraNode[$toHTML](space);
node[$addHTML](res.html, res.bbox);
root[$extra].noLayoutFailure = saved;
}
class AppearanceFilter extends StringObject {
constructor(attributes) {
super(TEMPLATE_NS_ID, "appearanceFilter");
@ -973,7 +991,7 @@ class Caption extends XFAObject {
children.push(value);
}
const style = toStyle(this, "font", "margin", "para", "visibility");
const style = toStyle(this, "font", "margin", "visibility");
switch (this.placement) {
case "left":
case "right":
@ -989,6 +1007,8 @@ class Caption extends XFAObject {
break;
}
setPara(this, null, value);
this.reserve = savedReserve;
return HTMLResult.success({
@ -1654,6 +1674,11 @@ class Draw extends XFAObject {
setMinMaxDimensions(this, style);
if (style.margin) {
style.padding = style.margin;
delete style.margin;
}
const classNames = ["xfaDraw"];
if (this.font) {
classNames.push("xfaFont");
@ -1688,42 +1713,7 @@ class Draw extends XFAObject {
}
html.children.push(value);
if (value.attributes.class && value.attributes.class.includes("xfaRich")) {
if (this.h === "") {
style.height = "auto";
}
if (this.w === "") {
style.width = "auto";
}
if (this.para) {
// By definition exData are external data so para
// has no effect on it.
attributes.style.display = "flex";
attributes.style.flexDirection = "column";
switch (this.para.vAlign) {
case "top":
attributes.style.justifyContent = "start";
break;
case "bottom":
attributes.style.justifyContent = "end";
break;
case "middle":
attributes.style.justifyContent = "center";
break;
}
const paraStyle = this.para[$toStyle]();
if (!value.attributes.style) {
value.attributes.style = paraStyle;
} else {
for (const [key, val] of Object.entries(paraStyle)) {
if (!(key in value.attributes.style)) {
value.attributes.style[key] = val;
}
}
}
}
}
setPara(this, style, value);
this.w = savedW;
this.h = savedH;
@ -2510,20 +2500,21 @@ class Field extends XFAObject {
width = w;
height = h;
if (this.ui instanceof CheckButton) {
if (this.ui.checkButton) {
switch (this.caption.placement) {
case "left":
case "right":
case "inline":
width += this.ui.size;
width += this.ui.checkButton.size;
break;
case "top":
case "bottom":
height += this.ui.size;
height += this.ui.checkButton.size;
break;
}
}
}
if (width && this.w === "") {
this.w = Math.min(
this.maxW <= 0 ? Infinity : this.maxW,
@ -2579,6 +2570,11 @@ class Field extends XFAObject {
class: classNames,
};
if (style.margin) {
style.padding = style.margin;
delete style.margin;
}
setAccess(this, classNames);
if (this.name) {
@ -2623,7 +2619,7 @@ class Field extends XFAObject {
} else {
const htmlValue = this.value[$toHTML]().html;
if (htmlValue !== null) {
value = htmlValue.value;
value = htmlValue.children[0].value;
}
}
if (this.ui.textEdit && this.value.text && this.value.text.maxChars) {
@ -2652,6 +2648,9 @@ class Field extends XFAObject {
}
if (this.ui.button) {
if (style.padding) {
delete style.padding;
}
if (caption.name === "div") {
caption.name = "span";
}
@ -2910,11 +2909,7 @@ class Font extends XFAObject {
// TODO: overlinePeriod
style.fontStyle = this.posture;
const fontSize = measureToString(0.99 * this.size);
if (fontSize !== "10px") {
style.fontSize = fontSize;
}
style.fontSize = measureToString(0.99 * this.size);
setFontFamily(this, this[$globalData].fontFinder, style);
@ -3513,8 +3508,14 @@ class Overflow extends XFAObject {
const parent = this[$getParent]();
const root = this[$getTemplateRoot]();
const target = root[$searchNode](this.target, parent);
const leader = root[$searchNode](this.leader, parent);
const trailer = root[$searchNode](this.trailer, parent);
this[$extra] = {
target: (target && target[0]) || null,
leader: (leader && leader[0]) || null,
trailer: (trailer && trailer[0]) || null,
addLeader: false,
addTrailer: false,
};
}
return this[$extra];
@ -3834,16 +3835,16 @@ class Para extends XFAObject {
[$toStyle]() {
const style = toStyle(this, "hAlign");
if (this.marginLeft !== "") {
style.marginLeft = measureToString(this.marginLeft);
style.paddingLeft = measureToString(this.marginLeft);
}
if (this.marginRight !== "") {
style.marginRight = measureToString(this.marginRight);
style.paddingight = measureToString(this.marginRight);
}
if (this.spaceAbove !== "") {
style.marginTop = measureToString(this.spaceAbove);
style.paddingTop = measureToString(this.spaceAbove);
}
if (this.spaceBelow !== "") {
style.marginBottom = measureToString(this.spaceBelow);
style.paddingBottom = measureToString(this.spaceBelow);
}
if (this.textIndent !== "") {
style.textIndent = measureToString(this.textIndent);
@ -4658,6 +4659,14 @@ class Subform extends XFAObject {
attributes.xfaName = this.name;
}
if (this.overflow) {
const overflowExtra = this.overflow[$getExtra]();
if (overflowExtra.addLeader) {
overflowExtra.addLeader = false;
handleOverflow(this, overflowExtra.leader, availableSpace);
}
}
const maxRun =
this.layout === "lr-tb" || this.layout === "rl-tb"
? MAX_ATTEMPTS_FOR_LRTB_LAYOUT
@ -4699,6 +4708,14 @@ class Subform extends XFAObject {
return HTMLResult.FAILURE;
}
if (this.overflow) {
const overflowExtra = this.overflow[$getExtra]();
if (overflowExtra.addTrailer) {
overflowExtra.addTrailer = false;
handleOverflow(this, overflowExtra.trailer, availableSpace);
}
}
let marginH = 0;
let marginV = 0;
if (this.margin) {
@ -5095,24 +5112,13 @@ class Template extends XFAObject {
const node = this[$extra].overflowNode;
this[$extra].overflowNode = null;
const overflowExtra = node[$getExtra]();
const target = overflowExtra.target;
overflowExtra.addLeader = overflowExtra.leader !== null;
overflowExtra.addTrailer = overflowExtra.trailer !== null;
flush(i);
if (node.leader) {
leader = this[$searchNode](node.leader, node[$getParent]());
leader = leader ? leader[0] : null;
}
if (node.trailer) {
trailer = this[$searchNode](node.trailer, node[$getParent]());
trailer = trailer ? trailer[0] : null;
}
let target = null;
if (node.target) {
target = this[$searchNode](node.target, node[$getParent]());
target = target ? target[0] : target;
}
i = Infinity;
if (target instanceof PageArea) {
// We must stop the contentAreas filling and go to the next page.

View file

@ -15,7 +15,7 @@
import { selectFont } from "./fonts.js";
const WIDTH_FACTOR = 1.05;
const WIDTH_FACTOR = 1.01;
class FontInfo {
constructor(xfaFont, margin, lineHeight, fontFinder) {
@ -183,7 +183,7 @@ class TextMeasure {
const pdfFont = lastFont.pdfFont;
const lineHeight =
lastFont.lineHeight ||
Math.round(Math.max(1, pdfFont.lineHeight) * fontSize);
Math.ceil(Math.max(1.2, pdfFont.lineHeight) * fontSize);
const scale = fontSize / 1000;
for (const line of str.split(/[\u2029\n]/)) {

View file

@ -84,7 +84,13 @@ const StyleMapping = new Map([
],
["xfa-spacerun", ""],
["xfa-tab-stops", ""],
["font-size", value => measureToString(getMeasurement(value))],
[
"font-size",
(value, original) => {
value = original.fontSize = getMeasurement(value);
return measureToString(0.99 * value);
},
],
["letter-spacing", value => measureToString(getMeasurement(value))],
["line-height", value => measureToString(getMeasurement(value))],
["margin", value => measureToString(getMeasurement(value))],
@ -104,6 +110,7 @@ function mapStyle(styleStr, fontFinder) {
if (!styleStr) {
return style;
}
const original = Object.create(null);
for (const [key, value] of styleStr.split(";").map(s => s.split(":", 2))) {
const mapping = StyleMapping.get(key);
if (mapping === "") {
@ -114,7 +121,7 @@ function mapStyle(styleStr, fontFinder) {
if (typeof mapping === "string") {
newValue = mapping;
} else {
newValue = mapping(value, fontFinder);
newValue = mapping(value, original);
}
}
if (key.endsWith("scale")) {
@ -135,6 +142,7 @@ function mapStyle(styleStr, fontFinder) {
typeface: style.fontFamily,
weight: style.fontWeight || "normal",
posture: style.fontStyle || "normal",
size: original.fontSize || 0,
},
fontFinder,
style