diff --git a/src/core/obj.js b/src/core/obj.js index 9bb26fc5dd71b..f05d373a284f5 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -15,9 +15,9 @@ import { bytesToString, createPromiseCapability, createValidAbsoluteUrl, FormatError, - info, InvalidPDFException, isBool, isString, MissingDataException, shadow, - stringToPDFString, stringToUTF8String, toRomanNumerals, unreachable, warn, - XRefParseException + info, InvalidPDFException, isBool, isNum, isString, MissingDataException, + PermissionFlag, shadow, stringToPDFString, stringToUTF8String, + toRomanNumerals, unreachable, warn, XRefParseException } from '../shared/util'; import { Dict, isCmd, isDict, isName, isRef, isRefsEqual, isStream, Ref, RefSet, @@ -177,6 +177,48 @@ class Catalog { return (root.items.length > 0 ? root.items : null); } + get permissions() { + let permissions = null; + try { + permissions = this._readPermissions(); + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + warn('Unable to read permissions.'); + } + return shadow(this, 'permissions', permissions); + } + + /** + * @private + */ + _readPermissions() { + const encrypt = this.xref.trailer.get('Encrypt'); + if (!isDict(encrypt)) { + return null; + } + + let flags = encrypt.get('P'); + if (!isNum(flags)) { + return null; + } + + // PDF integer objects are represented internally in signed 2's complement + // form. Therefore, convert the signed decimal integer to a signed 2's + // complement binary integer so we can use regular bitwise operations on it. + flags += 2 ** 32; + + const permissions = []; + for (const key in PermissionFlag) { + const value = PermissionFlag[key]; + if (flags & value) { + permissions.push(value); + } + } + return permissions; + } + get numPages() { const obj = this.toplevelPagesDict.get('Count'); if (!Number.isInteger(obj)) { diff --git a/src/core/worker.js b/src/core/worker.js index 454cff51ae1a1..8bfc9c4590b88 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -703,6 +703,10 @@ var WorkerMessageHandler = { } ); + handler.on('GetPermissions', function(data) { + return pdfManager.ensureCatalog('permissions'); + }); + handler.on('GetMetadata', function wphSetupGetMetadata(data) { return Promise.all([pdfManager.ensureDoc('documentInfo'), diff --git a/src/display/api.js b/src/display/api.js index f5c6b15e70051..9e58df398ad54 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -577,166 +577,188 @@ var PDFDataRangeTransport = (function pdfDataRangeTransportClosure() { /** * Proxy to a PDFDocument in the worker thread. Also, contains commonly used * properties that can be read synchronously. - * @class - * @alias PDFDocumentProxy */ -var PDFDocumentProxy = (function PDFDocumentProxyClosure() { - function PDFDocumentProxy(pdfInfo, transport, loadingTask) { - this._pdfInfo = pdfInfo; - this.transport = transport; +class PDFDocumentProxy { + constructor(pdfInfo, transport, loadingTask) { this.loadingTask = loadingTask; + + this._pdfInfo = pdfInfo; + this._transport = transport; } - PDFDocumentProxy.prototype = /** @lends PDFDocumentProxy.prototype */ { - /** - * @return {number} Total number of pages the PDF contains. - */ - get numPages() { - return this._pdfInfo.numPages; - }, - /** - * @return {string} A unique ID to identify a PDF. Not guaranteed to be - * unique. - */ - get fingerprint() { - return this._pdfInfo.fingerprint; - }, - /** - * @param {number} pageNumber The page number to get. The first page is 1. - * @return {Promise} A promise that is resolved with a {@link PDFPageProxy} - * object. - */ - getPage(pageNumber) { - return this.transport.getPage(pageNumber); - }, - /** - * @param {{num: number, gen: number}} ref The page reference. Must have - * the 'num' and 'gen' properties. - * @return {Promise} A promise that is resolved with the page index that is - * associated with the reference. - */ - getPageIndex: function PDFDocumentProxy_getPageIndex(ref) { - return this.transport.getPageIndex(ref); - }, - /** - * @return {Promise} A promise that is resolved with a lookup table for - * mapping named destinations to reference numbers. - * - * This can be slow for large documents: use getDestination instead - */ - getDestinations: function PDFDocumentProxy_getDestinations() { - return this.transport.getDestinations(); - }, - /** - * @param {string} id - The named destination to get. - * @return {Promise} A promise that is resolved with all information - * of the given named destination. - */ - getDestination: function PDFDocumentProxy_getDestination(id) { - return this.transport.getDestination(id); - }, - /** - * @return {Promise} A promise that is resolved with: - * an Array containing the pageLabels that correspond to the pageIndexes, - * or `null` when no pageLabels are present in the PDF file. - */ - getPageLabels: function PDFDocumentProxy_getPageLabels() { - return this.transport.getPageLabels(); - }, - /** - * @return {Promise} A promise that is resolved with a {string} containing - * the PageMode name. - */ - getPageMode() { - return this.transport.getPageMode(); - }, - /** - * @return {Promise} A promise that is resolved with a lookup table for - * mapping named attachments to their content. - */ - getAttachments: function PDFDocumentProxy_getAttachments() { - return this.transport.getAttachments(); - }, - /** - * @return {Promise} A promise that is resolved with an {Array} of all the - * JavaScript strings in the name tree, or `null` if no JavaScript exists. - */ - getJavaScript() { - return this.transport.getJavaScript(); - }, - /** - * @return {Promise} A promise that is resolved with an {Array} that is a - * tree outline (if it has one) of the PDF. The tree is in the format of: - * [ - * { - * title: string, - * bold: boolean, - * italic: boolean, - * color: rgb Uint8ClampedArray, - * dest: dest obj, - * url: string, - * items: array of more items like this - * }, - * ... - * ]. - */ - getOutline: function PDFDocumentProxy_getOutline() { - return this.transport.getOutline(); - }, - /** - * @return {Promise} A promise that is resolved with an {Object} that has - * info and metadata properties. Info is an {Object} filled with anything - * available in the information dictionary and similarly metadata is a - * {Metadata} object with information from the metadata section of the PDF. - */ - getMetadata: function PDFDocumentProxy_getMetadata() { - return this.transport.getMetadata(); - }, - /** - * @return {Promise} A promise that is resolved with a TypedArray that has - * the raw data from the PDF. - */ - getData: function PDFDocumentProxy_getData() { - return this.transport.getData(); - }, - /** - * @return {Promise} A promise that is resolved when the document's data - * is loaded. It is resolved with an {Object} that contains the length - * property that indicates size of the PDF data in bytes. - */ - getDownloadInfo: function PDFDocumentProxy_getDownloadInfo() { - return this.transport.downloadInfoCapability.promise; - }, - /** - * @return {Promise} A promise this is resolved with current stats about - * document structures (see {@link PDFDocumentStats}). - */ - getStats: function PDFDocumentProxy_getStats() { - return this.transport.getStats(); - }, - /** - * Cleans up resources allocated by the document, e.g. created @font-face. - */ - cleanup: function PDFDocumentProxy_cleanup() { - this.transport.startCleanup(); - }, - /** - * Destroys current document instance and terminates worker. - */ - destroy: function PDFDocumentProxy_destroy() { - return this.loadingTask.destroy(); - }, - /** - * @return {Object} A subset of the current {DocumentInitParameters}, - * which are either needed in the viewer and/or whose default values - * may be affected by the `apiCompatibilityParams`. - */ - get loadingParams() { - return this.transport.loadingParams; - }, - }; - return PDFDocumentProxy; -})(); + /** + * @return {number} Total number of pages the PDF contains. + */ + get numPages() { + return this._pdfInfo.numPages; + } + + /** + * @return {string} A (not guaranteed to be) unique ID to identify a PDF. + */ + get fingerprint() { + return this._pdfInfo.fingerprint; + } + + /** + * @param {number} pageNumber - The page number to get. The first page is 1. + * @return {Promise} A promise that is resolved with a {@link PDFPageProxy} + * object. + */ + getPage(pageNumber) { + return this._transport.getPage(pageNumber); + } + + /** + * @param {{num: number, gen: number}} ref - The page reference. Must have + * the `num` and `gen` properties. + * @return {Promise} A promise that is resolved with the page index that is + * associated with the reference. + */ + getPageIndex(ref) { + return this._transport.getPageIndex(ref); + } + + /** + * @return {Promise} A promise that is resolved with a lookup table for + * mapping named destinations to reference numbers. + * + * This can be slow for large documents. Use `getDestination` instead. + */ + getDestinations() { + return this._transport.getDestinations(); + } + + /** + * @param {string} id - The named destination to get. + * @return {Promise} A promise that is resolved with all information + * of the given named destination. + */ + getDestination(id) { + return this._transport.getDestination(id); + } + + /** + * @return {Promise} A promise that is resolved with an {Array} containing + * the page labels that correspond to the page indexes, or `null` when + * no page labels are present in the PDF file. + */ + getPageLabels() { + return this._transport.getPageLabels(); + } + + /** + * @return {Promise} A promise that is resolved with a {string} containing + * the page mode name. + */ + getPageMode() { + return this._transport.getPageMode(); + } + + /** + * @return {Promise} A promise that is resolved with a lookup table for + * mapping named attachments to their content. + */ + getAttachments() { + return this._transport.getAttachments(); + } + + /** + * @return {Promise} A promise that is resolved with an {Array} of all the + * JavaScript strings in the name tree, or `null` if no JavaScript exists. + */ + getJavaScript() { + return this._transport.getJavaScript(); + } + + /** + * @return {Promise} A promise that is resolved with an {Array} that is a + * tree outline (if it has one) of the PDF. The tree is in the format of: + * [ + * { + * title: string, + * bold: boolean, + * italic: boolean, + * color: rgb Uint8ClampedArray, + * dest: dest obj, + * url: string, + * items: array of more items like this + * }, + * ... + * ] + */ + getOutline() { + return this._transport.getOutline(); + } + + /** + * @return {Promise} A promise that is resolved with an {Array} that contains + * the permission flags for the PDF document, or `null` when + * no permissions are present in the PDF file. + */ + getPermissions() { + return this._transport.getPermissions(); + } + + /** + * @return {Promise} A promise that is resolved with an {Object} that has + * `info` and `metadata` properties. `info` is an {Object} filled with + * anything available in the information dictionary and similarly + * `metadata` is a {Metadata} object with information from the metadata + * section of the PDF. + */ + getMetadata() { + return this._transport.getMetadata(); + } + + /** + * @return {Promise} A promise that is resolved with a {TypedArray} that has + * the raw data from the PDF. + */ + getData() { + return this._transport.getData(); + } + + /** + * @return {Promise} A promise that is resolved when the document's data + * is loaded. It is resolved with an {Object} that contains the `length` + * property that indicates size of the PDF data in bytes. + */ + getDownloadInfo() { + return this._transport.downloadInfoCapability.promise; + } + + /** + * @return {Promise} A promise this is resolved with current statistics about + * document structures (see {@link PDFDocumentStats}). + */ + getStats() { + return this._transport.getStats(); + } + + /** + * Cleans up resources allocated by the document, e.g. created `@font-face`. + */ + cleanup() { + this._transport.startCleanup(); + } + + /** + * Destroys the current document instance and terminates the worker. + */ + destroy() { + return this.loadingTask.destroy(); + } + + /** + * @return {Object} A subset of the current {DocumentInitParameters}, + * which are either needed in the viewer and/or whose default values + * may be affected by the `apiCompatibilityParams`. + */ + get loadingParams() { + return this._transport.loadingParams; + } +} /** * Page getTextContent parameters. @@ -1616,8 +1638,8 @@ var PDFWorker = (function PDFWorkerClosure() { * For internal use only. * @ignore */ -var WorkerTransport = (function WorkerTransportClosure() { - function WorkerTransport(messageHandler, loadingTask, networkStream, params) { +class WorkerTransport { + constructor(messageHandler, loadingTask, networkStream, params) { this.messageHandler = messageHandler; this.loadingTask = loadingTask; this.commonObjs = new PDFObjects(); @@ -1642,553 +1664,535 @@ var WorkerTransport = (function WorkerTransportClosure() { this.setupMessageHandler(); } - WorkerTransport.prototype = { - destroy: function WorkerTransport_destroy() { - if (this.destroyCapability) { - return this.destroyCapability.promise; - } - this.destroyed = true; - this.destroyCapability = createPromiseCapability(); + destroy() { + if (this.destroyCapability) { + return this.destroyCapability.promise; + } - if (this._passwordCapability) { - this._passwordCapability.reject( - new Error('Worker was destroyed during onPassword callback')); - } + this.destroyed = true; + this.destroyCapability = createPromiseCapability(); - var waitOn = []; - // We need to wait for all renderings to be completed, e.g. - // timeout/rAF can take a long time. - this.pageCache.forEach(function (page) { - if (page) { - waitOn.push(page._destroy()); - } - }); - this.pageCache = []; - this.pagePromises = []; - // We also need to wait for the worker to finish its long running tasks. - var terminated = this.messageHandler.sendWithPromise('Terminate', null); - waitOn.push(terminated); - Promise.all(waitOn).then(() => { - this.fontLoader.clear(); - if (this._networkStream) { - this._networkStream.cancelAllRequests(); - } + if (this._passwordCapability) { + this._passwordCapability.reject( + new Error('Worker was destroyed during onPassword callback')); + } - if (this.messageHandler) { - this.messageHandler.destroy(); - this.messageHandler = null; - } - this.destroyCapability.resolve(); - }, this.destroyCapability.reject); - return this.destroyCapability.promise; - }, + const waitOn = []; + // We need to wait for all renderings to be completed, e.g. + // timeout/rAF can take a long time. + this.pageCache.forEach(function(page) { + if (page) { + waitOn.push(page._destroy()); + } + }); + this.pageCache = []; + this.pagePromises = []; + // We also need to wait for the worker to finish its long running tasks. + const terminated = this.messageHandler.sendWithPromise('Terminate', null); + waitOn.push(terminated); + Promise.all(waitOn).then(() => { + this.fontLoader.clear(); + if (this._networkStream) { + this._networkStream.cancelAllRequests(); + } - setupMessageHandler: function WorkerTransport_setupMessageHandler() { - var messageHandler = this.messageHandler; - var loadingTask = this.loadingTask; - - messageHandler.on('GetReader', function(data, sink) { - assert(this._networkStream); - this._fullReader = this._networkStream.getFullReader(); - this._fullReader.onProgress = (evt) => { - this._lastProgress = { - loaded: evt.loaded, - total: evt.total, - }; - }; - sink.onPull = () => { - this._fullReader.read().then(function({ value, done, }) { - if (done) { - sink.close(); - return; - } - assert(isArrayBuffer(value)); - // Enqueue data chunk into sink, and transfer it - // to other side as `Transferable` object. - sink.enqueue(new Uint8Array(value), 1, [value]); - }).catch((reason) => { - sink.error(reason); - }); - }; + if (this.messageHandler) { + this.messageHandler.destroy(); + this.messageHandler = null; + } + this.destroyCapability.resolve(); + }, this.destroyCapability.reject); + return this.destroyCapability.promise; + } + + setupMessageHandler() { + const { messageHandler, loadingTask, } = this; - sink.onCancel = (reason) => { - this._fullReader.cancel(reason); + messageHandler.on('GetReader', function(data, sink) { + assert(this._networkStream); + this._fullReader = this._networkStream.getFullReader(); + this._fullReader.onProgress = (evt) => { + this._lastProgress = { + loaded: evt.loaded, + total: evt.total, }; - }, this); + }; + sink.onPull = () => { + this._fullReader.read().then(function({ value, done, }) { + if (done) { + sink.close(); + return; + } + assert(isArrayBuffer(value)); + // Enqueue data chunk into sink, and transfer it + // to other side as `Transferable` object. + sink.enqueue(new Uint8Array(value), 1, [value]); + }).catch((reason) => { + sink.error(reason); + }); + }; - messageHandler.on('ReaderHeadersReady', function(data) { - let headersCapability = createPromiseCapability(); - let fullReader = this._fullReader; - fullReader.headersReady.then(() => { - // If stream or range are disabled, it's our only way to report - // loading progress. - if (!fullReader.isStreamingSupported || - !fullReader.isRangeSupported) { - if (this._lastProgress) { - let loadingTask = this.loadingTask; - if (loadingTask.onProgress) { - loadingTask.onProgress(this._lastProgress); - } + sink.onCancel = (reason) => { + this._fullReader.cancel(reason); + }; + }, this); + + messageHandler.on('ReaderHeadersReady', function(data) { + const headersCapability = createPromiseCapability(); + const fullReader = this._fullReader; + fullReader.headersReady.then(() => { + // If stream or range are disabled, it's our only way to report + // loading progress. + if (!fullReader.isStreamingSupported || + !fullReader.isRangeSupported) { + if (this._lastProgress) { + if (loadingTask.onProgress) { + loadingTask.onProgress(this._lastProgress); } - fullReader.onProgress = (evt) => { - let loadingTask = this.loadingTask; - if (loadingTask.onProgress) { - loadingTask.onProgress({ - loaded: evt.loaded, - total: evt.total, - }); - } - }; } - - headersCapability.resolve({ - isStreamingSupported: fullReader.isStreamingSupported, - isRangeSupported: fullReader.isRangeSupported, - contentLength: fullReader.contentLength, - }); - }, headersCapability.reject); - - return headersCapability.promise; - }, this); - - messageHandler.on('GetRangeReader', function(data, sink) { - assert(this._networkStream); - let _rangeReader = - this._networkStream.getRangeReader(data.begin, data.end); - - sink.onPull = () => { - _rangeReader.read().then(function({ value, done, }) { - if (done) { - sink.close(); - return; + fullReader.onProgress = (evt) => { + if (loadingTask.onProgress) { + loadingTask.onProgress({ + loaded: evt.loaded, + total: evt.total, + }); } - assert(isArrayBuffer(value)); - sink.enqueue(new Uint8Array(value), 1, [value]); - }).catch((reason) => { - sink.error(reason); - }); - }; - - sink.onCancel = (reason) => { - _rangeReader.cancel(reason); - }; - }, this); - - messageHandler.on('GetDoc', function transportDoc({ pdfInfo, }) { - this.numPages = pdfInfo.numPages; - var loadingTask = this.loadingTask; - var pdfDocument = new PDFDocumentProxy(pdfInfo, this, loadingTask); - this.pdfDocument = pdfDocument; - loadingTask._capability.resolve(pdfDocument); - }, this); - - messageHandler.on('PasswordRequest', - function transportPasswordRequest(exception) { - this._passwordCapability = createPromiseCapability(); - - if (loadingTask.onPassword) { - var updatePassword = (password) => { - this._passwordCapability.resolve({ - password, - }); }; - try { - loadingTask.onPassword(updatePassword, exception.code); - } catch (ex) { - this._passwordCapability.reject(ex); - } - } else { - this._passwordCapability.reject( - new PasswordException(exception.message, exception.code)); } - return this._passwordCapability.promise; - }, this); - messageHandler.on('PasswordException', - function transportPasswordException(exception) { - loadingTask._capability.reject( - new PasswordException(exception.message, exception.code)); - }, this); - - messageHandler.on('InvalidPDF', function transportInvalidPDF(exception) { - this.loadingTask._capability.reject( - new InvalidPDFException(exception.message)); - }, this); - - messageHandler.on('MissingPDF', function transportMissingPDF(exception) { - this.loadingTask._capability.reject( - new MissingPDFException(exception.message)); - }, this); + headersCapability.resolve({ + isStreamingSupported: fullReader.isStreamingSupported, + isRangeSupported: fullReader.isRangeSupported, + contentLength: fullReader.contentLength, + }); + }, headersCapability.reject); - messageHandler.on('UnexpectedResponse', - function transportUnexpectedResponse(exception) { - this.loadingTask._capability.reject( - new UnexpectedResponseException(exception.message, exception.status)); - }, this); + return headersCapability.promise; + }, this); - messageHandler.on('UnknownError', - function transportUnknownError(exception) { - this.loadingTask._capability.reject( - new UnknownErrorException(exception.message, exception.details)); - }, this); + messageHandler.on('GetRangeReader', function(data, sink) { + assert(this._networkStream); + const rangeReader = + this._networkStream.getRangeReader(data.begin, data.end); - messageHandler.on('DataLoaded', function transportPage(data) { - this.downloadInfoCapability.resolve(data); - }, this); - - messageHandler.on('StartRenderPage', function transportRender(data) { - if (this.destroyed) { - return; // Ignore any pending requests if the worker was terminated. - } - var page = this.pageCache[data.pageIndex]; + sink.onPull = () => { + rangeReader.read().then(function({ value, done, }) { + if (done) { + sink.close(); + return; + } + assert(isArrayBuffer(value)); + sink.enqueue(new Uint8Array(value), 1, [value]); + }).catch((reason) => { + sink.error(reason); + }); + }; - page._stats.timeEnd('Page Request'); - page._startRenderPage(data.transparency, data.intent); - }, this); + sink.onCancel = (reason) => { + rangeReader.cancel(reason); + }; + }, this); - messageHandler.on('RenderPageChunk', function transportRender(data) { - if (this.destroyed) { - return; // Ignore any pending requests if the worker was terminated. - } - var page = this.pageCache[data.pageIndex]; + messageHandler.on('GetDoc', function({ pdfInfo, }) { + this.numPages = pdfInfo.numPages; + this.pdfDocument = new PDFDocumentProxy(pdfInfo, this, loadingTask); + loadingTask._capability.resolve(this.pdfDocument); + }, this); - page._renderPageChunk(data.operatorList, data.intent); - }, this); + messageHandler.on('PasswordRequest', function(exception) { + this._passwordCapability = createPromiseCapability(); - messageHandler.on('commonobj', function transportObj(data) { - if (this.destroyed) { - return; // Ignore any pending requests if the worker was terminated. + if (loadingTask.onPassword) { + const updatePassword = (password) => { + this._passwordCapability.resolve({ + password, + }); + }; + try { + loadingTask.onPassword(updatePassword, exception.code); + } catch (ex) { + this._passwordCapability.reject(ex); } + } else { + this._passwordCapability.reject( + new PasswordException(exception.message, exception.code)); + } + return this._passwordCapability.promise; + }, this); + + messageHandler.on('PasswordException', function(exception) { + loadingTask._capability.reject( + new PasswordException(exception.message, exception.code)); + }, this); + + messageHandler.on('InvalidPDF', function(exception) { + loadingTask._capability.reject( + new InvalidPDFException(exception.message)); + }, this); + + messageHandler.on('MissingPDF', function(exception) { + loadingTask._capability.reject( + new MissingPDFException(exception.message)); + }, this); + + messageHandler.on('UnexpectedResponse', function(exception) { + loadingTask._capability.reject( + new UnexpectedResponseException(exception.message, exception.status)); + }, this); + + messageHandler.on('UnknownError', function(exception) { + loadingTask._capability.reject( + new UnknownErrorException(exception.message, exception.details)); + }, this); + + messageHandler.on('DataLoaded', function(data) { + this.downloadInfoCapability.resolve(data); + }, this); + + messageHandler.on('StartRenderPage', function(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } - var id = data[0]; - var type = data[1]; - if (this.commonObjs.hasData(id)) { - return; - } + const page = this.pageCache[data.pageIndex]; + page._stats.timeEnd('Page Request'); + page._startRenderPage(data.transparency, data.intent); + }, this); - switch (type) { - case 'Font': - var exportedData = data[2]; - let params = this._params; + messageHandler.on('RenderPageChunk', function(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } - if ('error' in exportedData) { - var exportedError = exportedData.error; - warn('Error during font loading: ' + exportedError); - this.commonObjs.resolve(id, exportedError); - break; - } - var fontRegistry = null; - if (params.pdfBug && globalScope.FontInspector && - globalScope.FontInspector.enabled) { - fontRegistry = { - registerFont(font, url) { - globalScope['FontInspector'].fontAdded(font, url); - }, - }; - } - var font = new FontFaceObject(exportedData, { - isEvalSupported: params.isEvalSupported, - disableFontFace: params.disableFontFace, - ignoreErrors: params.ignoreErrors, - onUnsupportedFeature: this._onUnsupportedFeature.bind(this), - fontRegistry, - }); - var fontReady = (fontObjs) => { - this.commonObjs.resolve(id, font); - }; + const page = this.pageCache[data.pageIndex]; + page._renderPageChunk(data.operatorList, data.intent); + }, this); - this.fontLoader.bind([font], fontReady); - break; - case 'FontPath': - this.commonObjs.resolve(id, data[2]); - break; - default: - throw new Error(`Got unknown common object type ${type}`); - } - }, this); + messageHandler.on('commonobj', function(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } - messageHandler.on('obj', function transportObj(data) { - if (this.destroyed) { - return; // Ignore any pending requests if the worker was terminated. - } + const [id, type, exportedData] = data; + if (this.commonObjs.hasData(id)) { + return; + } - var id = data[0]; - var pageIndex = data[1]; - var type = data[2]; - var pageProxy = this.pageCache[pageIndex]; - var imageData; - if (pageProxy.objs.hasData(id)) { - return; - } + switch (type) { + case 'Font': + const params = this._params; - switch (type) { - case 'JpegStream': - imageData = data[3]; - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = function() { - resolve(img); - }; - img.onerror = function() { - reject(new Error('Error during JPEG image loading')); - // Note that when the browser image loading/decoding fails, - // we'll fallback to the built-in PDF.js JPEG decoder; see - // `PartialEvaluator.buildPaintImageXObject` in the - // `src/core/evaluator.js` file. - }; - img.src = imageData; - }).then((img) => { - pageProxy.objs.resolve(id, img); - }); - case 'Image': - imageData = data[3]; - pageProxy.objs.resolve(id, imageData); - - // heuristics that will allow not to store large data - var MAX_IMAGE_SIZE_TO_STORE = 8000000; - if (imageData && 'data' in imageData && - imageData.data.length > MAX_IMAGE_SIZE_TO_STORE) { - pageProxy.cleanupAfterRender = true; - } + if ('error' in exportedData) { + const exportedError = exportedData.error; + warn(`Error during font loading: ${exportedError}`); + this.commonObjs.resolve(id, exportedError); break; - default: - throw new Error(`Got unknown object type ${type}`); - } - }, this); - - messageHandler.on('DocProgress', function transportDocProgress(data) { - if (this.destroyed) { - return; // Ignore any pending requests if the worker was terminated. - } + } - var loadingTask = this.loadingTask; - if (loadingTask.onProgress) { - loadingTask.onProgress({ - loaded: data.loaded, - total: data.total, + let fontRegistry = null; + if (params.pdfBug && globalScope.FontInspector && + globalScope.FontInspector.enabled) { + fontRegistry = { + registerFont(font, url) { + globalScope['FontInspector'].fontAdded(font, url); + }, + }; + } + const font = new FontFaceObject(exportedData, { + isEvalSupported: params.isEvalSupported, + disableFontFace: params.disableFontFace, + ignoreErrors: params.ignoreErrors, + onUnsupportedFeature: this._onUnsupportedFeature.bind(this), + fontRegistry, }); - } - }, this); + const fontReady = (fontObjs) => { + this.commonObjs.resolve(id, font); + }; - messageHandler.on('PageError', function transportError(data) { - if (this.destroyed) { - return; // Ignore any pending requests if the worker was terminated. - } + this.fontLoader.bind([font], fontReady); + break; + case 'FontPath': + this.commonObjs.resolve(id, exportedData); + break; + default: + throw new Error(`Got unknown common object type ${type}`); + } + }, this); - var page = this.pageCache[data.pageNum - 1]; - var intentState = page.intentStates[data.intent]; + messageHandler.on('obj', function(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } - if (intentState.displayReadyCapability) { - intentState.displayReadyCapability.reject(data.error); - } else { - throw new Error(data.error); - } + const [id, pageIndex, type, imageData] = data; + const pageProxy = this.pageCache[pageIndex]; + if (pageProxy.objs.hasData(id)) { + return; + } - if (intentState.operatorList) { - // Mark operator list as complete. - intentState.operatorList.lastChunk = true; - for (var i = 0; i < intentState.renderTasks.length; i++) { - intentState.renderTasks[i].operatorListChanged(); + switch (type) { + case 'JpegStream': + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = function() { + resolve(img); + }; + img.onerror = function() { + reject(new Error('Error during JPEG image loading')); + // Note that when the browser image loading/decoding fails, + // we'll fallback to the built-in PDF.js JPEG decoder; see + // `PartialEvaluator.buildPaintImageXObject` in the + // `src/core/evaluator.js` file. + }; + img.src = imageData; + }).then((img) => { + pageProxy.objs.resolve(id, img); + }); + case 'Image': + pageProxy.objs.resolve(id, imageData); + + // Heuristic that will allow us not to store large data. + const MAX_IMAGE_SIZE_TO_STORE = 8000000; + if (imageData && 'data' in imageData && + imageData.data.length > MAX_IMAGE_SIZE_TO_STORE) { + pageProxy.cleanupAfterRender = true; } - } - }, this); + break; + default: + throw new Error(`Got unknown object type ${type}`); + } + }, this); - messageHandler.on('UnsupportedFeature', this._onUnsupportedFeature, this); + messageHandler.on('DocProgress', function(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } - messageHandler.on('JpegDecode', function(data) { - if (this.destroyed) { - return Promise.reject(new Error('Worker was destroyed')); - } + if (loadingTask.onProgress) { + loadingTask.onProgress({ + loaded: data.loaded, + total: data.total, + }); + } + }, this); - if (typeof document === 'undefined') { - // Make sure that this code is not executing in node.js, as - // it's using DOM image, and there is no library to support that. - return Promise.reject(new Error('"document" is not defined.')); - } + messageHandler.on('PageError', function(data) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } - var imageUrl = data[0]; - var components = data[1]; - if (components !== 3 && components !== 1) { - return Promise.reject( - new Error('Only 3 components or 1 component can be returned')); - } + const page = this.pageCache[data.pageNum - 1]; + const intentState = page.intentStates[data.intent]; - return new Promise(function (resolve, reject) { - var img = new Image(); - img.onload = function () { - var width = img.width; - var height = img.height; - var size = width * height; - var rgbaLength = size * 4; - var buf = new Uint8ClampedArray(size * components); - var tmpCanvas = document.createElement('canvas'); - tmpCanvas.width = width; - tmpCanvas.height = height; - var tmpCtx = tmpCanvas.getContext('2d'); - tmpCtx.drawImage(img, 0, 0); - var data = tmpCtx.getImageData(0, 0, width, height).data; - var i, j; - - if (components === 3) { - for (i = 0, j = 0; i < rgbaLength; i += 4, j += 3) { - buf[j] = data[i]; - buf[j + 1] = data[i + 1]; - buf[j + 2] = data[i + 2]; - } - } else if (components === 1) { - for (i = 0, j = 0; i < rgbaLength; i += 4, j++) { - buf[j] = data[i]; - } - } - resolve({ data: buf, width, height, }); - }; - img.onerror = function () { - reject(new Error('JpegDecode failed to load image')); - }; - img.src = imageUrl; - }); - }, this); + if (intentState.displayReadyCapability) { + intentState.displayReadyCapability.reject(data.error); + } else { + throw new Error(data.error); + } - messageHandler.on('FetchBuiltInCMap', function (data) { - if (this.destroyed) { - return Promise.reject(new Error('Worker was destroyed')); + if (intentState.operatorList) { + // Mark operator list as complete. + intentState.operatorList.lastChunk = true; + for (let i = 0; i < intentState.renderTasks.length; i++) { + intentState.renderTasks[i].operatorListChanged(); } - return this.CMapReaderFactory.fetch({ - name: data.name, - }); - }, this); - }, + } + }, this); + + messageHandler.on('UnsupportedFeature', this._onUnsupportedFeature, this); - _onUnsupportedFeature({ featureId, }) { + messageHandler.on('JpegDecode', function(data) { if (this.destroyed) { - return; // Ignore any pending requests if the worker was terminated. + return Promise.reject(new Error('Worker was destroyed')); } - let loadingTask = this.loadingTask; - if (loadingTask.onUnsupportedFeature) { - loadingTask.onUnsupportedFeature(featureId); - } - }, - - getData: function WorkerTransport_getData() { - return this.messageHandler.sendWithPromise('GetData', null); - }, - getPage(pageNumber) { - if (!Number.isInteger(pageNumber) || - pageNumber <= 0 || pageNumber > this.numPages) { - return Promise.reject(new Error('Invalid page request')); + if (typeof document === 'undefined') { + // Make sure that this code is not executing in node.js, as + // it's using DOM image, and there is no library to support that. + return Promise.reject(new Error('"document" is not defined.')); } - var pageIndex = pageNumber - 1; - if (pageIndex in this.pagePromises) { - return this.pagePromises[pageIndex]; + const [imageUrl, components] = data; + if (components !== 3 && components !== 1) { + return Promise.reject( + new Error('Only 3 components or 1 component can be returned')); } - var promise = this.messageHandler.sendWithPromise('GetPage', { - pageIndex, - }).then((pageInfo) => { - if (this.destroyed) { - throw new Error('Transport destroyed'); - } - let page = new PDFPageProxy(pageIndex, pageInfo, this, - this._params.pdfBug); - this.pageCache[pageIndex] = page; - return page; - }); - this.pagePromises[pageIndex] = promise; - return promise; - }, - getPageIndex: function WorkerTransport_getPageIndexByRef(ref) { - return this.messageHandler.sendWithPromise('GetPageIndex', { - ref, - }).catch(function (reason) { - return Promise.reject(new Error(reason)); + return new Promise(function (resolve, reject) { + const img = new Image(); + img.onload = function () { + const width = img.width; + const height = img.height; + const size = width * height; + const rgbaLength = size * 4; + const buf = new Uint8ClampedArray(size * components); + const tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = width; + tmpCanvas.height = height; + const tmpCtx = tmpCanvas.getContext('2d'); + tmpCtx.drawImage(img, 0, 0); + const data = tmpCtx.getImageData(0, 0, width, height).data; + + if (components === 3) { + for (let i = 0, j = 0; i < rgbaLength; i += 4, j += 3) { + buf[j] = data[i]; + buf[j + 1] = data[i + 1]; + buf[j + 2] = data[i + 2]; + } + } else if (components === 1) { + for (let i = 0, j = 0; i < rgbaLength; i += 4, j++) { + buf[j] = data[i]; + } + } + resolve({ data: buf, width, height, }); + }; + img.onerror = function () { + reject(new Error('JpegDecode failed to load image')); + }; + img.src = imageUrl; }); - }, + }, this); - getAnnotations: function WorkerTransport_getAnnotations(pageIndex, intent) { - return this.messageHandler.sendWithPromise('GetAnnotations', { - pageIndex, - intent, + messageHandler.on('FetchBuiltInCMap', function(data) { + if (this.destroyed) { + return Promise.reject(new Error('Worker was destroyed')); + } + return this.CMapReaderFactory.fetch({ + name: data.name, }); - }, + }, this); + } - getDestinations: function WorkerTransport_getDestinations() { - return this.messageHandler.sendWithPromise('GetDestinations', null); - }, + _onUnsupportedFeature({ featureId, }) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } + if (this.loadingTask.onUnsupportedFeature) { + this.loadingTask.onUnsupportedFeature(featureId); + } + } + + getData() { + return this.messageHandler.sendWithPromise('GetData', null); + } + + getPage(pageNumber) { + if (!Number.isInteger(pageNumber) || + pageNumber <= 0 || pageNumber > this.numPages) { + return Promise.reject(new Error('Invalid page request')); + } - getDestination: function WorkerTransport_getDestination(id) { - if (typeof id !== 'string') { - return Promise.reject(new Error('Invalid destination request.')); + const pageIndex = pageNumber - 1; + if (pageIndex in this.pagePromises) { + return this.pagePromises[pageIndex]; + } + const promise = this.messageHandler.sendWithPromise('GetPage', { + pageIndex, + }).then((pageInfo) => { + if (this.destroyed) { + throw new Error('Transport destroyed'); } - return this.messageHandler.sendWithPromise('GetDestination', { - id, - }); - }, + const page = new PDFPageProxy(pageIndex, pageInfo, this, + this._params.pdfBug); + this.pageCache[pageIndex] = page; + return page; + }); + this.pagePromises[pageIndex] = promise; + return promise; + } - getPageLabels: function WorkerTransport_getPageLabels() { - return this.messageHandler.sendWithPromise('GetPageLabels', null); - }, + getPageIndex(ref) { + return this.messageHandler.sendWithPromise('GetPageIndex', { + ref, + }).catch(function(reason) { + return Promise.reject(new Error(reason)); + }); + } - getPageMode() { - return this.messageHandler.sendWithPromise('GetPageMode', null); - }, + getAnnotations(pageIndex, intent) { + return this.messageHandler.sendWithPromise('GetAnnotations', { + pageIndex, + intent, + }); + } - getAttachments: function WorkerTransport_getAttachments() { - return this.messageHandler.sendWithPromise('GetAttachments', null); - }, + getDestinations() { + return this.messageHandler.sendWithPromise('GetDestinations', null); + } - getJavaScript: function WorkerTransport_getJavaScript() { - return this.messageHandler.sendWithPromise('GetJavaScript', null); - }, + getDestination(id) { + if (typeof id !== 'string') { + return Promise.reject(new Error('Invalid destination request.')); + } + return this.messageHandler.sendWithPromise('GetDestination', { + id, + }); + } - getOutline: function WorkerTransport_getOutline() { - return this.messageHandler.sendWithPromise('GetOutline', null); - }, + getPageLabels() { + return this.messageHandler.sendWithPromise('GetPageLabels', null); + } - getMetadata: function WorkerTransport_getMetadata() { - return this.messageHandler.sendWithPromise('GetMetadata', null). - then((results) => { - return { - info: results[0], - metadata: (results[1] ? new Metadata(results[1]) : null), - contentDispositionFilename: (this._fullReader ? - this._fullReader.filename : null), - }; - }); - }, + getPageMode() { + return this.messageHandler.sendWithPromise('GetPageMode', null); + } - getStats: function WorkerTransport_getStats() { - return this.messageHandler.sendWithPromise('GetStats', null); - }, + getAttachments() { + return this.messageHandler.sendWithPromise('GetAttachments', null); + } - startCleanup: function WorkerTransport_startCleanup() { - this.messageHandler.sendWithPromise('Cleanup', null).then(() => { - for (var i = 0, ii = this.pageCache.length; i < ii; i++) { - var page = this.pageCache[i]; - if (page) { - page.cleanup(); - } - } - this.commonObjs.clear(); - this.fontLoader.clear(); - }); - }, + getJavaScript() { + return this.messageHandler.sendWithPromise('GetJavaScript', null); + } - get loadingParams() { - let params = this._params; - return shadow(this, 'loadingParams', { - disableAutoFetch: params.disableAutoFetch, - disableCreateObjectURL: params.disableCreateObjectURL, - disableFontFace: params.disableFontFace, - nativeImageDecoderSupport: params.nativeImageDecoderSupport, - }); - }, - }; - return WorkerTransport; + getOutline() { + return this.messageHandler.sendWithPromise('GetOutline', null); + } -})(); + getPermissions() { + return this.messageHandler.sendWithPromise('GetPermissions', null); + } + + getMetadata() { + return this.messageHandler.sendWithPromise('GetMetadata', null). + then((results) => { + return { + info: results[0], + metadata: (results[1] ? new Metadata(results[1]) : null), + contentDispositionFilename: (this._fullReader ? + this._fullReader.filename : null), + }; + }); + } + + getStats() { + return this.messageHandler.sendWithPromise('GetStats', null); + } + + startCleanup() { + this.messageHandler.sendWithPromise('Cleanup', null).then(() => { + for (let i = 0, ii = this.pageCache.length; i < ii; i++) { + const page = this.pageCache[i]; + if (page) { + page.cleanup(); + } + } + this.commonObjs.clear(); + this.fontLoader.clear(); + }); + } + + get loadingParams() { + const params = this._params; + return shadow(this, 'loadingParams', { + disableAutoFetch: params.disableAutoFetch, + disableCreateObjectURL: params.disableCreateObjectURL, + disableFontFace: params.disableFontFace, + nativeImageDecoderSupport: params.nativeImageDecoderSupport, + }); + } +} /** * A PDF document and page is built of many objects. E.g. there are objects diff --git a/src/shared/util.js b/src/shared/util.js index 80115a1021533..b823e6bdc8cb0 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -25,6 +25,18 @@ const NativeImageDecoding = { DISPLAY: 'display', }; +// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification. +const PermissionFlag = { + PRINT: 0x04, + MODIFY_CONTENTS: 0x08, + COPY: 0x10, + MODIFY_ANNOTATIONS: 0x20, + FILL_INTERACTIVE_FORMS: 0x100, + COPY_FOR_ACCESSIBILITY: 0x200, + ASSEMBLE: 0x400, + PRINT_HIGH_QUALITY: 0x800, +}; + var TextRenderingMode = { FILL: 0, STROKE: 1, @@ -1014,6 +1026,7 @@ export { NativeImageDecoding, PasswordException, PasswordResponses, + PermissionFlag, StreamType, TextRenderingMode, UnexpectedResponseException, diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 59475f4e8f2ec..1d1e661148ab7 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -325,3 +325,6 @@ !transparent.pdf !xobject-image.pdf !ccitt_EndOfBlock_false.pdf +!issue9972-1.pdf +!issue9972-2.pdf +!issue9972-3.pdf diff --git a/test/pdfs/issue9972-1.pdf b/test/pdfs/issue9972-1.pdf new file mode 100644 index 0000000000000..4ce8d3ff1eda7 Binary files /dev/null and b/test/pdfs/issue9972-1.pdf differ diff --git a/test/pdfs/issue9972-2.pdf b/test/pdfs/issue9972-2.pdf new file mode 100644 index 0000000000000..381eae5065347 Binary files /dev/null and b/test/pdfs/issue9972-2.pdf differ diff --git a/test/pdfs/issue9972-3.pdf b/test/pdfs/issue9972-3.pdf new file mode 100644 index 0000000000000..a5c9d1912830d Binary files /dev/null and b/test/pdfs/issue9972-3.pdf differ diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 35f415f2a7773..ebdc3d6d5fc3b 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -18,7 +18,8 @@ import { } from './test_utils'; import { createPromiseCapability, FontType, InvalidPDFException, MissingPDFException, - OPS, PasswordException, PasswordResponses, StreamType, stringToBytes + OPS, PasswordException, PasswordResponses, PermissionFlag, StreamType, + stringToBytes } from '../../src/shared/util'; import { DOMCanvasFactory, RenderingCancelledException, StatTimer @@ -84,9 +85,7 @@ describe('api', function() { expect(data[1] instanceof PDFDocumentProxy).toEqual(true); expect(loadingTask).toEqual(data[1].loadingTask); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('creates pdf doc from URL and aborts before worker initialized', function(done) { @@ -111,9 +110,7 @@ describe('api', function() { destroyed.then(function (data) { expect(true).toEqual(true); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('creates pdf doc from typed array', function(done) { var typedArrayPdf; @@ -149,9 +146,7 @@ describe('api', function() { loadingTask.promise.then(function(data) { expect(data instanceof PDFDocumentProxy).toEqual(true); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('creates pdf doc from invalid PDF file', function(done) { // A severely corrupt PDF file (even Adobe Reader fails to open it). @@ -213,9 +208,7 @@ describe('api', function() { Promise.all(promises).then(function (data) { expect(data[2] instanceof PDFDocumentProxy).toEqual(true); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('creates pdf doc from PDF file protected with only a user password', function (done) { @@ -257,9 +250,7 @@ describe('api', function() { }); Promise.all([result1, result2, result3]).then(function () { done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('creates pdf doc from password protected PDF file and aborts/throws ' + @@ -309,9 +300,7 @@ describe('api', function() { Promise.all([result1, result2]).then(function () { done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); }); @@ -333,9 +322,7 @@ describe('api', function() { expect(!!worker.port).toEqual(false); expect(worker.destroyed).toEqual(true); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('worker created or destroyed by getDocument', function (done) { if (isNodeJS()) { @@ -357,9 +344,7 @@ describe('api', function() { expect(!!destroyedWorker).toEqual(false); expect(worker.destroyed).toEqual(true); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('worker created and can be used in getDocument', function (done) { if (isNodeJS()) { @@ -386,9 +371,7 @@ describe('api', function() { expect(worker.destroyed).toEqual(false); worker.destroy(); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('creates more than one worker', function (done) { if (isNodeJS()) { @@ -408,9 +391,7 @@ describe('api', function() { worker2.destroy(); worker3.destroy(); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets current workerSrc', function() { if (isNodeJS()) { @@ -452,9 +433,7 @@ describe('api', function() { expect(data instanceof PDFPageProxy).toEqual(true); expect(data.pageIndex).toEqual(0); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets non-existent page', function(done) { var outOfRangePromise = doc.getPage(100); @@ -480,9 +459,7 @@ describe('api', function() { Promise.all([outOfRangePromise, nonIntegerPromise, nonNumberPromise]). then(function () { done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets page index', function(done) { // reference to second page @@ -491,9 +468,7 @@ describe('api', function() { promise.then(function(pageIndex) { expect(pageIndex).toEqual(1); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets invalid page index', function (done) { var ref = { num: 3, gen: 0, }; // Reference to a font dictionary. @@ -513,9 +488,7 @@ describe('api', function() { chapter1: [{ gen: 0, num: 17, }, { name: 'XYZ', }, 0, 841.89, null], }); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets a destination, from /Dests dictionary', function(done) { var promise = doc.getDestination('chapter1'); @@ -523,9 +496,7 @@ describe('api', function() { expect(data).toEqual([{ gen: 0, num: 17, }, { name: 'XYZ', }, 0, 841.89, null]); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets a non-existent destination, from /Dests dictionary', function(done) { @@ -533,9 +504,7 @@ describe('api', function() { promise.then(function(data) { expect(data).toEqual(null); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets destinations, from /Names (NameTree) dictionary', function(done) { @@ -550,9 +519,7 @@ describe('api', function() { }); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets a destination, from /Names (NameTree) dictionary', function(done) { var loadingTask = getDocument(buildGetDocumentParams('issue6204.pdf')); @@ -564,9 +531,7 @@ describe('api', function() { 0, 375, null]); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets a non-existent destination, from /Names (NameTree) dictionary', function(done) { @@ -578,9 +543,7 @@ describe('api', function() { expect(destination).toEqual(null); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets non-string destination', function(done) { @@ -614,9 +577,7 @@ describe('api', function() { promise.then(function (data) { expect(data).toEqual(null); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets page labels', function (done) { // PageLabels with Roman/Arabic numerals. @@ -657,9 +618,7 @@ describe('api', function() { loadingTask2.destroy(), loadingTask3.destroy() ]).then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets default page mode', function(done) { @@ -671,17 +630,13 @@ describe('api', function() { expect(mode).toEqual('UseNone'); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets non-default page mode', function(done) { doc.getPageMode().then(function(mode) { expect(mode).toEqual('UseOutlines'); done(); - }).catch(function(reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets non-existent attachments', function(done) { @@ -689,9 +644,7 @@ describe('api', function() { promise.then(function (data) { expect(data).toEqual(null); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets attachments', function(done) { if (isNodeJS()) { // The PDF file used is a linked test-case. @@ -708,9 +661,7 @@ describe('api', function() { expect(attachment.content.length).toEqual(30098); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets javascript', function(done) { @@ -718,9 +669,7 @@ describe('api', function() { promise.then(function (data) { expect(data).toEqual(null); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); // Keep this in sync with the pattern in viewer.js. The pattern is used to // detect whether or not to automatically start printing. @@ -736,9 +685,7 @@ describe('api', function() { expect(data).toEqual(['print({});']); expect(data[0]).toMatch(viewerPrintRegExp); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets javascript with printing instructions (JS action)', function(done) { @@ -752,9 +699,7 @@ describe('api', function() { ['this.print({bUI:true,bSilent:false,bShrinkToFit:true});']); expect(data[0]).toMatch(viewerPrintRegExp); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets non-existent outline', function(done) { var loadingTask = getDocument(buildGetDocumentParams('tracemonkey.pdf')); @@ -766,9 +711,7 @@ describe('api', function() { expect(outline).toEqual(null); loadingTask.destroy().then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets outline', function(done) { var promise = doc.getOutline(); @@ -791,9 +734,7 @@ describe('api', function() { expect(outlineItem.items.length).toEqual(1); expect(outlineItem.items[0].title).toEqual('Paragraph 1.1'); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets outline containing a url', function(done) { var loadingTask = getDocument(buildGetDocumentParams('issue3214.pdf')); @@ -818,10 +759,61 @@ describe('api', function() { loadingTask.destroy().then(done); }); - }).catch(function (reason) { - done.fail(reason); + }).catch(done.fail); + }); + + it('gets non-existent permissions', function(done) { + doc.getPermissions().then(function(permissions) { + expect(permissions).toEqual(null); + + done(); + }).catch(done.fail); + }); + + it('gets permissions', function (done) { + // Editing not allowed. + const loadingTask0 = + getDocument(buildGetDocumentParams('issue9972-1.pdf')); + const promise0 = loadingTask0.promise.then(function(pdfDocument) { + return pdfDocument.getPermissions(); + }); + + // Printing not allowed. + const loadingTask1 = + getDocument(buildGetDocumentParams('issue9972-2.pdf')); + const promise1 = loadingTask1.promise.then(function(pdfDocument) { + return pdfDocument.getPermissions(); + }); + + // Copying not allowed. + const loadingTask2 = + getDocument(buildGetDocumentParams('issue9972-3.pdf')); + const promise2 = loadingTask2.promise.then(function(pdfDocument) { + return pdfDocument.getPermissions(); }); + + const totalPermissionCount = Object.keys(PermissionFlag).length; + Promise.all([promise0, promise1, promise2]).then(function(permissions) { + expect(permissions[0].length).toEqual(totalPermissionCount - 1); + expect(permissions[0].includes(PermissionFlag.MODIFY_CONTENTS)) + .toBeFalsy(); + + expect(permissions[1].length).toEqual(totalPermissionCount - 2); + expect(permissions[1].includes(PermissionFlag.PRINT)).toBeFalsy(); + expect(permissions[1].includes(PermissionFlag.PRINT_HIGH_QUALITY)) + .toBeFalsy(); + + expect(permissions[2].length).toEqual(totalPermissionCount - 1); + expect(permissions[2].includes(PermissionFlag.COPY)).toBeFalsy(); + + Promise.all([ + loadingTask0.destroy(), + loadingTask1.destroy(), + loadingTask2.destroy(), + ]).then(done); + }).catch(done.fail); }); + it('gets metadata', function(done) { var promise = doc.getMetadata(); promise.then(function({ info, metadata, contentDispositionFilename, }) { @@ -837,9 +829,7 @@ describe('api', function() { expect(contentDispositionFilename).toEqual(null); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets data', function(done) { var promise = doc.getData(); @@ -847,27 +837,21 @@ describe('api', function() { expect(data instanceof Uint8Array).toEqual(true); expect(data.length).toEqual(basicApiFileLength); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets download info', function(done) { var promise = doc.getDownloadInfo(); promise.then(function (data) { expect(data).toEqual({ length: basicApiFileLength, }); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets document stats', function(done) { var promise = doc.getStats(); promise.then(function (stats) { expect(stats).toEqual({ streamTypes: [], fontTypes: [], }); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('checks that fingerprints are unique', function(done) { @@ -892,9 +876,7 @@ describe('api', function() { loadingTask1.destroy(), loadingTask2.destroy() ]).then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); describe('Cross-origin', function() { @@ -988,9 +970,7 @@ describe('api', function() { page = data; done(); }); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); afterAll(function(done) { @@ -1050,9 +1030,7 @@ describe('api', function() { Promise.all([defaultPromise, displayPromise, printPromise]).then( function () { done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets annotations containing relative URLs (bug 766086)', @@ -1112,9 +1090,7 @@ describe('api', function() { docBaseUrlLoadingTask.destroy(), invalidDocBaseUrlLoadingTask.destroy() ]).then(done); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets text content', function (done) { @@ -1136,9 +1112,7 @@ describe('api', function() { // A simple check that ensures the two `textContent` object match. expect(JSON.stringify(data[0])).toEqual(JSON.stringify(data[1])); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets operator list', function(done) { var promise = page.getOperatorList(); @@ -1147,9 +1121,7 @@ describe('api', function() { expect(!!oplist.argsArray).toEqual(true); expect(oplist.lastChunk).toEqual(true); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets operatorList with JPEG image (issue 4888)', function(done) { let loadingTask = getDocument(buildGetDocumentParams('cmykjpeg.pdf')); @@ -1166,9 +1138,7 @@ describe('api', function() { done(); }); }); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets document stats after parsing page', function(done) { var promise = page.getOperatorList().then(function () { @@ -1184,9 +1154,7 @@ describe('api', function() { expect(stats).toEqual({ streamTypes: expectedStreamTypes, fontTypes: expectedFontTypes, }); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('gets page stats after parsing page, without `pdfBug` set', @@ -1420,9 +1388,7 @@ describe('api', function() { }); promiseDone.then(function() { done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); }); describe('PDFDataRangeTransport', function () { @@ -1479,9 +1445,7 @@ describe('api', function() { expect(page.rotate).toEqual(0); expect(fetches).toBeGreaterThan(2); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); it('should fetch document info and page using range and streaming', function (done) { @@ -1520,9 +1484,7 @@ describe('api', function() { expect(page.rotate).toEqual(0); expect(fetches).toEqual(1); done(); - }).catch(function (reason) { - done.fail(reason); - }); + }).catch(done.fail); }); }); });