1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-20 15:18:08 +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

@ -518,6 +518,7 @@ const PDFViewerApplication = {
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
enableScripting: AppOptions.get("enableScripting"),
enableXfa: AppOptions.get("enableXfa"),
});
pdfRenderingQueue.setViewer(this.pdfViewer);
pdfLinkService.setViewer(this.pdfViewer);

View file

@ -205,6 +205,11 @@ const defaultOptions = {
value: "",
kind: OptionKind.API,
},
enableXfa: {
/** @type {boolean} */
value: false,
kind: OptionKind.API,
},
fontExtraProperties: {
/** @type {boolean} */
value: false,

View file

@ -42,6 +42,7 @@ import { NullL10n } from "./l10n_utils.js";
import { PDFPageView } from "./pdf_page_view.js";
import { SimpleLinkService } from "./pdf_link_service.js";
import { TextLayerBuilder } from "./text_layer_builder.js";
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
const DEFAULT_CACHE_SIZE = 10;
@ -478,6 +479,7 @@ class BaseViewer {
if (!pdfDocument) {
return;
}
const isPureXfa = pdfDocument.isPureXfa;
const pagesCount = pdfDocument.numPages;
const firstPagePromise = pdfDocument.getPage(1);
// Rendering (potentially) depends on this, hence fetching it immediately.
@ -523,6 +525,7 @@ class BaseViewer {
const viewport = firstPdfPage.getViewport({ scale: scale * CSS_UNITS });
const textLayerFactory =
this.textLayerMode !== TextLayerMode.DISABLE ? this : null;
const xfaLayerFactory = isPureXfa ? this : null;
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
const pageView = new PDFPageView({
@ -536,6 +539,7 @@ class BaseViewer {
textLayerFactory,
textLayerMode: this.textLayerMode,
annotationLayerFactory: this,
xfaLayerFactory,
imageResourcesPath: this.imageResourcesPath,
renderInteractiveForms: this.renderInteractiveForms,
renderer: this.renderer,
@ -1308,6 +1312,18 @@ class BaseViewer {
});
}
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPage} pdfPage
* @returns {XfaLayerBuilder}
*/
createXfaLayerBuilder(pageDiv, pdfPage) {
return new XfaLayerBuilder({
pageDiv,
pdfPage,
});
}
/**
* @type {boolean} Whether all pages of the PDF document have identical
* widths and heights.

View file

@ -204,6 +204,18 @@ class IPDFAnnotationLayerFactory {
) {}
}
/**
* @interface
*/
class IPDFXfaLayerFactory {
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPage} pdfPage
* @returns {XfaLayerBuilder}
*/
createXfaLayerBuilder(pageDiv, pdfPage) {}
}
/**
* @interface
*/
@ -243,5 +255,6 @@ export {
IPDFHistory,
IPDFLinkService,
IPDFTextLayerFactory,
IPDFXfaLayerFactory,
IRenderableView,
};

View file

@ -48,6 +48,7 @@ import { viewerCompatibilityParams } from "./viewer_compatibility.js";
* behaviour is enabled. The constants from {TextLayerMode} should be used.
* The default value is `TextLayerMode.ENABLE`.
* @property {IPDFAnnotationLayerFactory} annotationLayerFactory
* @property {IPDFXfaLayerFactory} xfaLayerFactory
* @property {string} [imageResourcesPath] - Path for image resources, mainly
* for annotation icons. Include trailing slash.
* @property {boolean} renderInteractiveForms - Turns on rendering of
@ -102,6 +103,7 @@ class PDFPageView {
this.renderingQueue = options.renderingQueue;
this.textLayerFactory = options.textLayerFactory;
this.annotationLayerFactory = options.annotationLayerFactory;
this.xfaLayerFactory = options.xfaLayerFactory;
this.renderer = options.renderer || RendererType.CANVAS;
this.enableWebGL = options.enableWebGL || false;
this.l10n = options.l10n || NullL10n;
@ -116,6 +118,7 @@ class PDFPageView {
this.annotationLayer = null;
this.textLayer = null;
this.zoomLayer = null;
this.xfaLayer = null;
const div = document.createElement("div");
div.className = "page";
@ -164,6 +167,24 @@ class PDFPageView {
}
}
/**
* @private
*/
async _renderXfaLayer() {
let error = null;
try {
await this.xfaLayer.render(this.viewport, "display");
} catch (ex) {
error = ex;
} finally {
this.eventBus.dispatch("xfalayerrendered", {
source: this,
pageNumber: this.id,
error,
});
}
}
/**
* @private
*/
@ -197,9 +218,14 @@ class PDFPageView {
const currentZoomLayerNode = (keepZoomLayer && this.zoomLayer) || null;
const currentAnnotationNode =
(keepAnnotations && this.annotationLayer?.div) || null;
const currentXfaLayerNode = this.xfaLayer?.div || null;
for (let i = childNodes.length - 1; i >= 0; i--) {
const node = childNodes[i];
if (currentZoomLayerNode === node || currentAnnotationNode === node) {
if (
currentZoomLayerNode === node ||
currentAnnotationNode === node ||
currentXfaLayerNode === node
) {
continue;
}
div.removeChild(node);
@ -393,6 +419,10 @@ class PDFPageView {
if (redrawAnnotations && this.annotationLayer) {
this._renderAnnotationLayer();
}
if (this.xfaLayer) {
this._renderXfaLayer();
}
}
get width() {
@ -553,6 +583,17 @@ class PDFPageView {
}
this._renderAnnotationLayer();
}
if (this.xfaLayerFactory) {
if (!this.xfaLayer) {
this.xfaLayer = this.xfaLayerFactory.createXfaLayerBuilder(
div,
pdfPage
);
}
this._renderXfaLayer();
}
div.setAttribute("data-loaded", true);
this.eventBus.dispatch("pagerender", {

View file

@ -14,6 +14,7 @@
*/
@import url(text_layer_builder.css);
@import url(annotation_layer_builder.css);
@import url(xfa_layer_builder.css);
.pdfViewer .canvasWrapper {
overflow: hidden;

22
web/xfa_layer_builder.css Normal file
View file

@ -0,0 +1,22 @@
*/* 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.
*/
.xfaLayer {
position: absolute;
top: 0;
left: 0;
z-index: 200;
transform-origin: 0 0;
}

97
web/xfa_layer_builder.js Normal file
View file

@ -0,0 +1,97 @@
/* 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.
*/
import { XfaLayer } from "pdfjs-lib";
/**
* @typedef {Object} XfaLayerBuilderOptions
* @property {HTMLDivElement} pageDiv
* @property {PDFPage} pdfPage
*/
class XfaLayerBuilder {
/**
* @param {XfaLayerBuilderOptions} options
*/
constructor({ pageDiv, pdfPage }) {
this.pageDiv = pageDiv;
this.pdfPage = pdfPage;
this.div = null;
this._cancelled = false;
}
/**
* @param {PageViewport} viewport
* @param {string} intent (default value is 'display')
* @returns {Promise<void>} A promise that is resolved when rendering of the
* annotations is complete.
*/
render(viewport, intent = "display") {
return this.pdfPage.getXfa().then(xfa => {
if (this._cancelled) {
return;
}
const parameters = {
viewport: viewport.clone({ dontFlip: true }),
div: this.div,
xfa,
page: this.pdfPage,
};
if (this.div) {
XfaLayer.update(parameters);
} else {
// Create an xfa layer div and render the form
this.div = document.createElement("div");
this.div.className = "xfaLayer";
this.pageDiv.appendChild(this.div);
parameters.div = this.div;
XfaLayer.render(parameters);
}
});
}
cancel() {
this._cancelled = true;
}
hide() {
if (!this.div) {
return;
}
this.div.hidden = true;
}
}
/**
* @implements IPDFXfaLayerFactory
*/
class DefaultXfaLayerFactory {
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPage} pdfPage
*/
createXfaLayerBuilder(pageDiv, pdfPage) {
return new XfaLayerBuilder({
pageDiv,
pdfPage,
});
}
}
export { DefaultXfaLayerFactory, XfaLayerBuilder };