1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

[api-minor] Improve thumbnail handling in documents that contain interactive forms

To improve performance of the sidebar we use the page-canvases to generate the thumbnails whenever possible, since that avoids unnecessary re-rendering when the sidebar is open. This works generally well, however there's an old problem in PDF documents that contain interactive forms (when those are enabled): Note how the thumbnails become partially (or fully) blank, since those Annotations are not included in the OperatorList.[1]

We obviously want to keep using the `PDFThumbnailView.setImage`-method for most documents, however we need a way to skip it only for those pages that contain interactive forms.
As it turns out it's unfortunately not all that simple to tell, after the fact, from looking only at the OperatorList that some Annotations were skipped. While it might have been possible to try and infer that in the viewer, it'd not have been pretty considering that at the time when rendering finishes the annotationLayer has not yet been built.
The overall simplest solution that I could come up with, was instead to include a *summary* of the interactive form-state when doing the final "flushing" of the OperatorList and expose that information in the API.

---
[1] Some examples from our test-suite: `annotation-tx2.pdf` where the thumbnail is completely blank, and `bug1737260.pdf` where the thumbnail is missing the "buttons" found on the page.
This commit is contained in:
Jonas Jenwald 2022-06-27 11:41:37 +02:00
parent c7b71a3376
commit 0c31320c12
7 changed files with 185 additions and 101 deletions

View file

@ -881,7 +881,11 @@ class Annotation {
);
if (!appearance) {
if (!isUsingOwnCanvas) {
return new OperatorList();
return {
opList: new OperatorList(),
separateForm: false,
separateCanvas: false,
};
}
appearance = new StringStream("");
appearance.dict = new Dict();
@ -930,7 +934,7 @@ class Annotation {
opList.addOp(OPS.endMarkedContent, []);
}
this.reset();
return opList;
return { opList, separateForm: false, separateCanvas: isUsingOwnCanvas };
}
async save(evaluator, task, annotationStorage) {
@ -1619,7 +1623,11 @@ class WidgetAnnotation extends Annotation {
// Do not render form elements on the canvas when interactive forms are
// enabled. The display layer is responsible for rendering them instead.
if (renderForms && !(this instanceof SignatureWidgetAnnotation)) {
return new OperatorList();
return {
opList: new OperatorList(),
separateForm: true,
separateCanvas: false,
};
}
if (!this._hasText) {
@ -1647,12 +1655,12 @@ class WidgetAnnotation extends Annotation {
);
}
const operatorList = new OperatorList();
const opList = new OperatorList();
// Even if there is an appearance stream, ignore it. This is the
// behaviour used by Adobe Reader.
if (!this._defaultAppearance || content === null) {
return operatorList;
return { opList, separateForm: false, separateCanvas: false };
}
const matrix = [1, 0, 0, 1, 0, 0];
@ -1672,10 +1680,10 @@ class WidgetAnnotation extends Annotation {
);
}
if (optionalContent !== undefined) {
operatorList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
opList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
}
operatorList.addOp(OPS.beginAnnotation, [
opList.addOp(OPS.beginAnnotation, [
this.data.id,
this.data.rect,
transform,
@ -1688,14 +1696,14 @@ class WidgetAnnotation extends Annotation {
stream,
task,
resources: this._fieldResources.mergedResources,
operatorList,
operatorList: opList,
});
operatorList.addOp(OPS.endAnnotation, []);
opList.addOp(OPS.endAnnotation, []);
if (optionalContent !== undefined) {
operatorList.addOp(OPS.endMarkedContent, []);
opList.addOp(OPS.endMarkedContent, []);
}
return operatorList;
return { opList, separateForm: false, separateCanvas: false };
}
_getMKDict(rotation) {
@ -2477,7 +2485,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
}
// No appearance
return new OperatorList();
return {
opList: new OperatorList(),
separateForm: false,
separateCanvas: false,
};
}
async save(evaluator, task, annotationStorage) {

View file

@ -455,7 +455,7 @@ class Page {
annotations.length === 0 ||
intent & RenderingIntentFlag.ANNOTATIONS_DISABLE
) {
pageOpList.flush(true);
pageOpList.flush(/* lastChunk = */ true);
return { length: pageOpList.totalLength };
}
const renderForms = !!(intent & RenderingIntentFlag.ANNOTATIONS_FORMS),
@ -493,10 +493,23 @@ class Page {
}
return Promise.all(opListPromises).then(function (opLists) {
for (const opList of opLists) {
let form = false,
canvas = false;
for (const { opList, separateForm, separateCanvas } of opLists) {
pageOpList.addOpList(opList);
if (separateForm) {
form = separateForm;
}
if (separateCanvas) {
canvas = separateCanvas;
}
}
pageOpList.flush(true);
pageOpList.flush(
/* lastChunk = */ true,
/* separateAnnots = */ { form, canvas }
);
return { length: pageOpList.totalLength };
});
});

View file

@ -690,7 +690,7 @@ class OperatorList {
return transfers;
}
flush(lastChunk = false) {
flush(lastChunk = false, separateAnnots = null) {
this.optimizer.flush();
const length = this.length;
this._totalLength += length;
@ -700,6 +700,7 @@ class OperatorList {
fnArray: this.fnArray,
argsArray: this.argsArray,
lastChunk,
separateAnnots,
length,
},
1,

View file

@ -1477,6 +1477,7 @@ class PDFPageProxy {
fnArray: [],
argsArray: [],
lastChunk: false,
separateAnnots: null,
};
if (this._stats) {
@ -1599,6 +1600,7 @@ class PDFPageProxy {
fnArray: [],
argsArray: [],
lastChunk: false,
separateAnnots: null,
};
if (this._stats) {
@ -1795,6 +1797,7 @@ class PDFPageProxy {
intentState.operatorList.argsArray.push(operatorListChunk.argsArray[i]);
}
intentState.operatorList.lastChunk = operatorListChunk.lastChunk;
intentState.operatorList.separateAnnots = operatorListChunk.separateAnnots;
// Notify all the rendering tasks there are more operators to be consumed.
for (const internalRenderTask of intentState.renderTasks) {
@ -3194,8 +3197,10 @@ class PDFObjects {
* Allows controlling of the rendering tasks.
*/
class RenderTask {
#internalRenderTask = null;
constructor(internalRenderTask) {
this._internalRenderTask = internalRenderTask;
this.#internalRenderTask = internalRenderTask;
/**
* Callback for incremental rendering -- a function that will be called
@ -3211,7 +3216,7 @@ class RenderTask {
* @type {Promise<void>}
*/
get promise() {
return this._internalRenderTask.capability.promise;
return this.#internalRenderTask.capability.promise;
}
/**
@ -3220,7 +3225,23 @@ class RenderTask {
* this object extends will be rejected when cancelled.
*/
cancel() {
this._internalRenderTask.cancel();
this.#internalRenderTask.cancel();
}
/**
* Whether form fields are rendered separately from the main operatorList.
* @type {boolean}
*/
get separateAnnots() {
const { separateAnnots } = this.#internalRenderTask.operatorList;
if (!separateAnnots) {
return false;
}
const { annotationCanvasMap } = this.#internalRenderTask;
return (
separateAnnots.form ||
(separateAnnots.canvas && annotationCanvasMap?.size > 0)
);
}
}