XFA - Add the possibily to layout and measure text
- some containers doesn't always have their 2 dimensions and those dimensions re based on contents; - so in order to measure text, we must get the glyph widths (for the xfa fonts) before starting the layout; - implement a word-wrap algorithm; - handle font change during text layout.
This commit is contained in:
parent
335d4cb2fc
commit
8eeb7ab4a3
@ -857,6 +857,10 @@ class PDFDocument {
|
|||||||
return shadow(this, "xfaFaxtory", null);
|
return shadow(this, "xfaFaxtory", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isPureXfa() {
|
||||||
|
return this.xfaFactory && this.xfaFactory.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
get htmlForXfa() {
|
get htmlForXfa() {
|
||||||
if (this.xfaFactory) {
|
if (this.xfaFactory) {
|
||||||
return this.xfaFactory.getPages();
|
return this.xfaFactory.getPages();
|
||||||
@ -898,8 +902,14 @@ class PDFDocument {
|
|||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
const operatorList = new OperatorList();
|
const operatorList = new OperatorList();
|
||||||
|
const pdfFonts = [];
|
||||||
const initialState = {
|
const initialState = {
|
||||||
font: null,
|
get font() {
|
||||||
|
return pdfFonts[pdfFonts.length - 1];
|
||||||
|
},
|
||||||
|
set font(font) {
|
||||||
|
pdfFonts.push(font);
|
||||||
|
},
|
||||||
clone() {
|
clone() {
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
@ -947,6 +957,7 @@ class PDFDocument {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
this.xfaFactory.setFonts(pdfFonts);
|
||||||
}
|
}
|
||||||
|
|
||||||
get formInfo() {
|
get formInfo() {
|
||||||
|
@ -872,6 +872,7 @@ class Font {
|
|||||||
this.capHeight = properties.capHeight / PDF_GLYPH_SPACE_UNITS;
|
this.capHeight = properties.capHeight / PDF_GLYPH_SPACE_UNITS;
|
||||||
this.ascent = properties.ascent / PDF_GLYPH_SPACE_UNITS;
|
this.ascent = properties.ascent / PDF_GLYPH_SPACE_UNITS;
|
||||||
this.descent = properties.descent / PDF_GLYPH_SPACE_UNITS;
|
this.descent = properties.descent / PDF_GLYPH_SPACE_UNITS;
|
||||||
|
this.lineHeight = this.ascent - this.descent;
|
||||||
this.fontMatrix = properties.fontMatrix;
|
this.fontMatrix = properties.fontMatrix;
|
||||||
this.bbox = properties.bbox;
|
this.bbox = properties.bbox;
|
||||||
this.defaultEncoding = properties.defaultEncoding;
|
this.defaultEncoding = properties.defaultEncoding;
|
||||||
@ -2495,13 +2496,16 @@ class Font {
|
|||||||
unitsPerEm: int16(tables.head.data[18], tables.head.data[19]),
|
unitsPerEm: int16(tables.head.data[18], tables.head.data[19]),
|
||||||
yMax: int16(tables.head.data[42], tables.head.data[43]),
|
yMax: int16(tables.head.data[42], tables.head.data[43]),
|
||||||
yMin: signedInt16(tables.head.data[38], tables.head.data[39]),
|
yMin: signedInt16(tables.head.data[38], tables.head.data[39]),
|
||||||
ascent: int16(tables.hhea.data[4], tables.hhea.data[5]),
|
ascent: signedInt16(tables.hhea.data[4], tables.hhea.data[5]),
|
||||||
descent: signedInt16(tables.hhea.data[6], tables.hhea.data[7]),
|
descent: signedInt16(tables.hhea.data[6], tables.hhea.data[7]),
|
||||||
|
lineGap: signedInt16(tables.hhea.data[8], tables.hhea.data[9]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// PDF FontDescriptor metrics lie -- using data from actual font.
|
// PDF FontDescriptor metrics lie -- using data from actual font.
|
||||||
this.ascent = metricsOverride.ascent / metricsOverride.unitsPerEm;
|
this.ascent = metricsOverride.ascent / metricsOverride.unitsPerEm;
|
||||||
this.descent = metricsOverride.descent / metricsOverride.unitsPerEm;
|
this.descent = metricsOverride.descent / metricsOverride.unitsPerEm;
|
||||||
|
this.lineGap = metricsOverride.lineGap / metricsOverride.unitsPerEm;
|
||||||
|
this.lineHeight = this.ascent - this.descent + this.lineGap;
|
||||||
|
|
||||||
// The 'post' table has glyphs names.
|
// The 'post' table has glyphs names.
|
||||||
if (tables.post) {
|
if (tables.post) {
|
||||||
|
@ -187,13 +187,8 @@ class WorkerMessageHandler {
|
|||||||
await pdfManager.ensureDoc("checkFirstPage");
|
await pdfManager.ensureDoc("checkFirstPage");
|
||||||
}
|
}
|
||||||
|
|
||||||
const [numPages, fingerprint, htmlForXfa] = await Promise.all([
|
const isPureXfa = await pdfManager.ensureDoc("isPureXfa");
|
||||||
pdfManager.ensureDoc("numPages"),
|
if (isPureXfa) {
|
||||||
pdfManager.ensureDoc("fingerprint"),
|
|
||||||
pdfManager.ensureDoc("htmlForXfa"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (htmlForXfa) {
|
|
||||||
const task = new WorkerTask("loadXfaFonts");
|
const task = new WorkerTask("loadXfaFonts");
|
||||||
startWorkerTask(task);
|
startWorkerTask(task);
|
||||||
await pdfManager
|
await pdfManager
|
||||||
@ -203,6 +198,17 @@ class WorkerMessageHandler {
|
|||||||
})
|
})
|
||||||
.then(() => finishWorkerTask(task));
|
.then(() => finishWorkerTask(task));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [numPages, fingerprint] = await Promise.all([
|
||||||
|
pdfManager.ensureDoc("numPages"),
|
||||||
|
pdfManager.ensureDoc("fingerprint"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get htmlForXfa after numPages to avoid to create HTML twice.
|
||||||
|
const htmlForXfa = isPureXfa
|
||||||
|
? await pdfManager.ensureDoc("htmlForXfa")
|
||||||
|
: null;
|
||||||
|
|
||||||
return { numPages, fingerprint, htmlForXfa };
|
return { numPages, fingerprint, htmlForXfa };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,8 +13,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { $toHTML } from "./xfa_object.js";
|
import { $fonts, $toHTML } from "./xfa_object.js";
|
||||||
import { Binder } from "./bind.js";
|
import { Binder } from "./bind.js";
|
||||||
|
import { warn } from "../../shared/util.js";
|
||||||
import { XFAParser } from "./parser.js";
|
import { XFAParser } from "./parser.js";
|
||||||
|
|
||||||
class XFAFactory {
|
class XFAFactory {
|
||||||
@ -22,18 +23,25 @@ class XFAFactory {
|
|||||||
try {
|
try {
|
||||||
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
|
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
|
||||||
this.form = new Binder(this.root).bind();
|
this.form = new Binder(this.root).bind();
|
||||||
this._createPages();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
warn(`XFA - an error occured during parsing and binding: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
return this.root && this.form;
|
||||||
|
}
|
||||||
|
|
||||||
_createPages() {
|
_createPages() {
|
||||||
this.pages = this.form[$toHTML]();
|
try {
|
||||||
this.dims = this.pages.children.map(c => {
|
this.pages = this.form[$toHTML]();
|
||||||
const { width, height } = c.attributes.style;
|
this.dims = this.pages.children.map(c => {
|
||||||
return [0, 0, parseInt(width), parseInt(height)];
|
const { width, height } = c.attributes.style;
|
||||||
});
|
return [0, 0, parseInt(width), parseInt(height)];
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
warn(`XFA - an error occured during layout: ${e}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getBoundingBox(pageIndex) {
|
getBoundingBox(pageIndex) {
|
||||||
@ -41,9 +49,35 @@ class XFAFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get numberPages() {
|
get numberPages() {
|
||||||
|
if (!this.pages) {
|
||||||
|
this._createPages();
|
||||||
|
}
|
||||||
return this.dims.length;
|
return this.dims.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFonts(fonts) {
|
||||||
|
this.form[$fonts] = Object.create(null);
|
||||||
|
for (const font of fonts) {
|
||||||
|
const cssFontInfo = font.cssFontInfo;
|
||||||
|
const name = cssFontInfo.fontFamily;
|
||||||
|
if (!this.form[$fonts][name]) {
|
||||||
|
this.form[$fonts][name] = Object.create(null);
|
||||||
|
}
|
||||||
|
let property = "regular";
|
||||||
|
if (cssFontInfo.italicAngle !== "0") {
|
||||||
|
if (parseFloat(cssFontInfo.fontWeight) >= 700) {
|
||||||
|
property = "bolditalic";
|
||||||
|
} else {
|
||||||
|
property = "italic";
|
||||||
|
}
|
||||||
|
} else if (parseFloat(cssFontInfo.fontWeight) >= 700) {
|
||||||
|
property = "bold";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.form[$fonts][name][property] = font;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getPages() {
|
getPages() {
|
||||||
if (!this.pages) {
|
if (!this.pages) {
|
||||||
this._createPages();
|
this._createPages();
|
||||||
|
@ -18,18 +18,14 @@ import {
|
|||||||
$getParent,
|
$getParent,
|
||||||
$getSubformParent,
|
$getSubformParent,
|
||||||
$nodeName,
|
$nodeName,
|
||||||
|
$pushGlyphs,
|
||||||
$toStyle,
|
$toStyle,
|
||||||
XFAObject,
|
XFAObject,
|
||||||
} from "./xfa_object.js";
|
} from "./xfa_object.js";
|
||||||
import { getMeasurement } from "./utils.js";
|
import { getMeasurement } from "./utils.js";
|
||||||
|
import { TextMeasure } from "./text.js";
|
||||||
import { warn } from "../../shared/util.js";
|
import { warn } from "../../shared/util.js";
|
||||||
|
|
||||||
const wordNonWordRegex = new RegExp(
|
|
||||||
"([\\p{N}\\p{L}\\p{M}]+)|([^\\p{N}\\p{L}\\p{M}]+)",
|
|
||||||
"gu"
|
|
||||||
);
|
|
||||||
const wordFirstRegex = new RegExp("^[\\p{N}\\p{L}\\p{M}]", "u");
|
|
||||||
|
|
||||||
function measureToString(m) {
|
function measureToString(m) {
|
||||||
if (typeof m === "string") {
|
if (typeof m === "string") {
|
||||||
return "0px";
|
return "0px";
|
||||||
@ -192,65 +188,15 @@ const converters = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function layoutText(text, fontSize, space) {
|
function layoutText(text, xfaFont, fonts, width) {
|
||||||
// Try to guess width and height for the given text in taking into
|
const measure = new TextMeasure(xfaFont, fonts);
|
||||||
// account the space where the text should fit.
|
if (typeof text === "string") {
|
||||||
// The computed dimensions are just an overestimation.
|
measure.addString(text);
|
||||||
// TODO: base this estimation on real metrics.
|
} else {
|
||||||
let width = 0;
|
text[$pushGlyphs](measure);
|
||||||
let height = 0;
|
|
||||||
let totalWidth = 0;
|
|
||||||
const lineHeight = fontSize * 1.5;
|
|
||||||
const averageCharSize = fontSize * 0.4;
|
|
||||||
const maxCharOnLine = Math.floor(space.width / averageCharSize);
|
|
||||||
const chunks = text.match(wordNonWordRegex);
|
|
||||||
let treatedChars = 0;
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
let chunk = chunks[0];
|
|
||||||
while (chunk) {
|
|
||||||
const w = chunk.length * averageCharSize;
|
|
||||||
if (width + w <= space.width) {
|
|
||||||
width += w;
|
|
||||||
treatedChars += chunk.length;
|
|
||||||
chunk = chunks[i++];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wordFirstRegex.test(chunk) || chunk.length > maxCharOnLine) {
|
|
||||||
const numOfCharOnLine = Math.floor(
|
|
||||||
(space.width - width) / averageCharSize
|
|
||||||
);
|
|
||||||
chunk = chunk.slice(numOfCharOnLine);
|
|
||||||
treatedChars += numOfCharOnLine;
|
|
||||||
if (height + lineHeight > space.height) {
|
|
||||||
return { width: 0, height: 0, splitPos: treatedChars };
|
|
||||||
}
|
|
||||||
totalWidth = Math.max(width, totalWidth);
|
|
||||||
width = 0;
|
|
||||||
height += lineHeight;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (height + lineHeight > space.height) {
|
|
||||||
return { width: 0, height: 0, splitPos: treatedChars };
|
|
||||||
}
|
|
||||||
|
|
||||||
totalWidth = Math.max(width, totalWidth);
|
|
||||||
width = w;
|
|
||||||
height += lineHeight;
|
|
||||||
chunk = chunks[i++];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalWidth === 0) {
|
return measure.compute(width);
|
||||||
totalWidth = width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalWidth !== 0) {
|
|
||||||
height += lineHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { width: totalWidth, height, splitPos: -1 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeBbox(node, html, availableSpace) {
|
function computeBbox(node, html, availableSpace) {
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
$extra,
|
$extra,
|
||||||
$finalize,
|
$finalize,
|
||||||
$flushHTML,
|
$flushHTML,
|
||||||
|
$fonts,
|
||||||
$getAvailableSpace,
|
$getAvailableSpace,
|
||||||
$getChildren,
|
$getChildren,
|
||||||
$getContainedChildren,
|
$getContainedChildren,
|
||||||
@ -1522,14 +1523,51 @@ class Draw extends XFAObject {
|
|||||||
|
|
||||||
fixDimensions(this);
|
fixDimensions(this);
|
||||||
|
|
||||||
if (this.w !== "" && this.h === "" && this.value) {
|
if ((this.w === "" || this.h === "") && this.value) {
|
||||||
const text = this.value[$text]();
|
const maxWidth = this.w === "" ? availableSpace.width : this.w;
|
||||||
if (text) {
|
const fonts = getRoot(this)[$fonts];
|
||||||
const { height } = layoutText(text, this.font.size, {
|
let font = this.font;
|
||||||
width: this.w,
|
if (!font) {
|
||||||
height: Infinity,
|
let parent = this[$getParent]();
|
||||||
});
|
while (!(parent instanceof Template)) {
|
||||||
this.h = height || "";
|
if (parent.font) {
|
||||||
|
font = parent.font;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parent = parent[$getParent]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let height = null;
|
||||||
|
let width = null;
|
||||||
|
if (
|
||||||
|
this.value.exData &&
|
||||||
|
this.value.exData[$content] &&
|
||||||
|
this.value.exData.contentType === "text/html"
|
||||||
|
) {
|
||||||
|
const res = layoutText(
|
||||||
|
this.value.exData[$content],
|
||||||
|
font,
|
||||||
|
fonts,
|
||||||
|
maxWidth
|
||||||
|
);
|
||||||
|
width = res.width;
|
||||||
|
height = res.height;
|
||||||
|
} else {
|
||||||
|
const text = this.value[$text]();
|
||||||
|
if (text) {
|
||||||
|
const res = layoutText(text, font, fonts, maxWidth);
|
||||||
|
width = res.width;
|
||||||
|
height = res.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width !== null && this.w === "") {
|
||||||
|
this.w = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height !== null && this.h === "") {
|
||||||
|
this.h = height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2623,7 +2661,7 @@ class Font extends XFAObject {
|
|||||||
]);
|
]);
|
||||||
this.posture = getStringOption(attributes.posture, ["normal", "italic"]);
|
this.posture = getStringOption(attributes.posture, ["normal", "italic"]);
|
||||||
this.size = getMeasurement(attributes.size, "10pt");
|
this.size = getMeasurement(attributes.size, "10pt");
|
||||||
this.typeface = attributes.typeface || "";
|
this.typeface = attributes.typeface || "Courier";
|
||||||
this.underline = getInteger({
|
this.underline = getInteger({
|
||||||
data: attributes.underline,
|
data: attributes.underline,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
@ -4484,7 +4522,6 @@ class Template extends XFAObject {
|
|||||||
children: [],
|
children: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this[$extra] = {
|
this[$extra] = {
|
||||||
overflowNode: null,
|
overflowNode: null,
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
|
218
src/core/xfa/text.js
Normal file
218
src/core/xfa/text.js
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const WIDTH_FACTOR = 1.2;
|
||||||
|
const HEIGHT_FACTOR = 1.2;
|
||||||
|
|
||||||
|
class FontInfo {
|
||||||
|
constructor(xfaFont, fonts) {
|
||||||
|
if (!xfaFont) {
|
||||||
|
[this.pdfFont, this.xfaFont] = this.defaultFont(fonts);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xfaFont = xfaFont;
|
||||||
|
let typeface = fonts[xfaFont.typeface];
|
||||||
|
if (!typeface) {
|
||||||
|
typeface = fonts[`${xfaFont.typeface}-PdfJS-XFA`];
|
||||||
|
}
|
||||||
|
if (!typeface) {
|
||||||
|
[this.pdfFont, this.xfaFont] = this.defaultFont(fonts);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pdfFont = null;
|
||||||
|
if (xfaFont.posture === "italic") {
|
||||||
|
if (xfaFont.weight === "bold") {
|
||||||
|
this.pdfFont = typeface.bolditalic;
|
||||||
|
} else {
|
||||||
|
this.pdfFont = typeface.italic;
|
||||||
|
}
|
||||||
|
} else if (xfaFont.weigth === "bold") {
|
||||||
|
this.pdfFont = typeface.bold;
|
||||||
|
} else {
|
||||||
|
this.pdfFont = typeface.regular;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.pdfFont) {
|
||||||
|
[this.pdfFont, this.xfaFont] = this.defaultFont(fonts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultFont(fonts) {
|
||||||
|
// TODO: Add a default font based on Liberation.
|
||||||
|
const font =
|
||||||
|
fonts.Helvetica ||
|
||||||
|
fonts["Myriad Pro"] ||
|
||||||
|
fonts.Arial ||
|
||||||
|
fonts.ArialMT ||
|
||||||
|
Object.values(fonts)[0];
|
||||||
|
const pdfFont = font.regular;
|
||||||
|
const info = this.pdfFont.cssFontInfo;
|
||||||
|
const xfaFont = {
|
||||||
|
typeface: info.fontFamily,
|
||||||
|
posture: "normal",
|
||||||
|
weight: "normal",
|
||||||
|
size: 10,
|
||||||
|
};
|
||||||
|
return [pdfFont, xfaFont];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FontSelector {
|
||||||
|
constructor(defaultXfaFont, fonts) {
|
||||||
|
this.fonts = fonts;
|
||||||
|
this.stack = [new FontInfo(defaultXfaFont, fonts)];
|
||||||
|
}
|
||||||
|
|
||||||
|
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.fonts);
|
||||||
|
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 pdfFont = lastFont.pdfFont;
|
||||||
|
const fontSize = lastFont.xfaFont.size;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
@ -34,6 +34,7 @@ const $dump = Symbol();
|
|||||||
const $extra = Symbol("extra");
|
const $extra = Symbol("extra");
|
||||||
const $finalize = Symbol();
|
const $finalize = Symbol();
|
||||||
const $flushHTML = Symbol();
|
const $flushHTML = Symbol();
|
||||||
|
const $fonts = Symbol();
|
||||||
const $getAttributeIt = Symbol();
|
const $getAttributeIt = Symbol();
|
||||||
const $getAvailableSpace = Symbol();
|
const $getAvailableSpace = Symbol();
|
||||||
const $getChildrenByClass = Symbol();
|
const $getChildrenByClass = Symbol();
|
||||||
@ -46,6 +47,7 @@ const $getContainedChildren = Symbol();
|
|||||||
const $getNextPage = Symbol();
|
const $getNextPage = Symbol();
|
||||||
const $getSubformParent = Symbol();
|
const $getSubformParent = Symbol();
|
||||||
const $getParent = Symbol();
|
const $getParent = Symbol();
|
||||||
|
const $pushGlyphs = Symbol();
|
||||||
const $global = Symbol();
|
const $global = Symbol();
|
||||||
const $hasItem = Symbol();
|
const $hasItem = Symbol();
|
||||||
const $hasSettableValue = Symbol();
|
const $hasSettableValue = Symbol();
|
||||||
@ -970,6 +972,7 @@ export {
|
|||||||
$extra,
|
$extra,
|
||||||
$finalize,
|
$finalize,
|
||||||
$flushHTML,
|
$flushHTML,
|
||||||
|
$fonts,
|
||||||
$getAttributeIt,
|
$getAttributeIt,
|
||||||
$getAvailableSpace,
|
$getAvailableSpace,
|
||||||
$getChildren,
|
$getChildren,
|
||||||
@ -998,6 +1001,7 @@ export {
|
|||||||
$onChild,
|
$onChild,
|
||||||
$onChildCheck,
|
$onChildCheck,
|
||||||
$onText,
|
$onText,
|
||||||
|
$pushGlyphs,
|
||||||
$removeChild,
|
$removeChild,
|
||||||
$resolvePrototypes,
|
$resolvePrototypes,
|
||||||
$root,
|
$root,
|
||||||
|
@ -18,8 +18,10 @@ import {
|
|||||||
$childrenToHTML,
|
$childrenToHTML,
|
||||||
$content,
|
$content,
|
||||||
$extra,
|
$extra,
|
||||||
|
$getChildren,
|
||||||
$nodeName,
|
$nodeName,
|
||||||
$onText,
|
$onText,
|
||||||
|
$pushGlyphs,
|
||||||
$text,
|
$text,
|
||||||
$toHTML,
|
$toHTML,
|
||||||
XmlObject,
|
XmlObject,
|
||||||
@ -167,6 +169,39 @@ class XhtmlObject extends XmlObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[$pushGlyphs](measure) {
|
||||||
|
const xfaFont = Object.create(null);
|
||||||
|
for (const [key, value] of this.style
|
||||||
|
.split(";")
|
||||||
|
.map(s => s.split(":", 2))) {
|
||||||
|
if (!key.startsWith("font-")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === "font-family") {
|
||||||
|
xfaFont.typeface = value;
|
||||||
|
} else if (key === "font-size") {
|
||||||
|
xfaFont.size = getMeasurement(value);
|
||||||
|
} else if (key === "font-weight") {
|
||||||
|
xfaFont.weight = value;
|
||||||
|
} else if (key === "font-style") {
|
||||||
|
xfaFont.posture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
measure.pushFont(xfaFont);
|
||||||
|
if (this[$content]) {
|
||||||
|
measure.addString(this[$content]);
|
||||||
|
} else {
|
||||||
|
for (const child of this[$getChildren]()) {
|
||||||
|
if (child[$nodeName] === "#text") {
|
||||||
|
measure.addString(child[$content]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
child[$pushGlyphs](measure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
measure.popFont();
|
||||||
|
}
|
||||||
|
|
||||||
[$toHTML](availableSpace) {
|
[$toHTML](availableSpace) {
|
||||||
const children = [];
|
const children = [];
|
||||||
this[$extra] = {
|
this[$extra] = {
|
||||||
@ -202,6 +237,12 @@ class B extends XhtmlObject {
|
|||||||
constructor(attributes) {
|
constructor(attributes) {
|
||||||
super(attributes, "b");
|
super(attributes, "b");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[$pushGlyphs](measure) {
|
||||||
|
measure.pushFont({ weight: "bold" });
|
||||||
|
super[$pushGlyphs](measure);
|
||||||
|
measure.popFont();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Body extends XhtmlObject {
|
class Body extends XhtmlObject {
|
||||||
@ -230,6 +271,10 @@ class Br extends XhtmlObject {
|
|||||||
return "\n";
|
return "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[$pushGlyphs](measure) {
|
||||||
|
measure.addString("\n");
|
||||||
|
}
|
||||||
|
|
||||||
[$toHTML](availableSpace) {
|
[$toHTML](availableSpace) {
|
||||||
return HTMLResult.success({
|
return HTMLResult.success({
|
||||||
name: "br",
|
name: "br",
|
||||||
@ -282,6 +327,12 @@ class I extends XhtmlObject {
|
|||||||
constructor(attributes) {
|
constructor(attributes) {
|
||||||
super(attributes, "i");
|
super(attributes, "i");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[$pushGlyphs](measure) {
|
||||||
|
measure.pushFont({ posture: "italic" });
|
||||||
|
super[$pushGlyphs](measure);
|
||||||
|
measure.popFont();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Li extends XhtmlObject {
|
class Li extends XhtmlObject {
|
||||||
@ -301,6 +352,11 @@ class P extends XhtmlObject {
|
|||||||
super(attributes, "p");
|
super(attributes, "p");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[$pushGlyphs](measure) {
|
||||||
|
super[$pushGlyphs](measure);
|
||||||
|
measure.addString("\n");
|
||||||
|
}
|
||||||
|
|
||||||
[$text]() {
|
[$text]() {
|
||||||
return super[$text]() + "\n";
|
return super[$text]() + "\n";
|
||||||
}
|
}
|
||||||
|
1
test/pdfs/xfa_issue13500.pdf.link
Normal file
1
test/pdfs/xfa_issue13500.pdf.link
Normal file
@ -0,0 +1 @@
|
|||||||
|
https://github.com/mozilla/pdf.js/files/6602628/Acrobat.pdf
|
@ -5341,5 +5341,13 @@
|
|||||||
"type": "eq",
|
"type": "eq",
|
||||||
"forms": true,
|
"forms": true,
|
||||||
"lastPage": 1
|
"lastPage": 1
|
||||||
|
},
|
||||||
|
{ "id": "xfa_issue13500",
|
||||||
|
"file": "pdfs/xfa_issue13500.pdf",
|
||||||
|
"md5": "b81274a19f5a95c1466db3648f1be491",
|
||||||
|
"link": true,
|
||||||
|
"rounds": 1,
|
||||||
|
"enableXfa": true,
|
||||||
|
"type": "eq"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -188,7 +188,7 @@ describe("XFAFactory", function () {
|
|||||||
</pageArea>
|
</pageArea>
|
||||||
</pageSet>
|
</pageSet>
|
||||||
<subform name="first">
|
<subform name="first">
|
||||||
<draw><value><text>foo</text></value></draw>
|
<draw w="1pt" h="1pt"><value><text>foo</text></value></draw>
|
||||||
</subform>
|
</subform>
|
||||||
</subform>
|
</subform>
|
||||||
</template>
|
</template>
|
||||||
|
Loading…
Reference in New Issue
Block a user