Skip to content

Commit 4e75b6f

Browse files
committed
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.
1 parent f9a0568 commit 4e75b6f

11 files changed

+397
-90
lines changed

src/core/document.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,10 @@ class PDFDocument {
857857
return shadow(this, "xfaFaxtory", null);
858858
}
859859

860+
get isPureXfa() {
861+
return this.xfaFactory && this.xfaFactory.isValid();
862+
}
863+
860864
get htmlForXfa() {
861865
if (this.xfaFactory) {
862866
return this.xfaFactory.getPages();
@@ -898,8 +902,14 @@ class PDFDocument {
898902
options,
899903
});
900904
const operatorList = new OperatorList();
905+
const pdfFonts = [];
901906
const initialState = {
902-
font: null,
907+
get font() {
908+
return pdfFonts[pdfFonts.length - 1];
909+
},
910+
set font(font) {
911+
pdfFonts.push(font);
912+
},
903913
clone() {
904914
return this;
905915
},
@@ -947,6 +957,7 @@ class PDFDocument {
947957
);
948958
}
949959
await Promise.all(promises);
960+
this.xfaFactory.setFonts(pdfFonts);
950961
}
951962

952963
get formInfo() {

src/core/fonts.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,7 @@ class Font {
844844
this.capHeight = properties.capHeight / PDF_GLYPH_SPACE_UNITS;
845845
this.ascent = properties.ascent / PDF_GLYPH_SPACE_UNITS;
846846
this.descent = properties.descent / PDF_GLYPH_SPACE_UNITS;
847+
this.lineHeight = this.ascent - this.descent;
847848
this.fontMatrix = properties.fontMatrix;
848849
this.bbox = properties.bbox;
849850
this.defaultEncoding = properties.defaultEncoding;
@@ -2466,13 +2467,16 @@ class Font {
24662467
unitsPerEm: int16(tables.head.data[18], tables.head.data[19]),
24672468
yMax: int16(tables.head.data[42], tables.head.data[43]),
24682469
yMin: signedInt16(tables.head.data[38], tables.head.data[39]),
2469-
ascent: int16(tables.hhea.data[4], tables.hhea.data[5]),
2470+
ascent: signedInt16(tables.hhea.data[4], tables.hhea.data[5]),
24702471
descent: signedInt16(tables.hhea.data[6], tables.hhea.data[7]),
2472+
lineGap: signedInt16(tables.hhea.data[8], tables.hhea.data[9]),
24712473
};
24722474

24732475
// PDF FontDescriptor metrics lie -- using data from actual font.
24742476
this.ascent = metricsOverride.ascent / metricsOverride.unitsPerEm;
24752477
this.descent = metricsOverride.descent / metricsOverride.unitsPerEm;
2478+
this.lineGap = metricsOverride.lineGap / metricsOverride.unitsPerEm;
2479+
this.lineHeight = this.ascent - this.descent + this.lineGap;
24762480

24772481
// The 'post' table has glyphs names.
24782482
if (tables.post) {

src/core/worker.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,8 @@ class WorkerMessageHandler {
187187
await pdfManager.ensureDoc("checkFirstPage");
188188
}
189189

190-
const [numPages, fingerprint, htmlForXfa] = await Promise.all([
191-
pdfManager.ensureDoc("numPages"),
192-
pdfManager.ensureDoc("fingerprint"),
193-
pdfManager.ensureDoc("htmlForXfa"),
194-
]);
195-
196-
if (htmlForXfa) {
190+
const isPureXfa = await pdfManager.ensureDoc("isPureXfa");
191+
if (isPureXfa) {
197192
const task = new WorkerTask("loadXfaFonts");
198193
startWorkerTask(task);
199194
await pdfManager
@@ -203,6 +198,17 @@ class WorkerMessageHandler {
203198
})
204199
.then(() => finishWorkerTask(task));
205200
}
201+
202+
const [numPages, fingerprint] = await Promise.all([
203+
pdfManager.ensureDoc("numPages"),
204+
pdfManager.ensureDoc("fingerprint"),
205+
]);
206+
207+
// Get htmlForXfa after numPages to avoid to create HTML 2 times.
208+
const htmlForXfa = isPureXfa
209+
? await pdfManager.ensureDoc("htmlForXfa")
210+
: null;
211+
206212
return { numPages, fingerprint, htmlForXfa };
207213
}
208214

src/core/xfa/factory.js

+42-8
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,71 @@
1313
* limitations under the License.
1414
*/
1515

16-
import { $toHTML } from "./xfa_object.js";
16+
import { $fonts, $toHTML } from "./xfa_object.js";
1717
import { Binder } from "./bind.js";
18+
import { warn } from "../../shared/util.js";
1819
import { XFAParser } from "./parser.js";
1920

2021
class XFAFactory {
2122
constructor(data) {
2223
try {
2324
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
2425
this.form = new Binder(this.root).bind();
25-
this._createPages();
2626
} catch (e) {
27-
console.log(e);
27+
warn(`XFA - an error occured during parsing and binding: ${e}`);
2828
}
2929
}
3030

31+
isValid() {
32+
return this.root && this.form;
33+
}
34+
3135
_createPages() {
32-
this.pages = this.form[$toHTML]();
33-
this.dims = this.pages.children.map(c => {
34-
const { width, height } = c.attributes.style;
35-
return [0, 0, parseInt(width), parseInt(height)];
36-
});
36+
try {
37+
this.pages = this.form[$toHTML]();
38+
this.dims = this.pages.children.map(c => {
39+
const { width, height } = c.attributes.style;
40+
return [0, 0, parseInt(width), parseInt(height)];
41+
});
42+
} catch (e) {
43+
warn(`XFA - an error occured during layout: ${e}`);
44+
}
3745
}
3846

3947
getBoundingBox(pageIndex) {
4048
return this.dims[pageIndex];
4149
}
4250

4351
get numberPages() {
52+
if (!this.pages) {
53+
this._createPages();
54+
}
4455
return this.dims.length;
4556
}
4657

58+
setFonts(fonts) {
59+
this.form[$fonts] = Object.create(null);
60+
for (const font of fonts) {
61+
const cssFontInfo = font.cssFontInfo;
62+
const name = cssFontInfo.fontFamily;
63+
if (!this.form[$fonts][name]) {
64+
this.form[$fonts][name] = Object.create(null);
65+
}
66+
let property = "regular";
67+
if (cssFontInfo.italicAngle !== "0") {
68+
if (parseFloat(cssFontInfo.fontWeight) >= 700) {
69+
property = "bolditalic";
70+
} else {
71+
property = "italic";
72+
}
73+
} else if (parseFloat(cssFontInfo.fontWeight) >= 700) {
74+
property = "bold";
75+
}
76+
77+
this.form[$fonts][name][property] = font;
78+
}
79+
}
80+
4781
getPages() {
4882
if (!this.pages) {
4983
this._createPages();

src/core/xfa/html_utils.js

+9-63
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,14 @@ import {
1818
$getParent,
1919
$getSubformParent,
2020
$nodeName,
21+
$pushGlyphs,
2122
$toStyle,
2223
XFAObject,
2324
} from "./xfa_object.js";
2425
import { getMeasurement } from "./utils.js";
26+
import { TextMeasure } from "./text.js";
2527
import { warn } from "../../shared/util.js";
2628

27-
const wordNonWordRegex = new RegExp(
28-
"([\\p{N}\\p{L}\\p{M}]+)|([^\\p{N}\\p{L}\\p{M}]+)",
29-
"gu"
30-
);
31-
const wordFirstRegex = new RegExp("^[\\p{N}\\p{L}\\p{M}]", "u");
32-
3329
function measureToString(m) {
3430
if (typeof m === "string") {
3531
return "0px";
@@ -192,65 +188,15 @@ const converters = {
192188
},
193189
};
194190

195-
function layoutText(text, fontSize, space) {
196-
// Try to guess width and height for the given text in taking into
197-
// account the space where the text should fit.
198-
// The computed dimensions are just an overestimation.
199-
// TODO: base this estimation on real metrics.
200-
let width = 0;
201-
let height = 0;
202-
let totalWidth = 0;
203-
const lineHeight = fontSize * 1.5;
204-
const averageCharSize = fontSize * 0.4;
205-
const maxCharOnLine = Math.floor(space.width / averageCharSize);
206-
const chunks = text.match(wordNonWordRegex);
207-
let treatedChars = 0;
208-
209-
let i = 0;
210-
let chunk = chunks[0];
211-
while (chunk) {
212-
const w = chunk.length * averageCharSize;
213-
if (width + w <= space.width) {
214-
width += w;
215-
treatedChars += chunk.length;
216-
chunk = chunks[i++];
217-
continue;
218-
}
219-
220-
if (!wordFirstRegex.test(chunk) || chunk.length > maxCharOnLine) {
221-
const numOfCharOnLine = Math.floor(
222-
(space.width - width) / averageCharSize
223-
);
224-
chunk = chunk.slice(numOfCharOnLine);
225-
treatedChars += numOfCharOnLine;
226-
if (height + lineHeight > space.height) {
227-
return { width: 0, height: 0, splitPos: treatedChars };
228-
}
229-
totalWidth = Math.max(width, totalWidth);
230-
width = 0;
231-
height += lineHeight;
232-
continue;
233-
}
234-
235-
if (height + lineHeight > space.height) {
236-
return { width: 0, height: 0, splitPos: treatedChars };
237-
}
238-
239-
totalWidth = Math.max(width, totalWidth);
240-
width = w;
241-
height += lineHeight;
242-
chunk = chunks[i++];
243-
}
244-
245-
if (totalWidth === 0) {
246-
totalWidth = width;
247-
}
248-
249-
if (totalWidth !== 0) {
250-
height += lineHeight;
191+
function layoutText(text, xfaFont, fonts, width) {
192+
const measure = new TextMeasure(xfaFont, fonts);
193+
if (typeof text === "string") {
194+
measure.addString(text);
195+
} else {
196+
text[$pushGlyphs](measure);
251197
}
252198

253-
return { width: totalWidth, height, splitPos: -1 };
199+
return measure.compute(width);
254200
}
255201

256202
function computeBbox(node, html, availableSpace) {

src/core/xfa/template.js

+47-10
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
$extra,
2323
$finalize,
2424
$flushHTML,
25+
$fonts,
2526
$getAvailableSpace,
2627
$getChildren,
2728
$getContainedChildren,
@@ -1521,14 +1522,51 @@ class Draw extends XFAObject {
15211522

15221523
fixDimensions(this);
15231524

1524-
if (this.w !== "" && this.h === "" && this.value) {
1525-
const text = this.value[$text]();
1526-
if (text) {
1527-
const { height } = layoutText(text, this.font.size, {
1528-
width: this.w,
1529-
height: Infinity,
1530-
});
1531-
this.h = height || "";
1525+
if ((this.w === "" || this.h === "") && this.value) {
1526+
const maxWidth = this.w === "" ? availableSpace.width : this.w;
1527+
const fonts = getRoot(this)[$fonts];
1528+
let font = this.font;
1529+
if (!font) {
1530+
let parent = this[$getParent]();
1531+
while (!(parent instanceof Template)) {
1532+
if (parent.font) {
1533+
font = parent.font;
1534+
break;
1535+
}
1536+
parent = parent[$getParent]();
1537+
}
1538+
}
1539+
1540+
let height = null;
1541+
let width = null;
1542+
if (
1543+
this.value.exData &&
1544+
this.value.exData[$content] &&
1545+
this.value.exData.contentType === "text/html"
1546+
) {
1547+
const res = layoutText(
1548+
this.value.exData[$content],
1549+
font,
1550+
fonts,
1551+
maxWidth
1552+
);
1553+
width = res.width;
1554+
height = res.height;
1555+
} else {
1556+
const text = this.value[$text]();
1557+
if (text) {
1558+
const res = layoutText(text, font, fonts, maxWidth);
1559+
width = res.width;
1560+
height = res.height;
1561+
}
1562+
}
1563+
1564+
if (width !== null && this.w === "") {
1565+
this.w = width;
1566+
}
1567+
1568+
if (height !== null && this.h === "") {
1569+
this.h = height;
15321570
}
15331571
}
15341572

@@ -2622,7 +2660,7 @@ class Font extends XFAObject {
26222660
]);
26232661
this.posture = getStringOption(attributes.posture, ["normal", "italic"]);
26242662
this.size = getMeasurement(attributes.size, "10pt");
2625-
this.typeface = attributes.typeface || "";
2663+
this.typeface = attributes.typeface || "Myriad Pro";
26262664
this.underline = getInteger({
26272665
data: attributes.underline,
26282666
defaultValue: 0,
@@ -4483,7 +4521,6 @@ class Template extends XFAObject {
44834521
children: [],
44844522
});
44854523
}
4486-
44874524
this[$extra] = {
44884525
overflowNode: null,
44894526
pageNumber: 1,

0 commit comments

Comments
 (0)