From 3c78ff5fb06c2f3285ece6c0a3d1b521d7d5d9f0 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 25 Feb 2024 14:12:36 +0100 Subject: [PATCH] [api-minor] Implement basic support for OptionalContent `Usage` dicts (issue 5764, bug 1826783) The following are some highlights of this patch: - In the Worker we only extract a *subset* of the potential contents of the `Usage` dictionary, to avoid having to implement/test a bunch of code that'd be completely unused in the viewer. - In order to still allow the user to *manually* override the default visible layers in the viewer, the viewable/printable state is purposely *not* enforced during initialization in the `OptionalContentConfig` constructor. - Printing will now always use the *default* visible layers, rather than using the same state as the viewer (as was the case previously). This ensures that the printing-output will correctly take the `Usage` dictionary into account, and in practice toggling of visible layers rarely seem to be necessary except in the viewer itself (if at all).[1] --- [1] In the unlikely case that it'd ever be deemed necessary to support fine-grained control of optional content visibility during printing, some new (additional) UI would likely be needed to support that case. --- src/core/catalog.js | 76 +++++++++++++++++++++----- src/display/api.js | 55 ++++++++++++------- src/display/optional_content_config.js | 46 +++++++++++++--- test/driver.js | 4 +- test/pdfs/bug1826783.pdf.link | 1 + test/test_manifest.json | 17 ++++++ web/app.js | 1 - web/firefox_print_service.js | 6 +- web/firefoxcom.js | 8 +-- web/pdf_layer_viewer.js | 2 +- web/pdf_print_service.js | 22 ++++++-- web/pdf_thumbnail_viewer.js | 4 +- web/pdf_viewer.js | 6 +- 13 files changed, 186 insertions(+), 62 deletions(-) create mode 100644 test/pdfs/bug1826783.pdf.link diff --git a/src/core/catalog.js b/src/core/catalog.js index c308a87f3..7081d1c53 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -445,20 +445,10 @@ class Catalog { continue; } groupRefs.put(groupRef); - const group = this.xref.fetch(groupRef); - groups.push({ - id: groupRef.toString(), - name: - typeof group.get("Name") === "string" - ? stringToPDFString(group.get("Name")) - : null, - intent: - typeof group.get("Intent") === "string" - ? stringToPDFString(group.get("Intent")) - : null, - }); + + groups.push(this.#readOptionalContentGroup(groupRef)); } - config = this._readOptionalContentConfig(defaultConfig, groupRefs); + config = this.#readOptionalContentConfig(defaultConfig, groupRefs); config.groups = groups; } catch (ex) { if (ex instanceof MissingDataException) { @@ -469,7 +459,65 @@ class Catalog { return shadow(this, "optionalContentConfig", config); } - _readOptionalContentConfig(config, contentGroupRefs) { + #readOptionalContentGroup(groupRef) { + const group = this.xref.fetch(groupRef); + const obj = { + id: groupRef.toString(), + name: null, + intent: null, + usage: { + print: null, + view: null, + }, + }; + + const name = group.get("Name"); + if (typeof name === "string") { + obj.name = stringToPDFString(name); + } + + let intent = group.getArray("Intent"); + if (!Array.isArray(intent)) { + intent = [intent]; + } + if (intent.every(i => i instanceof Name)) { + obj.intent = intent.map(i => i.name); + } + + const usage = group.get("Usage"); + if (!(usage instanceof Dict)) { + return obj; + } + const usageObj = obj.usage; + + const print = usage.get("Print"); + if (print instanceof Dict) { + const printState = print.get("PrintState"); + if (printState instanceof Name) { + switch (printState.name) { + case "ON": + case "OFF": + usageObj.print = { printState: printState.name }; + } + } + } + + const view = usage.get("View"); + if (view instanceof Dict) { + const viewState = view.get("ViewState"); + if (viewState instanceof Name) { + switch (viewState.name) { + case "ON": + case "OFF": + usageObj.view = { viewState: viewState.name }; + } + } + } + + return obj; + } + + #readOptionalContentConfig(config, contentGroupRefs) { function parseOnOff(refs) { const onParsed = []; if (Array.isArray(refs)) { diff --git a/src/display/api.js b/src/display/api.js index 2ae61ea29..ba22a872f 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -949,12 +949,26 @@ class PDFDocumentProxy { } /** + * @typedef {Object} GetOptionalContentConfigParameters + * @property {string} [intent] - Determines the optional content groups that + * are visible by default; valid values are: + * - 'display' (viewable groups). + * - 'print' (printable groups). + * - 'any' (all groups). + * The default value is 'display'. + */ + + /** + * @param {GetOptionalContentConfigParameters} [params] - Optional content + * config parameters. * @returns {Promise} A promise that is resolved with * an {@link OptionalContentConfig} that contains all the optional content * groups (assuming that the document has any). */ - getOptionalContentConfig() { - return this._transport.getOptionalContentConfig(); + getOptionalContentConfig({ intent = "display" } = {}) { + const { renderingIntent } = this._transport.getRenderingIntent(intent); + + return this._transport.getOptionalContentConfig(renderingIntent); } /** @@ -1340,17 +1354,14 @@ class PDFPageProxy { } /** - * @param {GetAnnotationsParameters} params - Annotation parameters. + * @param {GetAnnotationsParameters} [params] - Annotation parameters. * @returns {Promise>} A promise that is resolved with an * {Array} of the annotation objects. */ getAnnotations({ intent = "display" } = {}) { - const intentArgs = this._transport.getRenderingIntent(intent); + const { renderingIntent } = this._transport.getRenderingIntent(intent); - return this._transport.getAnnotations( - this._pageIndex, - intentArgs.renderingIntent - ); + return this._transport.getAnnotations(this._pageIndex, renderingIntent); } /** @@ -1411,20 +1422,20 @@ class PDFPageProxy { annotationMode, printAnnotationStorage ); + const { renderingIntent, cacheKey } = intentArgs; // If there was a pending destroy, cancel it so no cleanup happens during // this call to render... this.#pendingCleanup = false; // ... and ensure that a delayed cleanup is always aborted. this.#abortDelayedCleanup(); - if (!optionalContentConfigPromise) { - optionalContentConfigPromise = this._transport.getOptionalContentConfig(); - } + optionalContentConfigPromise ||= + this._transport.getOptionalContentConfig(renderingIntent); - let intentState = this._intentStates.get(intentArgs.cacheKey); + let intentState = this._intentStates.get(cacheKey); if (!intentState) { intentState = Object.create(null); - this._intentStates.set(intentArgs.cacheKey, intentState); + this._intentStates.set(cacheKey, intentState); } // Ensure that a pending `streamReader` cancel timeout is always aborted. @@ -1433,9 +1444,7 @@ class PDFPageProxy { intentState.streamReaderCancelTimeout = null; } - const intentPrint = !!( - intentArgs.renderingIntent & RenderingIntentFlag.PRINT - ); + const intentPrint = !!(renderingIntent & RenderingIntentFlag.PRINT); // If there's no displayReadyCapability yet, then the operatorList // was never requested before. Make the request and create the promise. @@ -1512,6 +1521,12 @@ class PDFPageProxy { } this._stats?.time("Rendering"); + if (!(optionalContentConfig.renderingIntent & renderingIntent)) { + throw new Error( + "Must use the same `intent`-argument when calling the `PDFPageProxy.render` " + + "and `PDFDocumentProxy.getOptionalContentConfig` methods." + ); + } internalRenderTask.initializeGraphics({ transparency, optionalContentConfig, @@ -2994,10 +3009,10 @@ class WorkerTransport { return this.messageHandler.sendWithPromise("GetOutline", null); } - getOptionalContentConfig() { - return this.messageHandler - .sendWithPromise("GetOptionalContentConfig", null) - .then(results => new OptionalContentConfig(results)); + getOptionalContentConfig(renderingIntent) { + return this.#cacheSimpleMethod("GetOptionalContentConfig").then( + data => new OptionalContentConfig(data, renderingIntent) + ); } getPermissions() { diff --git a/src/display/optional_content_config.js b/src/display/optional_content_config.js index 12c9a7621..20c6b71fb 100644 --- a/src/display/optional_content_config.js +++ b/src/display/optional_content_config.js @@ -13,33 +13,63 @@ * limitations under the License. */ -import { info, objectFromMap, unreachable, warn } from "../shared/util.js"; +import { + info, + objectFromMap, + RenderingIntentFlag, + unreachable, + warn, +} from "../shared/util.js"; import { MurmurHash3_64 } from "../shared/murmurhash3.js"; const INTERNAL = Symbol("INTERNAL"); class OptionalContentGroup { + #isDisplay = false; + + #isPrint = false; + + #userSet = false; + #visible = true; - constructor(name, intent) { + constructor(renderingIntent, { name, intent, usage }) { + this.#isDisplay = !!(renderingIntent & RenderingIntentFlag.DISPLAY); + this.#isPrint = !!(renderingIntent & RenderingIntentFlag.PRINT); + this.name = name; this.intent = intent; + this.usage = usage; } /** * @type {boolean} */ get visible() { - return this.#visible; + if (this.#userSet) { + return this.#visible; + } + if (!this.#visible) { + return false; + } + const { print, view } = this.usage; + + if (this.#isDisplay) { + return view?.viewState !== "OFF"; + } else if (this.#isPrint) { + return print?.printState !== "OFF"; + } + return true; } /** * @ignore */ - _setVisible(internal, visible) { + _setVisible(internal, visible, userSet = false) { if (internal !== INTERNAL) { unreachable("Internal method `_setVisible` called."); } + this.#userSet = userSet; this.#visible = visible; } } @@ -53,7 +83,9 @@ class OptionalContentConfig { #order = null; - constructor(data) { + constructor(data, renderingIntent = RenderingIntentFlag.DISPLAY) { + this.renderingIntent = renderingIntent; + this.name = null; this.creator = null; @@ -66,7 +98,7 @@ class OptionalContentConfig { for (const group of data.groups) { this.#groups.set( group.id, - new OptionalContentGroup(group.name, group.intent) + new OptionalContentGroup(renderingIntent, group) ); } @@ -202,7 +234,7 @@ class OptionalContentConfig { warn(`Optional content group not found: ${id}`); return; } - this.#groups.get(id)._setVisible(INTERNAL, !!visible); + this.#groups.get(id)._setVisible(INTERNAL, !!visible, /* userSet = */ true); this.#cachedGetHash = null; } diff --git a/test/driver.js b/test/driver.js index 7d1fd6e09..780c16742 100644 --- a/test/driver.js +++ b/test/driver.js @@ -684,7 +684,9 @@ class Driver { } task.pdfDoc = doc; - task.optionalContentConfigPromise = doc.getOptionalContentConfig(); + task.optionalContentConfigPromise = doc.getOptionalContentConfig({ + intent: task.print ? "print" : "display", + }); if (task.optionalContent) { const entries = Object.entries(task.optionalContent), diff --git a/test/pdfs/bug1826783.pdf.link b/test/pdfs/bug1826783.pdf.link new file mode 100644 index 000000000..1eb106272 --- /dev/null +++ b/test/pdfs/bug1826783.pdf.link @@ -0,0 +1 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9327375 diff --git a/test/test_manifest.json b/test/test_manifest.json index c1fe937a7..d00a9856a 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -4016,6 +4016,23 @@ "lastPage": 5, "type": "eq" }, + { + "id": "bug1826783-display", + "file": "pdfs/bug1826783.pdf", + "md5": "93e706efee15dd7b32d32d66f15a3ea2", + "rounds": 1, + "link": true, + "type": "eq" + }, + { + "id": "bug1826783-print", + "file": "pdfs/bug1826783.pdf", + "md5": "93e706efee15dd7b32d32d66f15a3ea2", + "rounds": 1, + "link": true, + "type": "eq", + "print": true + }, { "id": "issue8586", "file": "pdfs/issue8586.pdf", diff --git a/web/app.js b/web/app.js index 568769df6..a9581bbb0 100644 --- a/web/app.js +++ b/web/app.js @@ -1796,7 +1796,6 @@ const PDFViewerApplication = { pagesOverview: this.pdfViewer.getPagesOverview(), printContainer: this.appConfig.printContainer, printResolution: AppOptions.get("printResolution"), - optionalContentConfigPromise: this.pdfViewer.optionalContentConfigPromise, printAnnotationStoragePromise: this._printAnnotationStoragePromise, }); this.forceRendering(); diff --git a/web/firefox_print_service.js b/web/firefox_print_service.js index 8241ed64d..c82dcd6ec 100644 --- a/web/firefox_print_service.js +++ b/web/firefox_print_service.js @@ -119,15 +119,15 @@ class FirefoxPrintService { pagesOverview, printContainer, printResolution, - optionalContentConfigPromise = null, printAnnotationStoragePromise = null, }) { this.pdfDocument = pdfDocument; this.pagesOverview = pagesOverview; this.printContainer = printContainer; this._printResolution = printResolution || 150; - this._optionalContentConfigPromise = - optionalContentConfigPromise || pdfDocument.getOptionalContentConfig(); + this._optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ + intent: "print", + }); this._printAnnotationStoragePromise = printAnnotationStoragePromise || Promise.resolve(); } diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 8f7b5bb54..ecced7836 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -258,14 +258,8 @@ if (PDFJSDev.test("GECKOVIEW")) { const hasWillPrint = pdfViewer.enableScripting && !!(await pdfDocument.getJSActions())?.WillPrint; - const hasUnchangedOptionalContent = ( - await pdfViewer.optionalContentConfigPromise - ).hasInitialVisibility; - result = - hasUnchangedAnnotations && - !hasWillPrint && - hasUnchangedOptionalContent; + result = hasUnchangedAnnotations && !hasWillPrint; } catch { console.warn("Unable to check if the document can be downloaded."); } diff --git a/web/pdf_layer_viewer.js b/web/pdf_layer_viewer.js index d139de943..e9c1150fc 100644 --- a/web/pdf_layer_viewer.js +++ b/web/pdf_layer_viewer.js @@ -182,7 +182,7 @@ class PDFLayerViewer extends BaseTreeViewer { } const pdfDocument = this._pdfDocument; const optionalContentConfig = await (promise || - pdfDocument.getOptionalContentConfig()); + pdfDocument.getOptionalContentConfig({ intent: "display" })); if (pdfDocument !== this._pdfDocument) { return; // The document was closed while the optional content resolved. diff --git a/web/pdf_print_service.js b/web/pdf_print_service.js index cc4b330a6..1985aa4ad 100644 --- a/web/pdf_print_service.js +++ b/web/pdf_print_service.js @@ -13,7 +13,12 @@ * limitations under the License. */ -import { AnnotationMode, PixelsPerInch, shadow } from "pdfjs-lib"; +import { + AnnotationMode, + PixelsPerInch, + RenderingCancelledException, + shadow, +} from "pdfjs-lib"; import { getXfaHtmlForPrinting } from "./print_utils.js"; let activeService = null; @@ -58,7 +63,14 @@ function renderPage( optionalContentConfigPromise, printAnnotationStorage, }; - return pdfPage.render(renderContext).promise; + const renderTask = pdfPage.render(renderContext); + + return renderTask.promise.catch(reason => { + if (!(reason instanceof RenderingCancelledException)) { + console.error(reason); + } + throw reason; + }); }); } @@ -68,15 +80,15 @@ class PDFPrintService { pagesOverview, printContainer, printResolution, - optionalContentConfigPromise = null, printAnnotationStoragePromise = null, }) { this.pdfDocument = pdfDocument; this.pagesOverview = pagesOverview; this.printContainer = printContainer; this._printResolution = printResolution || 150; - this._optionalContentConfigPromise = - optionalContentConfigPromise || pdfDocument.getOptionalContentConfig(); + this._optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ + intent: "print", + }); this._printAnnotationStoragePromise = printAnnotationStoragePromise || Promise.resolve(); this.currentPage = -1; diff --git a/web/pdf_thumbnail_viewer.js b/web/pdf_thumbnail_viewer.js index a16d84c4f..468968762 100644 --- a/web/pdf_thumbnail_viewer.js +++ b/web/pdf_thumbnail_viewer.js @@ -189,7 +189,9 @@ class PDFThumbnailViewer { return; } const firstPagePromise = pdfDocument.getPage(1); - const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig(); + const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ + intent: "display", + }); firstPagePromise .then(firstPdfPage => { diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 4f88bcb1d..ad6e1712f 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -781,7 +781,9 @@ class PDFViewer { const pagesCount = pdfDocument.numPages; const firstPagePromise = pdfDocument.getPage(1); // Rendering (potentially) depends on this, hence fetching it immediately. - const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig(); + const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig({ + intent: "display", + }); const permissionsPromise = this.#enablePermissions ? pdfDocument.getPermissions() : Promise.resolve(); @@ -1822,7 +1824,7 @@ class PDFViewer { console.error("optionalContentConfigPromise: Not initialized yet."); // Prevent issues if the getter is accessed *before* the `onePageRendered` // promise has resolved; won't (normally) happen in the default viewer. - return this.pdfDocument.getOptionalContentConfig(); + return this.pdfDocument.getOptionalContentConfig({ intent: "display" }); } return this._optionalContentConfigPromise; }