diff --git a/src/core/annotation.js b/src/core/annotation.js index 55c7531a9..64a406c80 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -743,9 +743,17 @@ var LinkAnnotation = (function LinkAnnotationClosure() { if (isName(remoteDest)) { remoteDest = remoteDest.name; } - if (isString(remoteDest) && isString(url)) { + if (isString(url)) { var baseUrl = url.split('#')[0]; - url = baseUrl + '#' + remoteDest; + if (isString(remoteDest)) { + // In practice, a named destination may contain only a number. + // If that happens, use the '#nameddest=' form to avoid the link + // redirecting to a page, instead of the correct destination. + url = baseUrl + '#' + + (/^\d+$/.test(remoteDest) ? 'nameddest=' : '') + remoteDest; + } else if (isArray(remoteDest)) { + url = baseUrl + '#' + JSON.stringify(remoteDest); + } } } // The 'NewWindow' property, equal to `LinkTarget.BLANK`. diff --git a/test/unit/annotation_layer_spec.js b/test/unit/annotation_layer_spec.js index 31911bcb9..3a8ffea38 100644 --- a/test/unit/annotation_layer_spec.js +++ b/test/unit/annotation_layer_spec.js @@ -268,8 +268,9 @@ describe('Annotation layer', function() { var actionDict = new Dict(); actionDict.set('Type', Name.get('Action')); actionDict.set('S', Name.get('GoToR')); - actionDict.set('F', '../../0021/002156/215675E.pdf'); - actionDict.set('D', '15'); + actionDict.set('F', '../../0013/001346/134685E.pdf'); + actionDict.set('D', '4.3'); + actionDict.set('NewWindow', true); var annotationDict = new Dict(); annotationDict.set('Type', Name.get('Annot')); @@ -283,7 +284,58 @@ describe('Annotation layer', function() { var data = annotation.data; expect(data.annotationType).toEqual(AnnotationType.LINK); - expect(data.url).toBeUndefined(); + expect(data.url).toBeUndefined(); // ../../0013/001346/134685E.pdf#4.3 + expect(data.dest).toBeUndefined(); + expect(data.newWindow).toEqual(true); + }); + + it('should correctly parse a GoToR action, with named destination', + function() { + var actionDict = new Dict(); + actionDict.set('Type', Name.get('Action')); + actionDict.set('S', Name.get('GoToR')); + actionDict.set('F', 'http://www.example.com/test.pdf'); + actionDict.set('D', '15'); + + var annotationDict = new Dict(); + annotationDict.set('Type', Name.get('Annot')); + annotationDict.set('Subtype', Name.get('Link')); + annotationDict.set('A', actionDict); + + var xrefMock = new XrefMock([annotationDict]); + var annotationRef = new Ref(495, 0); + + var annotation = annotationFactory.create(xrefMock, annotationRef); + var data = annotation.data; + expect(data.annotationType).toEqual(AnnotationType.LINK); + + expect(data.url).toEqual('http://www.example.com/test.pdf#nameddest=15'); + expect(data.dest).toBeUndefined(); + expect(data.newWindow).toBeFalsy(); + }); + + it('should correctly parse a GoToR action, with explicit destination array', + function() { + var actionDict = new Dict(); + actionDict.set('Type', Name.get('Action')); + actionDict.set('S', Name.get('GoToR')); + actionDict.set('F', 'http://www.example.com/test.pdf'); + actionDict.set('D', [14, Name.get('XYZ'), null, 298.043, null]); + + var annotationDict = new Dict(); + annotationDict.set('Type', Name.get('Annot')); + annotationDict.set('Subtype', Name.get('Link')); + annotationDict.set('A', actionDict); + + var xrefMock = new XrefMock([annotationDict]); + var annotationRef = new Ref(489, 0); + + var annotation = annotationFactory.create(xrefMock, annotationRef); + var data = annotation.data; + expect(data.annotationType).toEqual(AnnotationType.LINK); + + expect(data.url).toEqual('http://www.example.com/test.pdf#' + + '[14,{"name":"XYZ"},null,298.043,null]'); expect(data.dest).toBeUndefined(); expect(data.newWindow).toBeFalsy(); }); diff --git a/web/app.js b/web/app.js index 155d01c36..9b8361146 100644 --- a/web/app.js +++ b/web/app.js @@ -857,6 +857,7 @@ var PDFViewerApplication = { pdfViewer.setDocument(pdfDocument); var firstPagePromise = pdfViewer.firstPagePromise; var pagesPromise = pdfViewer.pagesPromise; + var onePageRendered = pdfViewer.onePageRendered; this.pageRotation = 0; @@ -962,9 +963,8 @@ var PDFViewerApplication = { } }); - // outline depends on pagesRefMap - var promises = [pagesPromise, this.animationStartedPromise]; - Promise.all(promises).then(function() { + Promise.all([onePageRendered, this.animationStartedPromise]).then( + function() { pdfDocument.getOutline().then(function(outline) { self.pdfOutlineViewer.render({ outline: outline }); }); diff --git a/web/pdf_link_service.js b/web/pdf_link_service.js index 9228dd914..57ece6429 100644 --- a/web/pdf_link_service.js +++ b/web/pdf_link_service.js @@ -29,6 +29,11 @@ var parseQueryString = uiUtils.parseQueryString; +var PageNumberRegExp = /^\d+$/; +function isPageNumber(str) { + return PageNumberRegExp.test(str); +} + /** * @typedef {Object} PDFLinkServiceOptions * @property {EventBus} eventBus - The application event bus. @@ -40,7 +45,7 @@ var parseQueryString = uiUtils.parseQueryString; * @class * @implements {IPDFLinkService} */ -var PDFLinkService = (function () { +var PDFLinkService = (function PDFLinkServiceClosure() { /** * @constructs PDFLinkService * @param {PDFLinkServiceOptions} options @@ -100,7 +105,7 @@ var PDFLinkService = (function () { var self = this; var goToDestination = function(destRef) { - // dest array looks like that: + // dest array looks like that: var pageNumber = destRef instanceof Object ? self._pagesRefCache[destRef.num + ' ' + destRef.gen + ' R'] : (destRef + 1); @@ -150,30 +155,15 @@ var PDFLinkService = (function () { */ getDestinationHash: function PDFLinkService_getDestinationHash(dest) { if (typeof dest === 'string') { - return this.getAnchorUrl('#' + escape(dest)); + // In practice, a named destination may contain only a number. + // If that happens, use the '#nameddest=' form to avoid the link + // redirecting to a page, instead of the correct destination. + return this.getAnchorUrl( + '#' + (isPageNumber(dest) ? 'nameddest=' : '') + escape(dest)); } if (dest instanceof Array) { - var destRef = dest[0]; // see navigateTo method for dest format - var pageNumber = destRef instanceof Object ? - this._pagesRefCache[destRef.num + ' ' + destRef.gen + ' R'] : - (destRef + 1); - if (pageNumber) { - var pdfOpenParams = this.getAnchorUrl('#page=' + pageNumber); - var destKind = dest[1]; - if (typeof destKind === 'object' && 'name' in destKind && - destKind.name === 'XYZ') { - var scale = (dest[4] || this.pdfViewer.currentScaleValue); - var scaleNumber = parseFloat(scale); - if (scaleNumber) { - scale = scaleNumber * 100; - } - pdfOpenParams += '&zoom=' + scale; - if (dest[2] || dest[3]) { - pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0); - } - } - return pdfOpenParams; - } + var str = JSON.stringify(dest); + return this.getAnchorUrl('#' + escape(str)); } return this.getAnchorUrl(''); }, @@ -192,6 +182,7 @@ var PDFLinkService = (function () { * @param {string} hash */ setHash: function PDFLinkService_setHash(hash) { + var pageNumber, dest; if (hash.indexOf('=') >= 0) { var params = parseQueryString(hash); // borrowing syntax from "Parameters for Opening PDF Files" @@ -202,7 +193,6 @@ var PDFLinkService = (function () { this.navigateTo(params.nameddest); return; } - var pageNumber, dest; if ('page' in params) { pageNumber = (params.page | 0) || 1; } @@ -252,13 +242,23 @@ var PDFLinkService = (function () { mode: params.pagemode }); } - } else if (/^\d+$/.test(hash)) { // page number - this.page = hash; - } else { // named destination - if (this.pdfHistory) { - this.pdfHistory.updateNextHashParam(unescape(hash)); + } else if (isPageNumber(hash)) { // Page number. + this.page = hash | 0; + } else { // Named (or explicit) destination. + dest = unescape(hash); + try { + dest = JSON.parse(dest); + } catch (ex) {} + + if (typeof dest === 'string' || isValidExplicitDestination(dest)) { + if (this.pdfHistory) { + this.pdfHistory.updateNextHashParam(dest); + } + this.navigateTo(dest); + return; } - this.navigateTo(unescape(hash)); + console.error('PDFLinkService_setHash: \'' + unescape(hash) + + '\' is not a valid destination.'); } }, @@ -316,6 +316,60 @@ var PDFLinkService = (function () { } }; + function isValidExplicitDestination(dest) { + if (!(dest instanceof Array)) { + return false; + } + var destLength = dest.length, allowNull = true; + if (destLength < 2) { + return false; + } + var page = dest[0]; + if (!(typeof page === 'object' && + typeof page.num === 'number' && (page.num | 0) === page.num && + typeof page.gen === 'number' && (page.gen | 0) === page.gen) && + !(typeof page === 'number' && (page | 0) === page && page >= 0)) { + return false; + } + var zoom = dest[1]; + if (!(typeof zoom === 'object' && typeof zoom.name === 'string')) { + return false; + } + switch (zoom.name) { + case 'XYZ': + if (destLength !== 5) { + return false; + } + break; + case 'Fit': + case 'FitB': + return destLength === 2; + case 'FitH': + case 'FitBH': + case 'FitV': + case 'FitBV': + if (destLength !== 3) { + return false; + } + break; + case 'FitR': + if (destLength !== 6) { + return false; + } + allowNull = false; + break; + default: + return false; + } + for (var i = 2; i < destLength; i++) { + var param = dest[i]; + if (!(typeof param === 'number' || (allowNull && param === null))) { + return false; + } + } + return true; + } + return PDFLinkService; })(); diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 1a79b3745..d60d13cfb 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -587,6 +587,8 @@ var PDFViewer = (function pdfViewer() { scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); break; default: + console.error('PDFViewer_scrollPageIntoView: \'' + dest[1].name + + '\' is not a valid destination type.'); return; }