Skip to content

Commit ae94a07

Browse files
committed
XFA - Add a layer to display XFA forms
- add an option to enable XFA rendering if any; - for now, let the canvas layer: it could be useful to implement XFAF forms (embedded pdf in xml stream for the background and xfa form for the foreground); - ui elements in template DOM are pretty close to their html counterpart so we generate a fake html DOM from template one: - it makes easier to translate template properties to html ones; - it makes faster the creation of the html element in the main thread.
1 parent 3243672 commit ae94a07

19 files changed

+820
-27
lines changed

src/core/document.js

+113
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import {
1717
assert,
18+
bytesToString,
1819
FormatError,
1920
info,
2021
InvalidPDFException,
@@ -28,6 +29,7 @@ import {
2829
shadow,
2930
stringToBytes,
3031
stringToPDFString,
32+
stringToUTF8String,
3133
unreachable,
3234
Util,
3335
warn,
@@ -56,6 +58,7 @@ import { calculateMD5 } from "./crypto.js";
5658
import { Linearization } from "./parser.js";
5759
import { OperatorList } from "./operator_list.js";
5860
import { PartialEvaluator } from "./evaluator.js";
61+
import { XFAFactory } from "./xfa/factory.js";
5962

6063
const DEFAULT_USER_UNIT = 1.0;
6164
const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792];
@@ -79,6 +82,7 @@ class Page {
7982
builtInCMapCache,
8083
globalImageCache,
8184
nonBlendModesSet,
85+
xfaFactory,
8286
}) {
8387
this.pdfManager = pdfManager;
8488
this.pageIndex = pageIndex;
@@ -91,6 +95,7 @@ class Page {
9195
this.nonBlendModesSet = nonBlendModesSet;
9296
this.evaluatorOptions = pdfManager.evaluatorOptions;
9397
this.resourcesPromise = null;
98+
this.xfaFactory = xfaFactory;
9499

95100
const idCounters = {
96101
obj: 0,
@@ -137,6 +142,11 @@ class Page {
137142
}
138143

139144
_getBoundingBox(name) {
145+
if (this.xfaData) {
146+
const { width, height } = this.xfaData.attributes.style;
147+
return [0, 0, parseInt(width), parseInt(height)];
148+
}
149+
140150
const box = this._getInheritableProperty(name, /* getArray = */ true);
141151

142152
if (Array.isArray(box) && box.length === 4) {
@@ -231,6 +241,13 @@ class Page {
231241
return stream;
232242
}
233243

244+
get xfaData() {
245+
if (this.xfaFactory) {
246+
return shadow(this, "xfaData", this.xfaFactory.getPage(this.pageIndex));
247+
}
248+
return shadow(this, "xfaData", null);
249+
}
250+
234251
save(handler, task, annotationStorage) {
235252
const partialEvaluator = new PartialEvaluator({
236253
xref: this.xref,
@@ -695,6 +712,9 @@ class PDFDocument {
695712
}
696713

697714
get numPages() {
715+
if (this.xfaFactory) {
716+
return shadow(this, "numPages", this.xfaFactory.numberPages);
717+
}
698718
const linearization = this.linearization;
699719
const num = linearization ? linearization.numPages : this.catalog.numPages;
700720
return shadow(this, "numPages", num);
@@ -732,6 +752,80 @@ class PDFDocument {
732752
});
733753
}
734754

755+
get xfaData() {
756+
const acroForm = this.catalog.acroForm;
757+
if (!acroForm) {
758+
return null;
759+
}
760+
761+
const xfa = acroForm.get("XFA");
762+
const entries = {
763+
"xdp:xdp": "",
764+
template: "",
765+
datasets: "",
766+
config: "",
767+
connectionSet: "",
768+
localeSet: "",
769+
stylesheet: "",
770+
"/xdp:xdp": "",
771+
};
772+
if (isStream(xfa) && !xfa.isEmpty) {
773+
try {
774+
entries["xdp:xdp"] = stringToUTF8String(bytesToString(xfa.getBytes()));
775+
return entries;
776+
} catch (_) {
777+
warn("XFA - Invalid utf-8 string.");
778+
return null;
779+
}
780+
}
781+
782+
if (!Array.isArray(xfa) || xfa.length === 0) {
783+
return null;
784+
}
785+
786+
for (let i = 0, ii = xfa.length; i < ii; i += 2) {
787+
let name;
788+
if (i === 0) {
789+
name = "xdp:xdp";
790+
} else if (i === ii - 2) {
791+
name = "/xdp:xdp";
792+
} else {
793+
name = xfa[i];
794+
}
795+
796+
if (!entries.hasOwnProperty(name)) {
797+
continue;
798+
}
799+
const data = this.xref.fetchIfRef(xfa[i + 1]);
800+
if (!isStream(data) || data.isEmpty) {
801+
continue;
802+
}
803+
try {
804+
entries[name] = stringToUTF8String(bytesToString(data.getBytes()));
805+
} catch (_) {
806+
warn("XFA - Invalid utf-8 string.");
807+
return null;
808+
}
809+
}
810+
return entries;
811+
}
812+
813+
get xfaFactory() {
814+
if (
815+
this.pdfManager.enableXfa &&
816+
this.formInfo.hasXfa &&
817+
!this.formInfo.hasAcroForm
818+
) {
819+
const data = this.xfaData;
820+
return shadow(this, "xfaFactory", data ? new XFAFactory(data) : null);
821+
}
822+
return shadow(this, "xfaFaxtory", null);
823+
}
824+
825+
get isPureXfa() {
826+
return this.xfaFactory !== null;
827+
}
828+
735829
get formInfo() {
736830
const formInfo = { hasFields: false, hasAcroForm: false, hasXfa: false };
737831
const acroForm = this.catalog.acroForm;
@@ -918,6 +1012,24 @@ class PDFDocument {
9181012
}
9191013
const { catalog, linearization } = this;
9201014

1015+
if (this.xfaFactory) {
1016+
return Promise.resolve(
1017+
new Page({
1018+
pdfManager: this.pdfManager,
1019+
xref: this.xref,
1020+
pageIndex,
1021+
pageDict: Dict.empty,
1022+
ref: null,
1023+
globalIdFactory: this._globalIdFactory,
1024+
fontCache: catalog.fontCache,
1025+
builtInCMapCache: catalog.builtInCMapCache,
1026+
globalImageCache: catalog.globalImageCache,
1027+
nonBlendModesSet: catalog.nonBlendModesSet,
1028+
xfaFactory: this.xfaFactory,
1029+
})
1030+
);
1031+
}
1032+
9211033
const promise =
9221034
linearization && linearization.pageFirst === pageIndex
9231035
? this._getLinearizationPage(pageIndex)
@@ -935,6 +1047,7 @@ class PDFDocument {
9351047
builtInCMapCache: catalog.builtInCMapCache,
9361048
globalImageCache: catalog.globalImageCache,
9371049
nonBlendModesSet: catalog.nonBlendModesSet,
1050+
xfaFactory: null,
9381051
});
9391052
}));
9401053
}

src/core/pdf_manager.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,14 @@ class BasePdfManager {
106106
}
107107

108108
class LocalPdfManager extends BasePdfManager {
109-
constructor(docId, data, password, evaluatorOptions, docBaseUrl) {
109+
constructor(docId, data, password, evaluatorOptions, enableXfa, docBaseUrl) {
110110
super();
111111

112112
this._docId = docId;
113113
this._password = password;
114114
this._docBaseUrl = docBaseUrl;
115115
this.evaluatorOptions = evaluatorOptions;
116+
this.enableXfa = enableXfa;
116117

117118
const stream = new Stream(data);
118119
this.pdfDocument = new PDFDocument(this, stream);
@@ -141,14 +142,22 @@ class LocalPdfManager extends BasePdfManager {
141142
}
142143

143144
class NetworkPdfManager extends BasePdfManager {
144-
constructor(docId, pdfNetworkStream, args, evaluatorOptions, docBaseUrl) {
145+
constructor(
146+
docId,
147+
pdfNetworkStream,
148+
args,
149+
evaluatorOptions,
150+
enableXfa,
151+
docBaseUrl
152+
) {
145153
super();
146154

147155
this._docId = docId;
148156
this._password = args.password;
149157
this._docBaseUrl = docBaseUrl;
150158
this.msgHandler = args.msgHandler;
151159
this.evaluatorOptions = evaluatorOptions;
160+
this.enableXfa = enableXfa;
152161

153162
this.streamManager = new ChunkedStreamManager(pdfNetworkStream, {
154163
msgHandler: args.msgHandler,

src/core/worker.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,15 @@ class WorkerMessageHandler {
188188
await pdfManager.ensureDoc("checkFirstPage");
189189
}
190190

191-
const [numPages, fingerprint] = await Promise.all([
191+
const [numPages, fingerprint, isPureXfa] = await Promise.all([
192192
pdfManager.ensureDoc("numPages"),
193193
pdfManager.ensureDoc("fingerprint"),
194+
pdfManager.ensureDoc("isPureXfa"),
194195
]);
195-
return { numPages, fingerprint };
196+
return { numPages, fingerprint, isPureXfa };
196197
}
197198

198-
function getPdfManager(data, evaluatorOptions) {
199+
function getPdfManager(data, evaluatorOptions, enableXfa) {
199200
var pdfManagerCapability = createPromiseCapability();
200201
let newPdfManager;
201202

@@ -207,6 +208,7 @@ class WorkerMessageHandler {
207208
source.data,
208209
source.password,
209210
evaluatorOptions,
211+
enableXfa,
210212
docBaseUrl
211213
);
212214
pdfManagerCapability.resolve(newPdfManager);
@@ -246,6 +248,7 @@ class WorkerMessageHandler {
246248
rangeChunkSize: source.rangeChunkSize,
247249
},
248250
evaluatorOptions,
251+
enableXfa,
249252
docBaseUrl
250253
);
251254
// There may be a chance that `newPdfManager` is not initialized for
@@ -277,6 +280,7 @@ class WorkerMessageHandler {
277280
pdfFile,
278281
source.password,
279282
evaluatorOptions,
283+
enableXfa,
280284
docBaseUrl
281285
);
282286
pdfManagerCapability.resolve(newPdfManager);
@@ -399,7 +403,7 @@ class WorkerMessageHandler {
399403
fontExtraProperties: data.fontExtraProperties,
400404
};
401405

402-
getPdfManager(data, evaluatorOptions)
406+
getPdfManager(data, evaluatorOptions, data.enableXfa)
403407
.then(function (newPdfManager) {
404408
if (terminated) {
405409
// We were in a process of setting up the manager, but it got
@@ -487,6 +491,16 @@ class WorkerMessageHandler {
487491
});
488492
});
489493

494+
handler.on("GetPageXfa", function wphSetupGetXfa({ pageIndex }) {
495+
return pdfManager.getPage(pageIndex).then(function (page) {
496+
return page.xfaData;
497+
});
498+
});
499+
500+
handler.on("GetIsPureXfa", function wphSetupGetIsPureXfa(data) {
501+
return pdfManager.ensureDoc("isPureXfa");
502+
});
503+
490504
handler.on("GetOutline", function wphSetupGetOutline(data) {
491505
return pdfManager.ensureCatalog("documentOutline");
492506
});

src/core/xfa/factory.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,27 @@
1313
* limitations under the License.
1414
*/
1515

16+
import { $toHTML } from "./xfa_object.js";
1617
import { Binder } from "./bind.js";
1718
import { XFAParser } from "./parser.js";
1819

1920
class XFAFactory {
2021
constructor(data) {
21-
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
22-
this.form = new Binder(this.root).bind();
22+
try {
23+
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
24+
this.form = new Binder(this.root).bind();
25+
this.pages = this.form[$toHTML]();
26+
} catch (e) {
27+
console.log(e);
28+
}
29+
}
30+
31+
getPage(pageIndex) {
32+
return this.pages.children[pageIndex];
33+
}
34+
35+
get numberPages() {
36+
return this.pages.children.length;
2337
}
2438

2539
static _createDocument(data) {

src/core/xfa/html_utils.js

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* Copyright 2021 Mozilla Foundation
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
const converters = {
17+
pt: x => x,
18+
cm: x => Math.round((x / 2.54) * 72),
19+
mm: x => Math.round((x / (10 * 2.54)) * 72),
20+
in: x => Math.round(x * 72),
21+
};
22+
23+
function measureToString(m) {
24+
const conv = converters[m.unit];
25+
if (conv) {
26+
return `${conv(m.value)}px`;
27+
}
28+
return `${m.value}${m.unit}`;
29+
}
30+
31+
function setWidthHeight(node, style) {
32+
if (node.w) {
33+
style.width = measureToString(node.w);
34+
} else {
35+
if (node.maxW && node.maxW.value > 0) {
36+
style.maxWidth = measureToString(node.maxW);
37+
}
38+
if (node.minW && node.minW.value > 0) {
39+
style.minWidth = measureToString(node.minW);
40+
}
41+
}
42+
43+
if (node.h) {
44+
style.height = measureToString(node.h);
45+
} else {
46+
if (node.maxH && node.maxH.value > 0) {
47+
style.maxHeight = measureToString(node.maxH);
48+
}
49+
if (node.minH && node.minH.value > 0) {
50+
style.minHeight = measureToString(node.minH);
51+
}
52+
}
53+
}
54+
55+
function setPosition(node, style) {
56+
style.transform = "";
57+
if (node.rotate) {
58+
style.transform = `rotate(-${node.rotate}deg) `;
59+
style.transformOrigin = "top left";
60+
}
61+
62+
if (node.x !== "" || node.y !== "") {
63+
style.position = "absolute";
64+
style.left = node.x ? measureToString(node.x) : "0pt";
65+
style.top = node.y ? measureToString(node.y) : "0pt";
66+
}
67+
}
68+
69+
export { measureToString, setPosition, setWidthHeight };

0 commit comments

Comments
 (0)