[api-minor] Add intent support to the PDFPageProxy.getOperatorList method (issue 13704)

With this patch, the `PDFPageProxy.getOperatorList` method will now return `PDFOperatorList`-instances that also include Annotation-operatorLists (when those exist). Hence this closes a small, but potentially confusing, gap between the `render` and `getOperatorList` methods.

Previously we've been somewhat reluctant to do this, as explained below, but given that there's actual use-cases where it's required probably means that we'll *have* to implement it now.
Since we still need the ability to separate "normal" rendering operations from direct `getOperatorList` calls in the worker-thread, this API-change unfortunately causes the *internal* renderingIntent to become a bit "messy" which is indeed unfortunate (note the `"oplist-"` strings in various spots). As-is I suppose that it's not all that bad, but we may want to consider changing the *internal* renderingIntent to e.g. a bitfield in the future.

Besides fixing issue 13704, this patch would also be necessary if someone ever tries to implement e.g. issue 10165 (since currently `PDFPageProxy.getOperatorList` doesn't include Annotation-operatorLists).

*Please note:* This patch is *also* tagged "api-minor" for a second reason, which is that we're now including the Annotation-id in the `beginAnnotation` argument. The reason for this is to allow correlating the Annotation-data returned by `PDFPageProxy.getAnnotations`, with its corresponding operatorList-data (for those Annotations that have it).
This commit is contained in:
Jonas Jenwald 2021-07-10 16:47:39 +02:00
parent f1ae7d7b0e
commit 03cf28bf17
7 changed files with 121 additions and 17 deletions

View File

@ -702,7 +702,13 @@ class Annotation {
return resourcesPromise.then(resources => { return resourcesPromise.then(resources => {
const opList = new OperatorList(); const opList = new OperatorList();
opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]); opList.addOp(OPS.beginAnnotation, [
data.id,
data.rect,
transform,
matrix,
]);
return evaluator return evaluator
.getOperatorList({ .getOperatorList({
stream: appearance, stream: appearance,
@ -1307,6 +1313,7 @@ class WidgetAnnotation extends Annotation {
const transform = getTransformMatrix(this.data.rect, bbox, matrix); const transform = getTransformMatrix(this.data.rect, bbox, matrix);
operatorList.addOp(OPS.beginAnnotation, [ operatorList.addOp(OPS.beginAnnotation, [
this.data.id,
this.data.rect, this.data.rect,
transform, transform,
matrix, matrix,

View File

@ -366,12 +366,16 @@ class Page {
// Collect the operator list promises for the annotations. Each promise // Collect the operator list promises for the annotations. Each promise
// is resolved with the complete operator list for a single annotation. // is resolved with the complete operator list for a single annotation.
const annotationIntent = intent.startsWith("oplist-")
? intent.split("-")[1]
: intent;
const opListPromises = []; const opListPromises = [];
for (const annotation of annotations) { for (const annotation of annotations) {
if ( if (
(intent === "display" && (annotationIntent === "display" &&
annotation.mustBeViewed(annotationStorage)) || annotation.mustBeViewed(annotationStorage)) ||
(intent === "print" && annotation.mustBePrinted(annotationStorage)) (annotationIntent === "print" &&
annotation.mustBePrinted(annotationStorage))
) { ) {
opListPromises.push( opListPromises.push(
annotation annotation

View File

@ -594,14 +594,14 @@ class OperatorList {
// Close to chunk size. // Close to chunk size.
static get CHUNK_SIZE_ABOUT() { static get CHUNK_SIZE_ABOUT() {
return shadow(this, "CHUNK_SIZE_ABOUT", OperatorList.CHUNK_SIZE - 5); return shadow(this, "CHUNK_SIZE_ABOUT", this.CHUNK_SIZE - 5);
} }
constructor(intent, streamSink) { constructor(intent, streamSink) {
this._streamSink = streamSink; this._streamSink = streamSink;
this.fnArray = []; this.fnArray = [];
this.argsArray = []; this.argsArray = [];
if (streamSink && intent !== "oplist") { if (streamSink && !(intent && intent.startsWith("oplist-"))) {
this.optimizer = new QueueOptimizer(this); this.optimizer = new QueueOptimizer(this);
} else { } else {
this.optimizer = new NullOptimizer(this); this.optimizer = new NullOptimizer(this);

View File

@ -1149,7 +1149,7 @@ class PDFDocumentProxy {
* Page annotation parameters. * Page annotation parameters.
* *
* @typedef {Object} GetAnnotationsParameters * @typedef {Object} GetAnnotationsParameters
* @property {string} intent - Determines the annotations that will be fetched, * @property {string} [intent] - Determines the annotations that are fetched,
* can be either 'display' (viewable annotations) or 'print' (printable * can be either 'display' (viewable annotations) or 'print' (printable
* annotations). If the parameter is omitted, all annotations are fetched. * annotations). If the parameter is omitted, all annotations are fetched.
*/ */
@ -1187,6 +1187,14 @@ class PDFDocumentProxy {
* states set. * states set.
*/ */
/**
* Page getOperatorList parameters.
*
* @typedef {Object} GetOperatorListParameters
* @property {string} [intent] - Rendering intent, can be 'display' or 'print'.
* The default value is 'display'.
*/
/** /**
* Structure tree node. The root node will have a role "Root". * Structure tree node. The root node will have a role "Root".
* *
@ -1299,12 +1307,18 @@ class PDFPageProxy {
* {Array} of the annotation objects. * {Array} of the annotation objects.
*/ */
getAnnotations({ intent = null } = {}) { getAnnotations({ intent = null } = {}) {
if (!this._annotationsPromise || this._annotationsIntent !== intent) { const renderingIntent =
intent === "display" || intent === "print" ? intent : null;
if (
!this._annotationsPromise ||
this._annotationsIntent !== renderingIntent
) {
this._annotationsPromise = this._transport.getAnnotations( this._annotationsPromise = this._transport.getAnnotations(
this._pageIndex, this._pageIndex,
intent renderingIntent
); );
this._annotationsIntent = intent; this._annotationsIntent = renderingIntent;
} }
return this._annotationsPromise; return this._annotationsPromise;
} }
@ -1332,7 +1346,7 @@ class PDFPageProxy {
/** /**
* Begins the process of rendering a page to the desired context. * Begins the process of rendering a page to the desired context.
* *
* @param {RenderParameters} params Page render parameters. * @param {RenderParameters} params - Page render parameters.
* @returns {RenderTask} An object that contains a promise that is * @returns {RenderTask} An object that contains a promise that is
* resolved when the page finishes rendering. * resolved when the page finishes rendering.
*/ */
@ -1473,10 +1487,12 @@ class PDFPageProxy {
} }
/** /**
* @param {GetOperatorListParameters} params - Page getOperatorList
* parameters.
* @returns {Promise<PDFOperatorList>} A promise resolved with an * @returns {Promise<PDFOperatorList>} A promise resolved with an
* {@link PDFOperatorList} object that represents page's operator list. * {@link PDFOperatorList} object that represents the page's operator list.
*/ */
getOperatorList() { getOperatorList({ intent = "display" } = {}) {
function operatorListChanged() { function operatorListChanged() {
if (intentState.operatorList.lastChunk) { if (intentState.operatorList.lastChunk) {
intentState.opListReadCapability.resolve(intentState.operatorList); intentState.opListReadCapability.resolve(intentState.operatorList);
@ -1485,7 +1501,9 @@ class PDFPageProxy {
} }
} }
const renderingIntent = "oplist"; const renderingIntent = `oplist-${
intent === "print" ? "print" : "display"
}`;
let intentState = this._intentStates.get(renderingIntent); let intentState = this._intentStates.get(renderingIntent);
if (!intentState) { if (!intentState) {
intentState = Object.create(null); intentState = Object.create(null);
@ -1600,7 +1618,7 @@ class PDFPageProxy {
force: true, force: true,
}); });
if (intent === "oplist") { if (intent.startsWith("oplist-")) {
// Avoid errors below, since the renderTasks are just stubs. // Avoid errors below, since the renderTasks are just stubs.
continue; continue;
} }

View File

@ -2401,7 +2401,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
this.restore(); this.restore();
} }
beginAnnotation(rect, transform, matrix) { beginAnnotation(id, rect, transform, matrix) {
this.save(); this.save();
resetCtxToDefault(this.ctx); resetCtxToDefault(this.ctx);
this.current = new CanvasExtraState(); this.current = new CanvasExtraState();

View File

@ -1689,6 +1689,12 @@ describe("annotation", function () {
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
expect(operatorList.argsArray[0]).toEqual([
"271R",
[0, 0, 32, 10],
[32, 0, 0, 10, 0, 0],
[1, 0, 0, 1, 0, 0],
]);
expect(operatorList.argsArray[1]).toEqual( expect(operatorList.argsArray[1]).toEqual(
new Uint8ClampedArray([26, 51, 76]) new Uint8ClampedArray([26, 51, 76])
); );
@ -2324,6 +2330,12 @@ describe("annotation", function () {
OPS.showText, OPS.showText,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
expect(operatorList.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0],
]);
expect(operatorList.argsArray[3][0][0].unicode).toEqual("4"); expect(operatorList.argsArray[3][0][0].unicode).toEqual("4");
}); });
@ -2375,6 +2387,12 @@ describe("annotation", function () {
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
expect(operatorList.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0],
]);
expect(operatorList.argsArray[1]).toEqual( expect(operatorList.argsArray[1]).toEqual(
new Uint8ClampedArray([26, 51, 76]) new Uint8ClampedArray([26, 51, 76])
); );
@ -2393,6 +2411,12 @@ describe("annotation", function () {
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
expect(operatorList.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0],
]);
expect(operatorList.argsArray[1]).toEqual( expect(operatorList.argsArray[1]).toEqual(
new Uint8ClampedArray([76, 51, 26]) new Uint8ClampedArray([76, 51, 26])
); );
@ -2449,6 +2473,12 @@ describe("annotation", function () {
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
expect(operatorList.argsArray[0]).toEqual([
"1249R",
[0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0],
]);
expect(operatorList.argsArray[1]).toEqual( expect(operatorList.argsArray[1]).toEqual(
new Uint8ClampedArray([26, 51, 76]) new Uint8ClampedArray([26, 51, 76])
); );
@ -2503,6 +2533,12 @@ describe("annotation", function () {
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
expect(operatorList.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0],
]);
expect(operatorList.argsArray[1]).toEqual( expect(operatorList.argsArray[1]).toEqual(
new Uint8ClampedArray([26, 51, 76]) new Uint8ClampedArray([26, 51, 76])
); );
@ -2700,6 +2736,12 @@ describe("annotation", function () {
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
expect(operatorList.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0],
]);
expect(operatorList.argsArray[1]).toEqual( expect(operatorList.argsArray[1]).toEqual(
new Uint8ClampedArray([26, 51, 76]) new Uint8ClampedArray([26, 51, 76])
); );
@ -2718,6 +2760,12 @@ describe("annotation", function () {
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
expect(operatorList.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0],
]);
expect(operatorList.argsArray[1]).toEqual( expect(operatorList.argsArray[1]).toEqual(
new Uint8ClampedArray([76, 51, 26]) new Uint8ClampedArray([76, 51, 26])
); );
@ -2772,6 +2820,12 @@ describe("annotation", function () {
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
expect(operatorList.argsArray[0]).toEqual([
"124R",
[0, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0],
]);
expect(operatorList.argsArray[1]).toEqual( expect(operatorList.argsArray[1]).toEqual(
new Uint8ClampedArray([76, 51, 26]) new Uint8ClampedArray([76, 51, 26])
); );

View File

@ -1622,8 +1622,9 @@ describe("api", function () {
it("gets operator list", async function () { it("gets operator list", async function () {
const operatorList = await page.getOperatorList(); const operatorList = await page.getOperatorList();
expect(!!operatorList.fnArray).toEqual(true);
expect(!!operatorList.argsArray).toEqual(true); expect(operatorList.fnArray.length).toBeGreaterThan(100);
expect(operatorList.argsArray.length).toBeGreaterThan(100);
expect(operatorList.lastChunk).toEqual(true); expect(operatorList.lastChunk).toEqual(true);
}); });
@ -1687,6 +1688,26 @@ describe("api", function () {
} }
); );
it("gets operator list, containing Annotation-operatorLists", async function () {
const loadingTask = getDocument(
buildGetDocumentParams("annotation-line.pdf")
);
const pdfDoc = await loadingTask.promise;
const pdfPage = await pdfDoc.getPage(1);
const operatorList = await pdfPage.getOperatorList();
expect(operatorList.fnArray.length).toBeGreaterThan(20);
expect(operatorList.argsArray.length).toBeGreaterThan(20);
expect(operatorList.lastChunk).toEqual(true);
// The `getOperatorList` method, similar to the `render` method,
// is supposed to include any existing Annotation-operatorLists.
expect(operatorList.fnArray.includes(OPS.beginAnnotation)).toEqual(true);
expect(operatorList.fnArray.includes(OPS.endAnnotation)).toEqual(true);
await loadingTask.destroy();
});
it("gets document stats after parsing page", async function () { it("gets document stats after parsing page", async function () {
const stats = await page.getOperatorList().then(function () { const stats = await page.getOperatorList().then(function () {
return pdfDocument.getStats(); return pdfDocument.getStats();