Merge pull request #7341 from Snuffleupagus/getDestinationHash-Array

[api-minor] Improve handling of links that are using explicit destination arrays
This commit is contained in:
Tim van der Meij 2016-06-09 00:29:10 +02:00
commit f97d52182a
10 changed files with 201 additions and 44 deletions

View File

@ -757,9 +757,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`.

View File

@ -56,6 +56,7 @@ var isName = corePrimitives.isName;
var isCmd = corePrimitives.isCmd;
var isDict = corePrimitives.isDict;
var isRef = corePrimitives.isRef;
var isRefsEqual = corePrimitives.isRefsEqual;
var isStream = corePrimitives.isStream;
var CipherTransformFactory = coreCrypto.CipherTransformFactory;
var Lexer = coreParser.Lexer;
@ -522,7 +523,7 @@ var Catalog = (function CatalogClosure() {
return capability.promise;
},
getPageIndex: function Catalog_getPageIndex(ref) {
getPageIndex: function Catalog_getPageIndex(pageRef) {
// The page tree nodes have the count of all the leaves below them. To get
// how many pages are before we just have to walk up the tree and keep
// adding the count of siblings to the left of the node.
@ -531,15 +532,21 @@ var Catalog = (function CatalogClosure() {
var total = 0;
var parentRef;
return xref.fetchAsync(kidRef).then(function (node) {
if (isRefsEqual(kidRef, pageRef) && !isDict(node, 'Page') &&
!(isDict(node) && !node.has('Type') && node.has('Contents'))) {
throw new Error('The reference does not point to a /Page Dict.');
}
if (!node) {
return null;
}
assert(isDict(node), 'node must be a Dict.');
parentRef = node.getRaw('Parent');
return node.getAsync('Parent');
}).then(function (parent) {
if (!parent) {
return null;
}
assert(isDict(parent), 'parent must be a Dict.');
return parent.getAsync('Kids');
}).then(function (kids) {
if (!kids) {
@ -549,7 +556,7 @@ var Catalog = (function CatalogClosure() {
var found = false;
for (var i = 0; i < kids.length; i++) {
var kid = kids[i];
assert(isRef(kid), 'kids must be a ref');
assert(isRef(kid), 'kid must be a Ref.');
if (kid.num === kidRef.num) {
found = true;
break;
@ -585,7 +592,7 @@ var Catalog = (function CatalogClosure() {
});
}
return next(ref);
return next(pageRef);
}
};

View File

@ -290,6 +290,10 @@ function isRef(v) {
return v instanceof Ref;
}
function isRefsEqual(v1, v2) {
return v1.num === v2.num && v1.gen === v2.gen;
}
function isStream(v) {
return typeof v === 'object' && v !== null && v.getBytes !== undefined;
}
@ -304,5 +308,6 @@ exports.isCmd = isCmd;
exports.isDict = isDict;
exports.isName = isName;
exports.isRef = isRef;
exports.isRefsEqual = isRefsEqual;
exports.isStream = isStream;
}));

View File

@ -1683,7 +1683,12 @@ var WorkerTransport = (function WorkerTransportClosure() {
},
getPageIndex: function WorkerTransport_getPageIndexByRef(ref) {
return this.messageHandler.sendWithPromise('GetPageIndex', { ref: ref });
return this.messageHandler.sendWithPromise('GetPageIndex', { ref: ref }).
then(function (pageIndex) {
return pageIndex;
}, function (reason) {
return Promise.reject(new Error(reason));
});
},
getAnnotations: function WorkerTransport_getAnnotations(pageIndex, intent) {

View File

@ -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();
});

View File

@ -360,6 +360,16 @@ describe('api', function() {
done.fail(reason);
});
});
it('gets invalid page index', function (done) {
var ref = { num: 3, gen: 0 }; // Reference to a font dictionary.
var promise = doc.getPageIndex(ref);
promise.then(function () {
done.fail('shall fail for invalid page reference.');
}, function (data) {
expect(data instanceof Error).toEqual(true);
done();
});
});
it('gets destinations, from /Dests dictionary', function(done) {
var promise = doc.getDestinations();

View File

@ -1,5 +1,5 @@
/* globals expect, it, describe, beforeEach, Name, Dict, Ref, RefSet, Cmd,
jasmine, isDict */
jasmine, isDict, isRefsEqual */
'use strict';
@ -158,4 +158,18 @@ describe('primitives', function() {
expect(isDict(dict, 'Page')).toEqual(true);
});
});
describe('isRefsEqual', function () {
it('should handle different Refs pointing to the same object', function () {
var ref1 = new Ref(1, 0);
var ref2 = new Ref(1, 0);
expect(isRefsEqual(ref1, ref2)).toEqual(true);
});
it('should handle Refs pointing to different objects', function () {
var ref1 = new Ref(1, 0);
var ref2 = new Ref(2, 0);
expect(isRefsEqual(ref1, ref2)).toEqual(false);
});
});
});

View File

@ -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 });
});

View File

@ -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: <page-ref> </XYZ|FitXXX> <args..>
// dest array looks like that: <page-ref> </XYZ|/FitXXX> <args..>
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);
if ('search' in params) {
@ -209,7 +200,6 @@ var PDFLinkService = (function () {
this.navigateTo(params.nameddest);
return;
}
var pageNumber, dest;
if ('page' in params) {
pageNumber = (params.page | 0) || 1;
}
@ -259,13 +249,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.');
}
},
@ -323,6 +323,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;
})();

View File

@ -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;
}