From 311bac3ebba86b2a8f456882e1dc682c10fc72be Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 14 Apr 2019 13:13:59 +0200 Subject: [PATCH] [api-minor] Add support for ViewerPreferences in the API (issue 10736) Please see the specification, https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf#M11.9.12864.1Heading.71.Viewer.Preferences Furthermore, note that this patch *only* adds API support and unit-tests but does not attempt to integrate e.g. the `ViewerPreferences -> Direction` property into the viewer (which would be necessary to address issue 10736). The reason for this is that it's not entirely clear to me exactly if/how that could be implemented; e.g. would it be as simple as setting the `dir` attribute on the `viewerContainer` DOM element, or will it be more complicated? There's also the question of how the `ViewerPreferences -> Direction` value interacts with the `PageMode`, and this will generally require a fair bit of manual testing. Since the direction of the *entire* viewer depends on the browser locale, there's also a somewhat open question regarding what default value to use for different locales. Finally, if the viewer supports `ViewerPreferences -> Direction` then I'm assuming that it will be necessary to allow users to override the default value, which will require (most likely) new `SecondaryToolbar` buttons and icons for those etc. Hence this patch only lays the necessary foundation for eventually addressing issue 10736, but defers the actual implementation until later. (Time permitting, I'll try to look into the viewer part later.) --- src/core/obj.js | 137 +++++++++++++++++++++++++++++++++++++++++- src/core/worker.js | 6 +- src/display/api.js | 14 ++++- test/unit/api_spec.js | 26 +++++++- 4 files changed, 176 insertions(+), 7 deletions(-) diff --git a/src/core/obj.js b/src/core/obj.js index 434b5831e..696c007bd 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -14,9 +14,10 @@ */ import { - bytesToString, createPromiseCapability, createValidAbsoluteUrl, FormatError, - info, InvalidPDFException, isBool, isNum, isString, PermissionFlag, shadow, - stringToPDFString, stringToUTF8String, unreachable, warn + assert, bytesToString, createPromiseCapability, createValidAbsoluteUrl, + FormatError, info, InvalidPDFException, isBool, isNum, isString, + PermissionFlag, shadow, stringToPDFString, stringToUTF8String, unreachable, + warn } from '../shared/util'; import { Dict, isCmd, isDict, isName, isRef, isRefsEqual, isStream, Ref, RefSet, @@ -415,6 +416,136 @@ class Catalog { return shadow(this, 'pageMode', pageMode); } + get viewerPreferences() { + const ViewerPreferencesValidators = { + HideToolbar: isBool, + HideMenubar: isBool, + HideWindowUI: isBool, + FitWindow: isBool, + CenterWindow: isBool, + DisplayDocTitle: isBool, + NonFullScreenPageMode: isName, + Direction: isName, + ViewArea: isName, + ViewClip: isName, + PrintArea: isName, + PrintClip: isName, + PrintScaling: isName, + Duplex: isName, + PickTrayByPDFSize: isBool, + PrintPageRange: Array.isArray, + NumCopies: Number.isInteger, + }; + + const obj = this.catDict.get('ViewerPreferences'); + const prefs = Object.create(null); + + if (isDict(obj)) { + for (const key in ViewerPreferencesValidators) { + if (!obj.has(key)) { + continue; + } + const value = obj.get(key); + // Make sure the (standard) value conforms to the specification. + if (!ViewerPreferencesValidators[key](value)) { + info(`Bad value in ViewerPreferences for "${key}".`); + continue; + } + let prefValue; + + switch (key) { + case 'NonFullScreenPageMode': + switch (value.name) { + case 'UseNone': + case 'UseOutlines': + case 'UseThumbs': + case 'UseOC': + prefValue = value.name; + break; + default: + prefValue = 'UseNone'; + } + break; + case 'Direction': + switch (value.name) { + case 'L2R': + case 'R2L': + prefValue = value.name; + break; + default: + prefValue = 'L2R'; + } + break; + case 'ViewArea': + case 'ViewClip': + case 'PrintArea': + case 'PrintClip': + switch (value.name) { + case 'MediaBox': + case 'CropBox': + case 'BleedBox': + case 'TrimBox': + case 'ArtBox': + prefValue = value.name; + break; + default: + prefValue = 'CropBox'; + } + break; + case 'PrintScaling': + switch (value.name) { + case 'None': + case 'AppDefault': + prefValue = value.name; + break; + default: + prefValue = 'AppDefault'; + } + break; + case 'Duplex': + switch (value.name) { + case 'Simplex': + case 'DuplexFlipShortEdge': + case 'DuplexFlipLongEdge': + prefValue = value.name; + break; + default: + prefValue = 'None'; + } + break; + case 'PrintPageRange': + const length = value.length; + if (length % 2 !== 0) { // The number of elements must be even. + break; + } + const isValid = value.every((page, i, arr) => { + return (Number.isInteger(page) && page > 0) && + (i === 0 || page >= arr[i - 1]) && page <= this.numPages; + }); + if (isValid) { + prefValue = value; + } + break; + case 'NumCopies': + if (value > 0) { + prefValue = value; + } + break; + default: + assert(typeof value === 'boolean'); + prefValue = value; + } + + if (prefValue !== undefined) { + prefs[key] = prefValue; + } else { + info(`Bad value in ViewerPreferences for "${key}".`); + } + } + } + return shadow(this, 'viewerPreferences', prefs); + } + get openActionDestination() { const obj = this.catDict.get('OpenAction'); let openActionDest = null; diff --git a/src/core/worker.js b/src/core/worker.js index c1684c0fc..fee45edaf 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -525,7 +525,11 @@ var WorkerMessageHandler = { return pdfManager.ensureCatalog('pageMode'); }); - handler.on('getOpenActionDestination', function(data) { + handler.on('GetViewerPreferences', function(data) { + return pdfManager.ensureCatalog('viewerPreferences'); + }); + + handler.on('GetOpenActionDestination', function(data) { return pdfManager.ensureCatalog('openActionDestination'); }); diff --git a/src/display/api.js b/src/display/api.js index e73046361..4da92ae93 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -674,6 +674,14 @@ class PDFDocumentProxy { return this._transport.getPageMode(); } + /** + * @return {Promise} A promise that is resolved with an {Object} containing + * the viewer preferences. + */ + getViewerPreferences() { + return this._transport.getViewerPreferences(); + } + /** * @return {Promise} A promise that is resolved with an {Array} containing the * destination, or `null` when no open action is present in the PDF file. @@ -2246,8 +2254,12 @@ class WorkerTransport { return this.messageHandler.sendWithPromise('GetPageMode', null); } + getViewerPreferences() { + return this.messageHandler.sendWithPromise('GetViewerPreferences', null); + } + getOpenActionDestination() { - return this.messageHandler.sendWithPromise('getOpenActionDestination', + return this.messageHandler.sendWithPromise('GetOpenActionDestination', null); } diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index f0cb6d6b8..de132ddba 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -18,8 +18,9 @@ import { NodeFileReaderFactory, TEST_PDFS_PATH } from './test_utils'; import { - createPromiseCapability, FontType, InvalidPDFException, MissingPDFException, - OPS, PasswordException, PasswordResponses, PermissionFlag, StreamType + createPromiseCapability, FontType, InvalidPDFException, isEmptyObj, + MissingPDFException, OPS, PasswordException, PasswordResponses, + PermissionFlag, StreamType } from '../../src/shared/util'; import { DOMCanvasFactory, RenderingCancelledException, StatTimer @@ -646,6 +647,27 @@ describe('api', function() { }).catch(done.fail); }); + it('gets default viewer preferences', function(done) { + var loadingTask = getDocument(buildGetDocumentParams('tracemonkey.pdf')); + + loadingTask.promise.then(function(pdfDocument) { + return pdfDocument.getViewerPreferences(); + }).then(function(prefs) { + expect(typeof prefs === 'object' && prefs !== null && + isEmptyObj(prefs)).toEqual(true); + + loadingTask.destroy().then(done); + }).catch(done.fail); + }); + it('gets non-default viewer preferences', function(done) { + doc.getViewerPreferences().then(function(prefs) { + expect(prefs).toEqual({ + Direction: 'L2R', + }); + done(); + }).catch(done.fail); + }); + it('gets default open action destination', function(done) { var loadingTask = getDocument(buildGetDocumentParams('tracemonkey.pdf'));