diff --git a/src/core/obj.js b/src/core/obj.js index 9aeb8f4a5..342313117 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -153,6 +153,47 @@ class Catalog { return shadow(this, "metadata", metadata); } + get markInfo() { + let markInfo = null; + try { + markInfo = this._readMarkInfo(); + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + warn("Unable to read mark info."); + } + return shadow(this, "markInfo", markInfo); + } + + /** + * @private + */ + _readMarkInfo() { + const obj = this._catDict.get("MarkInfo"); + if (!isDict(obj)) { + return null; + } + + const markInfo = Object.assign(Object.create(null), { + Marked: false, + UserProperties: false, + Suspects: false, + }); + for (const key in markInfo) { + if (!obj.has(key)) { + continue; + } + const value = obj.get(key); + if (!isBool(value)) { + continue; + } + markInfo[key] = value; + } + + return markInfo; + } + get toplevelPagesDict() { const pagesObj = this._catDict.get("Pages"); if (!isDict(pagesObj)) { diff --git a/src/core/worker.js b/src/core/worker.js index eae578ad6..7dee0f195 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -499,6 +499,10 @@ class WorkerMessageHandler { ]); }); + handler.on("GetMarkInfo", function wphSetupGetMarkInfo(data) { + return pdfManager.ensureCatalog("markInfo"); + }); + handler.on("GetData", function wphSetupGetData(data) { pdfManager.requestLoadedStream(); return pdfManager.onLoadedStream().then(function (stream) { diff --git a/src/display/api.js b/src/display/api.js index ff7a6aaa3..a44ae82d1 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -805,6 +805,23 @@ class PDFDocumentProxy { return this._transport.getMetadata(); } + /** + * @typedef {Object} MarkInfo + * Properties correspond to Table 321 of the PDF 32000-1:2008 spec. + * @property {boolean} Marked + * @property {boolean} UserProperties + * @property {boolean} Suspects + */ + + /** + * @returns {Promise} A promise that is resolved with + * a {MarkInfo} object that contains the MarkInfo flags for the PDF + * document, or `null` when no MarkInfo values are present in the PDF file. + */ + getMarkInfo() { + return this._transport.getMarkInfo(); + } + /** * @returns {Promise} A promise that is resolved with a * {TypedArray} that has the raw data from the PDF. @@ -2639,6 +2656,10 @@ class WorkerTransport { }); } + getMarkInfo() { + return this.messageHandler.sendWithPromise("GetMarkInfo", null); + } + getStats() { return this.messageHandler.sendWithPromise("GetStats", null); } diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index a980d5d6b..6fcc068a8 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1209,6 +1209,24 @@ describe("api", function () { .catch(done.fail); }); + it("gets markInfo", function (done) { + const loadingTask = getDocument( + buildGetDocumentParams("annotation-line.pdf") + ); + + loadingTask.promise + .then(function (pdfDoc) { + return pdfDoc.getMarkInfo(); + }) + .then(function (info) { + expect(info.Marked).toEqual(true); + expect(info.UserProperties).toEqual(false); + expect(info.Suspects).toEqual(false); + done(); + }) + .catch(done.fail); + }); + it("gets data", function (done) { const promise = pdfDocument.getData(); promise diff --git a/web/app.js b/web/app.js index 53a1bff38..a414c4b7c 100644 --- a/web/app.js +++ b/web/app.js @@ -247,6 +247,7 @@ const PDFViewerApplication = { triggerDelayedFallback: null, _saveInProgress: false, _wheelUnusedTicks: 0, + _idleCallbacks: new Set(), // Called once when the document is loaded. async initialize(appConfig) { @@ -743,6 +744,10 @@ const PDFViewerApplication = { this.contentDispositionFilename = null; this.triggerDelayedFallback = null; this._saveInProgress = false; + for (const callback of this._idleCallbacks) { + window.cancelIdleCallback(callback); + } + this._idleCallbacks.clear(); this.pdfSidebar.reset(); this.pdfOutlineViewer.reset(); @@ -1334,6 +1339,16 @@ const PDFViewerApplication = { pdfViewer.optionalContentConfigPromise.then(optionalContentConfig => { this.pdfLayerViewer.render({ optionalContentConfig, pdfDocument }); }); + if ("requestIdleCallback" in window) { + const callback = window.requestIdleCallback( + () => { + this._collectTelemetry(pdfDocument); + this._idleCallbacks.delete(callback); + }, + { timeout: 1000 } + ); + this._idleCallbacks.add(callback); + } }); this._initializePageLabels(pdfDocument); @@ -1398,6 +1413,23 @@ const PDFViewerApplication = { scripting.createSandbox({ objects, dispatchEventName, calculationOrder }); }, + /** + * A place to fetch data for telemetry after one page is rendered and the + * viewer is idle. + * @private + */ + async _collectTelemetry(pdfDocument) { + const markInfo = await this.pdfDocument.getMarkInfo(); + if (pdfDocument !== this.pdfDocument) { + return; // Document was closed while waiting for mark info. + } + const tagged = markInfo?.Marked || false; + this.externalServices.reportTelemetry({ + type: "tagged", + tagged, + }); + }, + /** * @private */