From 03cf28bf171c0f62150204bdc4f930aa083cff6f Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 10 Jul 2021 16:47:39 +0200 Subject: [PATCH] [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). --- src/core/annotation.js | 9 +++++- src/core/document.js | 8 ++++-- src/core/operator_list.js | 4 +-- src/display/api.js | 36 ++++++++++++++++++------ src/display/canvas.js | 2 +- test/unit/annotation_spec.js | 54 ++++++++++++++++++++++++++++++++++++ test/unit/api_spec.js | 25 +++++++++++++++-- 7 files changed, 121 insertions(+), 17 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 211052c32..a0b58fefa 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -702,7 +702,13 @@ class Annotation { return resourcesPromise.then(resources => { const opList = new OperatorList(); - opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]); + opList.addOp(OPS.beginAnnotation, [ + data.id, + data.rect, + transform, + matrix, + ]); + return evaluator .getOperatorList({ stream: appearance, @@ -1307,6 +1313,7 @@ class WidgetAnnotation extends Annotation { const transform = getTransformMatrix(this.data.rect, bbox, matrix); operatorList.addOp(OPS.beginAnnotation, [ + this.data.id, this.data.rect, transform, matrix, diff --git a/src/core/document.js b/src/core/document.js index 9143e8903..c6a1c6e1e 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -366,12 +366,16 @@ class Page { // Collect the operator list promises for the annotations. Each promise // is resolved with the complete operator list for a single annotation. + const annotationIntent = intent.startsWith("oplist-") + ? intent.split("-")[1] + : intent; const opListPromises = []; for (const annotation of annotations) { if ( - (intent === "display" && + (annotationIntent === "display" && annotation.mustBeViewed(annotationStorage)) || - (intent === "print" && annotation.mustBePrinted(annotationStorage)) + (annotationIntent === "print" && + annotation.mustBePrinted(annotationStorage)) ) { opListPromises.push( annotation diff --git a/src/core/operator_list.js b/src/core/operator_list.js index 3dfc2b0de..e335fdfe4 100644 --- a/src/core/operator_list.js +++ b/src/core/operator_list.js @@ -594,14 +594,14 @@ class OperatorList { // Close to chunk size. 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) { this._streamSink = streamSink; this.fnArray = []; this.argsArray = []; - if (streamSink && intent !== "oplist") { + if (streamSink && !(intent && intent.startsWith("oplist-"))) { this.optimizer = new QueueOptimizer(this); } else { this.optimizer = new NullOptimizer(this); diff --git a/src/display/api.js b/src/display/api.js index d17e96368..45f40df3a 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1149,7 +1149,7 @@ class PDFDocumentProxy { * Page annotation parameters. * * @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 * annotations). If the parameter is omitted, all annotations are fetched. */ @@ -1187,6 +1187,14 @@ class PDFDocumentProxy { * 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". * @@ -1299,12 +1307,18 @@ class PDFPageProxy { * {Array} of the annotation objects. */ 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._pageIndex, - intent + renderingIntent ); - this._annotationsIntent = intent; + this._annotationsIntent = renderingIntent; } return this._annotationsPromise; } @@ -1332,7 +1346,7 @@ class PDFPageProxy { /** * 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 * resolved when the page finishes rendering. */ @@ -1473,10 +1487,12 @@ class PDFPageProxy { } /** + * @param {GetOperatorListParameters} params - Page getOperatorList + * parameters. * @returns {Promise} 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() { if (intentState.operatorList.lastChunk) { 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); if (!intentState) { intentState = Object.create(null); @@ -1600,7 +1618,7 @@ class PDFPageProxy { force: true, }); - if (intent === "oplist") { + if (intent.startsWith("oplist-")) { // Avoid errors below, since the renderTasks are just stubs. continue; } diff --git a/src/display/canvas.js b/src/display/canvas.js index 44039eea1..6fa9338ea 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -2401,7 +2401,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { this.restore(); } - beginAnnotation(rect, transform, matrix) { + beginAnnotation(id, rect, transform, matrix) { this.save(); resetCtxToDefault(this.ctx); this.current = new CanvasExtraState(); diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index a6f4a808c..6cca893fd 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1689,6 +1689,12 @@ describe("annotation", function () { OPS.setFillRGBColor, 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( new Uint8ClampedArray([26, 51, 76]) ); @@ -2324,6 +2330,12 @@ describe("annotation", function () { OPS.showText, 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"); }); @@ -2375,6 +2387,12 @@ describe("annotation", function () { OPS.setFillRGBColor, 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( new Uint8ClampedArray([26, 51, 76]) ); @@ -2393,6 +2411,12 @@ describe("annotation", function () { OPS.setFillRGBColor, 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( new Uint8ClampedArray([76, 51, 26]) ); @@ -2449,6 +2473,12 @@ describe("annotation", function () { OPS.setFillRGBColor, 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( new Uint8ClampedArray([26, 51, 76]) ); @@ -2503,6 +2533,12 @@ describe("annotation", function () { OPS.setFillRGBColor, 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( new Uint8ClampedArray([26, 51, 76]) ); @@ -2700,6 +2736,12 @@ describe("annotation", function () { OPS.setFillRGBColor, 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( new Uint8ClampedArray([26, 51, 76]) ); @@ -2718,6 +2760,12 @@ describe("annotation", function () { OPS.setFillRGBColor, 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( new Uint8ClampedArray([76, 51, 26]) ); @@ -2772,6 +2820,12 @@ describe("annotation", function () { OPS.setFillRGBColor, 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( new Uint8ClampedArray([76, 51, 26]) ); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index cd03229b1..7f4c9d389 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1622,8 +1622,9 @@ describe("api", function () { it("gets operator list", async function () { 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); }); @@ -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 () { const stats = await page.getOperatorList().then(function () { return pdfDocument.getStats();