Skip to content

Commit

Permalink
XFA - Add the possibily to layout and measure text
Browse files Browse the repository at this point in the history
  - 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.
  • Loading branch information
calixteman committed Jun 16, 2021
1 parent f9a0568 commit 4e75b6f
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 90 deletions.
13 changes: 12 additions & 1 deletion src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,10 @@ class PDFDocument {
return shadow(this, "xfaFaxtory", null);
}

get isPureXfa() {
return this.xfaFactory && this.xfaFactory.isValid();
}

get htmlForXfa() {
if (this.xfaFactory) {
return this.xfaFactory.getPages();
Expand Down Expand Up @@ -898,8 +902,14 @@ class PDFDocument {
options,
});
const operatorList = new OperatorList();
const pdfFonts = [];
const initialState = {
font: null,
get font() {
return pdfFonts[pdfFonts.length - 1];
},
set font(font) {
pdfFonts.push(font);
},
clone() {
return this;
},
Expand Down Expand Up @@ -947,6 +957,7 @@ class PDFDocument {
);
}
await Promise.all(promises);
this.xfaFactory.setFonts(pdfFonts);
}

get formInfo() {
Expand Down
6 changes: 5 additions & 1 deletion src/core/fonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,7 @@ class Font {
this.capHeight = properties.capHeight / PDF_GLYPH_SPACE_UNITS;
this.ascent = properties.ascent / PDF_GLYPH_SPACE_UNITS;
this.descent = properties.descent / PDF_GLYPH_SPACE_UNITS;
this.lineHeight = this.ascent - this.descent;
this.fontMatrix = properties.fontMatrix;
this.bbox = properties.bbox;
this.defaultEncoding = properties.defaultEncoding;
Expand Down Expand Up @@ -2466,13 +2467,16 @@ class Font {
unitsPerEm: int16(tables.head.data[18], tables.head.data[19]),
yMax: int16(tables.head.data[42], tables.head.data[43]),
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]),
lineGap: signedInt16(tables.hhea.data[8], tables.hhea.data[9]),
};

// PDF FontDescriptor metrics lie -- using data from actual font.
this.ascent = metricsOverride.ascent / 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.
if (tables.post) {
Expand Down
20 changes: 13 additions & 7 deletions src/core/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,8 @@ class WorkerMessageHandler {
await pdfManager.ensureDoc("checkFirstPage");
}

const [numPages, fingerprint, htmlForXfa] = await Promise.all([
pdfManager.ensureDoc("numPages"),
pdfManager.ensureDoc("fingerprint"),
pdfManager.ensureDoc("htmlForXfa"),
]);

if (htmlForXfa) {
const isPureXfa = await pdfManager.ensureDoc("isPureXfa");
if (isPureXfa) {
const task = new WorkerTask("loadXfaFonts");
startWorkerTask(task);
await pdfManager
Expand All @@ -203,6 +198,17 @@ class WorkerMessageHandler {
})
.then(() => finishWorkerTask(task));
}

const [numPages, fingerprint] = await Promise.all([
pdfManager.ensureDoc("numPages"),
pdfManager.ensureDoc("fingerprint"),
]);

// Get htmlForXfa after numPages to avoid to create HTML 2 times.
const htmlForXfa = isPureXfa
? await pdfManager.ensureDoc("htmlForXfa")
: null;

return { numPages, fingerprint, htmlForXfa };
}

Expand Down
50 changes: 42 additions & 8 deletions src/core/xfa/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,71 @@
* limitations under the License.
*/

import { $toHTML } from "./xfa_object.js";
import { $fonts, $toHTML } from "./xfa_object.js";
import { Binder } from "./bind.js";
import { warn } from "../../shared/util.js";
import { XFAParser } from "./parser.js";

class XFAFactory {
constructor(data) {
try {
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
this.form = new Binder(this.root).bind();
this._createPages();
} catch (e) {
console.log(e);
warn(`XFA - an error occured during parsing and binding: ${e}`);
}
}

isValid() {
return this.root && this.form;
}

_createPages() {
this.pages = this.form[$toHTML]();
this.dims = this.pages.children.map(c => {
const { width, height } = c.attributes.style;
return [0, 0, parseInt(width), parseInt(height)];
});
try {
this.pages = this.form[$toHTML]();
this.dims = this.pages.children.map(c => {
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) {
return this.dims[pageIndex];
}

get numberPages() {
if (!this.pages) {
this._createPages();
}
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() {
if (!this.pages) {
this._createPages();
Expand Down
72 changes: 9 additions & 63 deletions src/core/xfa/html_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,14 @@ import {
$getParent,
$getSubformParent,
$nodeName,
$pushGlyphs,
$toStyle,
XFAObject,
} from "./xfa_object.js";
import { getMeasurement } from "./utils.js";
import { TextMeasure } from "./text.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) {
if (typeof m === "string") {
return "0px";
Expand Down Expand Up @@ -192,65 +188,15 @@ const converters = {
},
};

function layoutText(text, fontSize, space) {
// Try to guess width and height for the given text in taking into
// account the space where the text should fit.
// The computed dimensions are just an overestimation.
// TODO: base this estimation on real metrics.
let width = 0;
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) {
totalWidth = width;
}

if (totalWidth !== 0) {
height += lineHeight;
function layoutText(text, xfaFont, fonts, width) {
const measure = new TextMeasure(xfaFont, fonts);
if (typeof text === "string") {
measure.addString(text);
} else {
text[$pushGlyphs](measure);
}

return { width: totalWidth, height, splitPos: -1 };
return measure.compute(width);
}

function computeBbox(node, html, availableSpace) {
Expand Down
57 changes: 47 additions & 10 deletions src/core/xfa/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
$extra,
$finalize,
$flushHTML,
$fonts,
$getAvailableSpace,
$getChildren,
$getContainedChildren,
Expand Down Expand Up @@ -1521,14 +1522,51 @@ class Draw extends XFAObject {

fixDimensions(this);

if (this.w !== "" && this.h === "" && this.value) {
const text = this.value[$text]();
if (text) {
const { height } = layoutText(text, this.font.size, {
width: this.w,
height: Infinity,
});
this.h = height || "";
if ((this.w === "" || this.h === "") && this.value) {
const maxWidth = this.w === "" ? availableSpace.width : this.w;
const fonts = getRoot(this)[$fonts];
let font = this.font;
if (!font) {
let parent = this[$getParent]();
while (!(parent instanceof Template)) {
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;
}
}

Expand Down Expand Up @@ -2622,7 +2660,7 @@ class Font extends XFAObject {
]);
this.posture = getStringOption(attributes.posture, ["normal", "italic"]);
this.size = getMeasurement(attributes.size, "10pt");
this.typeface = attributes.typeface || "";
this.typeface = attributes.typeface || "Myriad Pro";
this.underline = getInteger({
data: attributes.underline,
defaultValue: 0,
Expand Down Expand Up @@ -4483,7 +4521,6 @@ class Template extends XFAObject {
children: [],
});
}

this[$extra] = {
overflowNode: null,
pageNumber: 1,
Expand Down
Loading

0 comments on commit 4e75b6f

Please sign in to comment.