Browse Source

Merge pull request #7341 from Snuffleupagus/getDestinationHash-Array

[api-minor] Improve handling of links that are using explicit destination arrays
Tim van der Meij 9 years ago
parent
commit
f97d52182a
  1. 12
      src/core/annotation.js
  2. 13
      src/core/obj.js
  3. 5
      src/core/primitives.js
  4. 7
      src/display/api.js
  5. 56
      test/unit/annotation_layer_spec.js
  6. 10
      test/unit/api_spec.js
  7. 16
      test/unit/primitives_spec.js
  8. 6
      web/app.js
  9. 116
      web/pdf_link_service.js
  10. 2
      web/pdf_viewer.js

12
src/core/annotation.js

@ -757,9 +757,17 @@ var LinkAnnotation = (function LinkAnnotationClosure() {
if (isName(remoteDest)) { if (isName(remoteDest)) {
remoteDest = remoteDest.name; remoteDest = remoteDest.name;
} }
if (isString(remoteDest) && isString(url)) { if (isString(url)) {
var baseUrl = url.split('#')[0]; 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`. // The 'NewWindow' property, equal to `LinkTarget.BLANK`.

13
src/core/obj.js

@ -56,6 +56,7 @@ var isName = corePrimitives.isName;
var isCmd = corePrimitives.isCmd; var isCmd = corePrimitives.isCmd;
var isDict = corePrimitives.isDict; var isDict = corePrimitives.isDict;
var isRef = corePrimitives.isRef; var isRef = corePrimitives.isRef;
var isRefsEqual = corePrimitives.isRefsEqual;
var isStream = corePrimitives.isStream; var isStream = corePrimitives.isStream;
var CipherTransformFactory = coreCrypto.CipherTransformFactory; var CipherTransformFactory = coreCrypto.CipherTransformFactory;
var Lexer = coreParser.Lexer; var Lexer = coreParser.Lexer;
@ -522,7 +523,7 @@ var Catalog = (function CatalogClosure() {
return capability.promise; 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 // 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 // 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. // adding the count of siblings to the left of the node.
@ -531,15 +532,21 @@ var Catalog = (function CatalogClosure() {
var total = 0; var total = 0;
var parentRef; var parentRef;
return xref.fetchAsync(kidRef).then(function (node) { 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) { if (!node) {
return null; return null;
} }
assert(isDict(node), 'node must be a Dict.');
parentRef = node.getRaw('Parent'); parentRef = node.getRaw('Parent');
return node.getAsync('Parent'); return node.getAsync('Parent');
}).then(function (parent) { }).then(function (parent) {
if (!parent) { if (!parent) {
return null; return null;
} }
assert(isDict(parent), 'parent must be a Dict.');
return parent.getAsync('Kids'); return parent.getAsync('Kids');
}).then(function (kids) { }).then(function (kids) {
if (!kids) { if (!kids) {
@ -549,7 +556,7 @@ var Catalog = (function CatalogClosure() {
var found = false; var found = false;
for (var i = 0; i < kids.length; i++) { for (var i = 0; i < kids.length; i++) {
var kid = kids[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) { if (kid.num === kidRef.num) {
found = true; found = true;
break; break;
@ -585,7 +592,7 @@ var Catalog = (function CatalogClosure() {
}); });
} }
return next(ref); return next(pageRef);
} }
}; };

5
src/core/primitives.js

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

7
src/display/api.js

@ -1683,7 +1683,12 @@ var WorkerTransport = (function WorkerTransportClosure() {
}, },
getPageIndex: function WorkerTransport_getPageIndexByRef(ref) { 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) { getAnnotations: function WorkerTransport_getAnnotations(pageIndex, intent) {

56
test/unit/annotation_layer_spec.js

@ -268,7 +268,33 @@ describe('Annotation layer', function() {
var actionDict = new Dict(); var actionDict = new Dict();
actionDict.set('Type', Name.get('Action')); actionDict.set('Type', Name.get('Action'));
actionDict.set('S', Name.get('GoToR')); actionDict.set('S', Name.get('GoToR'));
actionDict.set('F', '../../0021/002156/215675E.pdf'); 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'));
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).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'); actionDict.set('D', '15');
var annotationDict = new Dict(); var annotationDict = new Dict();
@ -276,6 +302,31 @@ describe('Annotation layer', function() {
annotationDict.set('Subtype', Name.get('Link')); annotationDict.set('Subtype', Name.get('Link'));
annotationDict.set('A', actionDict); 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 xrefMock = new XrefMock([annotationDict]);
var annotationRef = new Ref(489, 0); var annotationRef = new Ref(489, 0);
@ -283,7 +334,8 @@ describe('Annotation layer', function() {
var data = annotation.data; var data = annotation.data;
expect(data.annotationType).toEqual(AnnotationType.LINK); expect(data.annotationType).toEqual(AnnotationType.LINK);
expect(data.url).toBeUndefined(); expect(data.url).toEqual('http://www.example.com/test.pdf#' +
'[14,{"name":"XYZ"},null,298.043,null]');
expect(data.dest).toBeUndefined(); expect(data.dest).toBeUndefined();
expect(data.newWindow).toBeFalsy(); expect(data.newWindow).toBeFalsy();
}); });

10
test/unit/api_spec.js

@ -360,6 +360,16 @@ describe('api', function() {
done.fail(reason); 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) { it('gets destinations, from /Dests dictionary', function(done) {
var promise = doc.getDestinations(); var promise = doc.getDestinations();

16
test/unit/primitives_spec.js

@ -1,5 +1,5 @@
/* globals expect, it, describe, beforeEach, Name, Dict, Ref, RefSet, Cmd, /* globals expect, it, describe, beforeEach, Name, Dict, Ref, RefSet, Cmd,
jasmine, isDict */ jasmine, isDict, isRefsEqual */
'use strict'; 'use strict';
@ -158,4 +158,18 @@ describe('primitives', function() {
expect(isDict(dict, 'Page')).toEqual(true); 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);
});
});
}); });

6
web/app.js

@ -857,6 +857,7 @@ var PDFViewerApplication = {
pdfViewer.setDocument(pdfDocument); pdfViewer.setDocument(pdfDocument);
var firstPagePromise = pdfViewer.firstPagePromise; var firstPagePromise = pdfViewer.firstPagePromise;
var pagesPromise = pdfViewer.pagesPromise; var pagesPromise = pdfViewer.pagesPromise;
var onePageRendered = pdfViewer.onePageRendered;
this.pageRotation = 0; this.pageRotation = 0;
@ -962,9 +963,8 @@ var PDFViewerApplication = {
} }
}); });
// outline depends on pagesRefMap Promise.all([onePageRendered, this.animationStartedPromise]).then(
var promises = [pagesPromise, this.animationStartedPromise]; function() {
Promise.all(promises).then(function() {
pdfDocument.getOutline().then(function(outline) { pdfDocument.getOutline().then(function(outline) {
self.pdfOutlineViewer.render({ outline: outline }); self.pdfOutlineViewer.render({ outline: outline });
}); });

116
web/pdf_link_service.js

@ -29,6 +29,11 @@
var parseQueryString = uiUtils.parseQueryString; var parseQueryString = uiUtils.parseQueryString;
var PageNumberRegExp = /^\d+$/;
function isPageNumber(str) {
return PageNumberRegExp.test(str);
}
/** /**
* @typedef {Object} PDFLinkServiceOptions * @typedef {Object} PDFLinkServiceOptions
* @property {EventBus} eventBus - The application event bus. * @property {EventBus} eventBus - The application event bus.
@ -40,7 +45,7 @@ var parseQueryString = uiUtils.parseQueryString;
* @class * @class
* @implements {IPDFLinkService} * @implements {IPDFLinkService}
*/ */
var PDFLinkService = (function () { var PDFLinkService = (function PDFLinkServiceClosure() {
/** /**
* @constructs PDFLinkService * @constructs PDFLinkService
* @param {PDFLinkServiceOptions} options * @param {PDFLinkServiceOptions} options
@ -100,7 +105,7 @@ var PDFLinkService = (function () {
var self = this; var self = this;
var goToDestination = function(destRef) { 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 ? var pageNumber = destRef instanceof Object ?
self._pagesRefCache[destRef.num + ' ' + destRef.gen + ' R'] : self._pagesRefCache[destRef.num + ' ' + destRef.gen + ' R'] :
(destRef + 1); (destRef + 1);
@ -150,30 +155,15 @@ var PDFLinkService = (function () {
*/ */
getDestinationHash: function PDFLinkService_getDestinationHash(dest) { getDestinationHash: function PDFLinkService_getDestinationHash(dest) {
if (typeof dest === 'string') { 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) { if (dest instanceof Array) {
var destRef = dest[0]; // see navigateTo method for dest format var str = JSON.stringify(dest);
var pageNumber = destRef instanceof Object ? return this.getAnchorUrl('#' + escape(str));
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;
}
} }
return this.getAnchorUrl(''); return this.getAnchorUrl('');
}, },
@ -192,6 +182,7 @@ var PDFLinkService = (function () {
* @param {string} hash * @param {string} hash
*/ */
setHash: function PDFLinkService_setHash(hash) { setHash: function PDFLinkService_setHash(hash) {
var pageNumber, dest;
if (hash.indexOf('=') >= 0) { if (hash.indexOf('=') >= 0) {
var params = parseQueryString(hash); var params = parseQueryString(hash);
if ('search' in params) { if ('search' in params) {
@ -209,7 +200,6 @@ var PDFLinkService = (function () {
this.navigateTo(params.nameddest); this.navigateTo(params.nameddest);
return; return;
} }
var pageNumber, dest;
if ('page' in params) { if ('page' in params) {
pageNumber = (params.page | 0) || 1; pageNumber = (params.page | 0) || 1;
} }
@ -259,13 +249,23 @@ var PDFLinkService = (function () {
mode: params.pagemode mode: params.pagemode
}); });
} }
} else if (/^\d+$/.test(hash)) { // page number } else if (isPageNumber(hash)) { // Page number.
this.page = hash; this.page = hash | 0;
} else { // named destination } else { // Named (or explicit) destination.
if (this.pdfHistory) { dest = unescape(hash);
this.pdfHistory.updateNextHashParam(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; return PDFLinkService;
})(); })();

2
web/pdf_viewer.js

@ -587,6 +587,8 @@ var PDFViewer = (function pdfViewer() {
scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); scale = Math.min(Math.abs(widthScale), Math.abs(heightScale));
break; break;
default: default:
console.error('PDFViewer_scrollPageIntoView: \'' + dest[1].name +
'\' is not a valid destination type.');
return; return;
} }

Loading…
Cancel
Save