1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-26 10:08:06 +02:00

XFA - Add a layer to display XFA forms (#13069)

- 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.
This commit is contained in:
calixteman 2021-03-19 10:11:40 +01:00 committed by GitHub
parent a164941351
commit 24e598a895
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 760 additions and 27 deletions

View file

@ -15,6 +15,7 @@
import {
assert,
bytesToString,
FormatError,
info,
InvalidPDFException,
@ -28,6 +29,7 @@ import {
shadow,
stringToBytes,
stringToPDFString,
stringToUTF8String,
unreachable,
Util,
warn,
@ -56,6 +58,7 @@ import { calculateMD5 } from "./crypto.js";
import { Linearization } from "./parser.js";
import { OperatorList } from "./operator_list.js";
import { PartialEvaluator } from "./evaluator.js";
import { XFAFactory } from "./xfa/factory.js";
const DEFAULT_USER_UNIT = 1.0;
const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792];
@ -79,6 +82,7 @@ class Page {
builtInCMapCache,
globalImageCache,
nonBlendModesSet,
xfaFactory,
}) {
this.pdfManager = pdfManager;
this.pageIndex = pageIndex;
@ -91,6 +95,7 @@ class Page {
this.nonBlendModesSet = nonBlendModesSet;
this.evaluatorOptions = pdfManager.evaluatorOptions;
this.resourcesPromise = null;
this.xfaFactory = xfaFactory;
const idCounters = {
obj: 0,
@ -137,6 +142,11 @@ class Page {
}
_getBoundingBox(name) {
if (this.xfaData) {
const { width, height } = this.xfaData.attributes.style;
return [0, 0, parseInt(width), parseInt(height)];
}
const box = this._getInheritableProperty(name, /* getArray = */ true);
if (Array.isArray(box) && box.length === 4) {
@ -231,6 +241,13 @@ class Page {
return stream;
}
get xfaData() {
if (this.xfaFactory) {
return shadow(this, "xfaData", this.xfaFactory.getPage(this.pageIndex));
}
return shadow(this, "xfaData", null);
}
save(handler, task, annotationStorage) {
const partialEvaluator = new PartialEvaluator({
xref: this.xref,
@ -695,6 +712,9 @@ class PDFDocument {
}
get numPages() {
if (this.xfaFactory) {
return shadow(this, "numPages", this.xfaFactory.numberPages);
}
const linearization = this.linearization;
const num = linearization ? linearization.numPages : this.catalog.numPages;
return shadow(this, "numPages", num);
@ -732,6 +752,80 @@ class PDFDocument {
});
}
get xfaData() {
const acroForm = this.catalog.acroForm;
if (!acroForm) {
return null;
}
const xfa = acroForm.get("XFA");
const entries = {
"xdp:xdp": "",
template: "",
datasets: "",
config: "",
connectionSet: "",
localeSet: "",
stylesheet: "",
"/xdp:xdp": "",
};
if (isStream(xfa) && !xfa.isEmpty) {
try {
entries["xdp:xdp"] = stringToUTF8String(bytesToString(xfa.getBytes()));
return entries;
} catch (_) {
warn("XFA - Invalid utf-8 string.");
return null;
}
}
if (!Array.isArray(xfa) || xfa.length === 0) {
return null;
}
for (let i = 0, ii = xfa.length; i < ii; i += 2) {
let name;
if (i === 0) {
name = "xdp:xdp";
} else if (i === ii - 2) {
name = "/xdp:xdp";
} else {
name = xfa[i];
}
if (!entries.hasOwnProperty(name)) {
continue;
}
const data = this.xref.fetchIfRef(xfa[i + 1]);
if (!isStream(data) || data.isEmpty) {
continue;
}
try {
entries[name] = stringToUTF8String(bytesToString(data.getBytes()));
} catch (_) {
warn("XFA - Invalid utf-8 string.");
return null;
}
}
return entries;
}
get xfaFactory() {
if (
this.pdfManager.enableXfa &&
this.formInfo.hasXfa &&
!this.formInfo.hasAcroForm
) {
const data = this.xfaData;
return shadow(this, "xfaFactory", data ? new XFAFactory(data) : null);
}
return shadow(this, "xfaFaxtory", null);
}
get isPureXfa() {
return this.xfaFactory !== null;
}
get formInfo() {
const formInfo = { hasFields: false, hasAcroForm: false, hasXfa: false };
const acroForm = this.catalog.acroForm;
@ -918,6 +1012,24 @@ class PDFDocument {
}
const { catalog, linearization } = this;
if (this.xfaFactory) {
return Promise.resolve(
new Page({
pdfManager: this.pdfManager,
xref: this.xref,
pageIndex,
pageDict: Dict.empty,
ref: null,
globalIdFactory: this._globalIdFactory,
fontCache: catalog.fontCache,
builtInCMapCache: catalog.builtInCMapCache,
globalImageCache: catalog.globalImageCache,
nonBlendModesSet: catalog.nonBlendModesSet,
xfaFactory: this.xfaFactory,
})
);
}
const promise =
linearization && linearization.pageFirst === pageIndex
? this._getLinearizationPage(pageIndex)
@ -935,6 +1047,7 @@ class PDFDocument {
builtInCMapCache: catalog.builtInCMapCache,
globalImageCache: catalog.globalImageCache,
nonBlendModesSet: catalog.nonBlendModesSet,
xfaFactory: null,
});
}));
}

View file

@ -106,13 +106,14 @@ class BasePdfManager {
}
class LocalPdfManager extends BasePdfManager {
constructor(docId, data, password, evaluatorOptions, docBaseUrl) {
constructor(docId, data, password, evaluatorOptions, enableXfa, docBaseUrl) {
super();
this._docId = docId;
this._password = password;
this._docBaseUrl = docBaseUrl;
this.evaluatorOptions = evaluatorOptions;
this.enableXfa = enableXfa;
const stream = new Stream(data);
this.pdfDocument = new PDFDocument(this, stream);
@ -141,7 +142,14 @@ class LocalPdfManager extends BasePdfManager {
}
class NetworkPdfManager extends BasePdfManager {
constructor(docId, pdfNetworkStream, args, evaluatorOptions, docBaseUrl) {
constructor(
docId,
pdfNetworkStream,
args,
evaluatorOptions,
enableXfa,
docBaseUrl
) {
super();
this._docId = docId;
@ -149,6 +157,7 @@ class NetworkPdfManager extends BasePdfManager {
this._docBaseUrl = docBaseUrl;
this.msgHandler = args.msgHandler;
this.evaluatorOptions = evaluatorOptions;
this.enableXfa = enableXfa;
this.streamManager = new ChunkedStreamManager(pdfNetworkStream, {
msgHandler: args.msgHandler,

View file

@ -188,14 +188,15 @@ class WorkerMessageHandler {
await pdfManager.ensureDoc("checkFirstPage");
}
const [numPages, fingerprint] = await Promise.all([
const [numPages, fingerprint, isPureXfa] = await Promise.all([
pdfManager.ensureDoc("numPages"),
pdfManager.ensureDoc("fingerprint"),
pdfManager.ensureDoc("isPureXfa"),
]);
return { numPages, fingerprint };
return { numPages, fingerprint, isPureXfa };
}
function getPdfManager(data, evaluatorOptions) {
function getPdfManager(data, evaluatorOptions, enableXfa) {
var pdfManagerCapability = createPromiseCapability();
let newPdfManager;
@ -207,6 +208,7 @@ class WorkerMessageHandler {
source.data,
source.password,
evaluatorOptions,
enableXfa,
docBaseUrl
);
pdfManagerCapability.resolve(newPdfManager);
@ -246,6 +248,7 @@ class WorkerMessageHandler {
rangeChunkSize: source.rangeChunkSize,
},
evaluatorOptions,
enableXfa,
docBaseUrl
);
// There may be a chance that `newPdfManager` is not initialized for
@ -277,6 +280,7 @@ class WorkerMessageHandler {
pdfFile,
source.password,
evaluatorOptions,
enableXfa,
docBaseUrl
);
pdfManagerCapability.resolve(newPdfManager);
@ -399,7 +403,7 @@ class WorkerMessageHandler {
fontExtraProperties: data.fontExtraProperties,
};
getPdfManager(data, evaluatorOptions)
getPdfManager(data, evaluatorOptions, data.enableXfa)
.then(function (newPdfManager) {
if (terminated) {
// We were in a process of setting up the manager, but it got
@ -487,6 +491,16 @@ class WorkerMessageHandler {
});
});
handler.on("GetPageXfa", function wphSetupGetXfa({ pageIndex }) {
return pdfManager.getPage(pageIndex).then(function (page) {
return pdfManager.ensure(page, "xfaData");
});
});
handler.on("GetIsPureXfa", function wphSetupGetIsPureXfa(data) {
return pdfManager.ensureDoc("isPureXfa");
});
handler.on("GetOutline", function wphSetupGetOutline(data) {
return pdfManager.ensureCatalog("documentOutline");
});

View file

@ -13,13 +13,27 @@
* limitations under the License.
*/
import { $toHTML } from "./xfa_object.js";
import { Binder } from "./bind.js";
import { XFAParser } from "./parser.js";
class XFAFactory {
constructor(data) {
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
this.form = new Binder(this.root).bind();
try {
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
this.form = new Binder(this.root).bind();
this.pages = this.form[$toHTML]();
} catch (e) {
console.log(e);
}
}
getPage(pageIndex) {
return this.pages.children[pageIndex];
}
get numberPages() {
return this.pages.children.length;
}
static _createDocument(data) {

View file

@ -0,0 +1,69 @@
/* 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 converters = {
pt: x => x,
cm: x => Math.round((x / 2.54) * 72),
mm: x => Math.round((x / (10 * 2.54)) * 72),
in: x => Math.round(x * 72),
};
function measureToString(m) {
const conv = converters[m.unit];
if (conv) {
return `${conv(m.value)}px`;
}
return `${m.value}${m.unit}`;
}
function setWidthHeight(node, style) {
if (node.w) {
style.width = measureToString(node.w);
} else {
if (node.maxW && node.maxW.value > 0) {
style.maxWidth = measureToString(node.maxW);
}
if (node.minW && node.minW.value > 0) {
style.minWidth = measureToString(node.minW);
}
}
if (node.h) {
style.height = measureToString(node.h);
} else {
if (node.maxH && node.maxH.value > 0) {
style.maxHeight = measureToString(node.maxH);
}
if (node.minH && node.minH.value > 0) {
style.minHeight = measureToString(node.minH);
}
}
}
function setPosition(node, style) {
style.transform = "";
if (node.rotate) {
style.transform = `rotate(-${node.rotate}deg) `;
style.transformOrigin = "top left";
}
if (node.x !== "" || node.y !== "") {
style.position = "absolute";
style.left = node.x ? measureToString(node.x) : "0pt";
style.top = node.y ? measureToString(node.y) : "0pt";
}
}
export { measureToString, setPosition, setWidthHeight };

View file

@ -15,8 +15,11 @@
import {
$appendChild,
$childrenToHTML,
$content,
$extra,
$finalize,
$getParent,
$hasItem,
$hasSettableValue,
$isTransparent,
@ -26,6 +29,8 @@ import {
$removeChild,
$setSetAttributes,
$setValue,
$toHTML,
$uid,
ContentObject,
Option01,
OptionObject,
@ -45,6 +50,7 @@ import {
getRelevant,
getStringOption,
} from "./utils.js";
import { measureToString, setPosition, setWidthHeight } from "./html_utils.js";
import { warn } from "../../shared/util.js";
const TEMPLATE_NS_ID = NamespaceIds.template.id;
@ -656,6 +662,29 @@ class ContentArea extends XFAObject {
this.desc = null;
this.extras = null;
}
[$toHTML]() {
// TODO: incomplete.
const left = measureToString(this.x);
const top = measureToString(this.y);
const style = {
position: "absolute",
left,
top,
width: measureToString(this.w),
height: measureToString(this.h),
};
return {
name: "div",
children: [],
attributes: {
style,
className: "xfa-contentarea",
id: this[$uid],
},
};
}
}
class Corner extends XFAObject {
@ -1946,6 +1975,41 @@ class PageArea extends XFAObject {
this.field = new XFAObjectArray();
this.subform = new XFAObjectArray();
}
[$toHTML]() {
// TODO: incomplete.
if (this.contentArea.children.length === 0) {
return null;
}
const children = this[$childrenToHTML]({
filter: new Set(["area", "draw", "field", "subform", "contentArea"]),
include: true,
});
// TODO: handle the case where there are several content areas.
const contentArea = children.find(
node => node.attributes.className === "xfa-contentarea"
);
const style = Object.create(null);
if (this.medium && this.medium.short.value && this.medium.long.value) {
style.width = measureToString(this.medium.short);
style.height = measureToString(this.medium.long);
} else {
// TODO: compute it from contentAreas
}
return {
name: "div",
children,
attributes: {
id: this[$uid],
style,
},
contentArea,
};
}
}
class PageSet extends XFAObject {
@ -1970,6 +2034,20 @@ class PageSet extends XFAObject {
this.pageArea = new XFAObjectArray();
this.pageSet = new XFAObjectArray();
}
[$toHTML]() {
// TODO: incomplete.
return {
name: "div",
children: this[$childrenToHTML]({
filter: new Set(["pageArea", "pageSet"]),
include: true,
}),
attributes: {
id: this[$uid],
},
};
}
}
class Para extends XFAObject {
@ -2465,6 +2543,64 @@ class Subform extends XFAObject {
this.subform = new XFAObjectArray();
this.subformSet = new XFAObjectArray();
}
[$toHTML]() {
// TODO: incomplete.
this[$extra] = Object.create(null);
const parent = this[$getParent]();
let page = null;
if (parent[$nodeName] === "template") {
// Root subform: should have page info.
if (this.pageSet !== null) {
this[$extra].pageNumber = 0;
} else {
// TODO
warn("XFA - No pageSet in root subform");
}
} else if (parent[$extra] && parent[$extra].pageNumber !== undefined) {
// This subform is a child of root subform
// so push it in a new page.
const pageNumber = parent[$extra].pageNumber;
const pageAreas = parent.pageSet.pageArea.children;
parent[$extra].pageNumber =
(parent[$extra].pageNumber + 1) % pageAreas.length;
page = pageAreas[pageNumber][$toHTML]();
}
const style = Object.create(null);
setWidthHeight(this, style);
setPosition(this, style);
const attributes = {
style,
id: this[$uid],
};
if (this.name) {
attributes["xfa-name"] = this.name;
}
const children = this[$childrenToHTML]({
// TODO: exObject & exclGroup
filter: new Set(["area", "draw", "field", "subform", "subformSet"]),
include: true,
});
const html = {
name: "div",
attributes,
children,
};
if (page) {
page.contentArea.children.push(html);
delete page.contentArea;
return page;
}
return html;
}
}
class SubformSet extends XFAObject {
@ -2580,8 +2716,32 @@ class Template extends XFAObject {
"interactiveForms",
]);
this.extras = null;
// Spec is unclear:
// A container element that describes a single subform capable of
// enclosing other containers.
// Can we have more than one subform ?
this.subform = new XFAObjectArray();
}
[$finalize]() {
if (this.subform.children.length === 0) {
warn("XFA - No subforms in template node.");
}
if (this.subform.children.length >= 2) {
warn("XFA - Several subforms in template node: please file a bug.");
}
}
[$toHTML]() {
if (this.subform.children.length > 0) {
return this.subform.children[0][$toHTML]();
}
return {
name: "div",
children: [],
};
}
}
class Text extends ContentObject {

View file

@ -74,7 +74,7 @@ function getMeasurement(str, def = "0") {
}
return {
value: sign === "-" ? -value : value,
unit,
unit: unit || "pt",
};
}

View file

@ -20,6 +20,7 @@ import { NamespaceIds } from "./namespaces.js";
// We use these symbols to avoid name conflict between tags
// and properties/methods names.
const $appendChild = Symbol();
const $childrenToHTML = Symbol();
const $clean = Symbol();
const $cleanup = Symbol();
const $clone = Symbol();
@ -27,6 +28,7 @@ const $consumed = Symbol();
const $content = Symbol("content");
const $data = Symbol("data");
const $dump = Symbol();
const $extra = Symbol("extra");
const $finalize = Symbol();
const $getAttributeIt = Symbol();
const $getChildrenByClass = Symbol();
@ -56,6 +58,8 @@ const $setId = Symbol();
const $setSetAttributes = Symbol();
const $setValue = Symbol();
const $text = Symbol();
const $toHTML = Symbol();
const $uid = Symbol("uid");
const _applyPrototype = Symbol();
const _attributes = Symbol();
@ -73,6 +77,8 @@ const _parent = Symbol("parent");
const _setAttributes = Symbol();
const _validator = Symbol();
let uid = 0;
class XFAObject {
constructor(nsId, name, hasChildren = false) {
this[$namespaceId] = nsId;
@ -80,6 +86,7 @@ class XFAObject {
this[_hasChildren] = hasChildren;
this[_parent] = null;
this[_children] = [];
this[$uid] = `${name}${uid++}`;
}
[$onChild](child) {
@ -252,6 +259,23 @@ class XFAObject {
return dumped;
}
[$toHTML]() {
return null;
}
[$childrenToHTML]({ filter = null, include = true }) {
const res = [];
this[$getChildren]().forEach(node => {
if (!filter || include === filter.has(node[$nodeName])) {
const html = node[$toHTML]();
if (html) {
res.push(html);
}
}
});
return res;
}
[$setSetAttributes](attributes) {
if (attributes.use || attributes.id) {
// Just keep set attributes because this node uses a proto or is a proto.
@ -604,6 +628,17 @@ class XmlObject extends XFAObject {
}
}
[$toHTML]() {
if (this[$nodeName] === "#text") {
return {
name: "#text",
value: this[$content],
};
}
return null;
}
[$getChildren](name = null) {
if (!name) {
return this[_children];
@ -766,6 +801,7 @@ class Option10 extends IntegerObject {
export {
$appendChild,
$childrenToHTML,
$clean,
$cleanup,
$clone,
@ -773,6 +809,7 @@ export {
$content,
$data,
$dump,
$extra,
$finalize,
$getAttributeIt,
$getChildren,
@ -801,6 +838,8 @@ export {
$setSetAttributes,
$setValue,
$text,
$toHTML,
$uid,
ContentObject,
IntegerObject,
Option01,