From e64bc1fd1387d43c3447c7018eaa16f1794f0d2a Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 30 Sep 2016 16:08:03 +0200 Subject: [PATCH] Move parsing of destination dictionaries to a helper function This not only reduces code duplication, but it also allow us to easily support the same kind of URLs we currently do for Link annotations in the Outline as well. --- src/core/annotation.js | 102 ++--------------------------- src/core/obj.js | 133 ++++++++++++++++++++++++++++++++------ test/unit/api_spec.js | 1 + web/pdf_outline_viewer.js | 7 +- 4 files changed, 126 insertions(+), 117 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 4539db43d..37c331596 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -38,14 +38,11 @@ var AnnotationFlag = sharedUtil.AnnotationFlag; var AnnotationType = sharedUtil.AnnotationType; var OPS = sharedUtil.OPS; var Util = sharedUtil.Util; -var isBool = sharedUtil.isBool; var isString = sharedUtil.isString; var isArray = sharedUtil.isArray; var isInt = sharedUtil.isInt; -var isValidUrl = sharedUtil.isValidUrl; var stringToBytes = sharedUtil.stringToBytes; var stringToPDFString = sharedUtil.stringToPDFString; -var stringToUTF8String = sharedUtil.stringToUTF8String; var warn = sharedUtil.warn; var Dict = corePrimitives.Dict; var isDict = corePrimitives.isDict; @@ -53,6 +50,7 @@ var isName = corePrimitives.isName; var isRef = corePrimitives.isRef; var Stream = coreStream.Stream; var ColorSpace = coreColorSpace.ColorSpace; +var Catalog = coreObj.Catalog; var ObjectLoader = coreObj.ObjectLoader; var FileSpec = coreObj.FileSpec; var OperatorList = coreEvaluator.OperatorList; @@ -842,103 +840,13 @@ var LinkAnnotation = (function LinkAnnotationClosure() { function LinkAnnotation(params) { Annotation.call(this, params); - var dict = params.dict; var data = this.data; data.annotationType = AnnotationType.LINK; - var action = dict.get('A'), url, dest; - if (action && isDict(action)) { - var linkType = action.get('S').name; - switch (linkType) { - case 'URI': - url = action.get('URI'); - if (isName(url)) { - // Some bad PDFs do not put parentheses around relative URLs. - url = '/' + url.name; - } else if (url) { - url = addDefaultProtocolToUrl(url); - } - // TODO: pdf spec mentions urls can be relative to a Base - // entry in the dictionary. - break; - - case 'GoTo': - dest = action.get('D'); - break; - - case 'GoToR': - var urlDict = action.get('F'); - if (isDict(urlDict)) { - // We assume that we found a FileSpec dictionary - // and fetch the URL without checking any further. - url = urlDict.get('F') || null; - } else if (isString(urlDict)) { - url = urlDict; - } - - // NOTE: the destination is relative to the *remote* document. - var remoteDest = action.get('D'); - if (remoteDest) { - if (isName(remoteDest)) { - remoteDest = remoteDest.name; - } - if (isString(url)) { - var baseUrl = url.split('#')[0]; - 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`. - var newWindow = action.get('NewWindow'); - if (isBool(newWindow)) { - data.newWindow = newWindow; - } - break; - - case 'Named': - data.action = action.get('N').name; - break; - - default: - warn('unrecognized link type: ' + linkType); - } - } else if (dict.has('Dest')) { // Simple destination link. - dest = dict.get('Dest'); - } - - if (url) { - if (isValidUrl(url, /* allowRelative = */ false)) { - data.url = tryConvertUrlEncoding(url); - } - } - if (dest) { - data.dest = isName(dest) ? dest.name : dest; - } - } - - // Lets URLs beginning with 'www.' default to using the 'http://' protocol. - function addDefaultProtocolToUrl(url) { - if (isString(url) && url.indexOf('www.') === 0) { - return ('http://' + url); - } - return url; - } - - function tryConvertUrlEncoding(url) { - // According to ISO 32000-1:2008, section 12.6.4.7, URIs should be encoded - // in 7-bit ASCII. Some bad PDFs use UTF-8 encoding, see Bugzilla 1122280. - try { - return stringToUTF8String(url); - } catch (e) { - return url; - } + Catalog.parseDestDictionary({ + destDict: params.dict, + resultObj: data, + }); } Util.inherit(LinkAnnotation, Annotation, {}); diff --git a/src/core/obj.js b/src/core/obj.js index a839d95e3..254f1d444 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -41,6 +41,7 @@ var createPromiseCapability = sharedUtil.createPromiseCapability; var error = sharedUtil.error; var info = sharedUtil.info; var isArray = sharedUtil.isArray; +var isBool = sharedUtil.isBool; var isInt = sharedUtil.isInt; var isString = sharedUtil.isString; var shadow = sharedUtil.shadow; @@ -152,23 +153,11 @@ var Catalog = (function CatalogClosure() { } assert(outlineDict.has('Title'), 'Invalid outline item'); - var actionDict = outlineDict.get('A'), dest = null, url = null; - if (actionDict) { - var destEntry = actionDict.get('D'); - if (destEntry) { - dest = destEntry; - } else { - var uriEntry = actionDict.get('URI'); - if (isString(uriEntry) && isValidUrl(uriEntry, false)) { - url = uriEntry; - } - } - } else if (outlineDict.has('Dest')) { - dest = outlineDict.getRaw('Dest'); - if (isName(dest)) { - dest = dest.name; - } - } + var data = { url: null, dest: null, }; + Catalog.parseDestDictionary({ + destDict: outlineDict, + resultObj: data, + }); var title = outlineDict.get('Title'); var flags = outlineDict.get('F') || 0; @@ -179,8 +168,9 @@ var Catalog = (function CatalogClosure() { rgbColor = ColorSpace.singletons.rgb.getRgb(color, 0); } var outlineItem = { - dest: dest, - url: url, + dest: data.dest, + url: data.url, + newWindow: data.newWindow, title: stringToPDFString(title), color: rgbColor, count: outlineDict.get('Count'), @@ -595,6 +585,111 @@ var Catalog = (function CatalogClosure() { } }; + /** + * Helper function used to parse the contents of destination dictionaries. + * @param {Dict} destDict - The dictionary containing the destination. + * @param {Object} resultObj - The object where the parsed destination + * properties will be placed. + */ + Catalog.parseDestDictionary = function Catalog_parseDestDictionary(params) { + // Lets URLs beginning with 'www.' default to using the 'http://' protocol. + function addDefaultProtocolToUrl(url) { + if (isString(url) && url.indexOf('www.') === 0) { + return ('http://' + url); + } + return url; + } + // According to ISO 32000-1:2008, section 12.6.4.7, URIs should be encoded + // in 7-bit ASCII. Some bad PDFs use UTF-8 encoding, see Bugzilla 1122280. + function tryConvertUrlEncoding(url) { + try { + return stringToUTF8String(url); + } catch (e) { + return url; + } + } + + var destDict = params.destDict; + var resultObj = params.resultObj; + + var action = destDict.get('A'), url, dest; + if (action && isDict(action)) { + var linkType = action.get('S').name; + switch (linkType) { + case 'URI': + url = action.get('URI'); + if (isName(url)) { + // Some bad PDFs do not put parentheses around relative URLs. + url = '/' + url.name; + } else if (url) { + url = addDefaultProtocolToUrl(url); + } + // TODO: pdf spec mentions urls can be relative to a Base + // entry in the dictionary. + break; + + case 'GoTo': + dest = action.get('D'); + break; + + case 'GoToR': + var urlDict = action.get('F'); + if (isDict(urlDict)) { + // We assume that we found a FileSpec dictionary + // and fetch the URL without checking any further. + url = urlDict.get('F') || null; + } else if (isString(urlDict)) { + url = urlDict; + } + + // NOTE: the destination is relative to the *remote* document. + var remoteDest = action.get('D'); + if (remoteDest) { + if (isName(remoteDest)) { + remoteDest = remoteDest.name; + } + if (isString(url)) { + var baseUrl = url.split('#')[0]; + 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`. + var newWindow = action.get('NewWindow'); + if (isBool(newWindow)) { + resultObj.newWindow = newWindow; + } + break; + + case 'Named': + resultObj.action = action.get('N').name; + break; + + default: + warn('Catalog_parseDestDictionary: Unrecognized link type "' + + linkType + '".'); + } + } else if (destDict.has('Dest')) { // Simple destination link. + dest = destDict.get('Dest'); + } + + if (url) { + if (isValidUrl(url, /* allowRelative = */ false)) { + resultObj.url = tryConvertUrlEncoding(url); + } + } + if (dest) { + resultObj.dest = isName(dest) ? dest.name : dest; + } + }; + return Catalog; })(); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 1e0e03a6d..f599f5ab2 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -644,6 +644,7 @@ describe('api', function() { expect(typeof outlineItemTwo.title).toEqual('string'); expect(outlineItemTwo.dest).toEqual(null); expect(outlineItemTwo.url).toEqual('http://google.com'); + expect(outlineItemTwo.newWindow).toBeUndefined(); var outlineItemOne = outline[1]; expect(outlineItemOne.bold).toEqual(false); diff --git a/web/pdf_outline_viewer.js b/web/pdf_outline_viewer.js index 8d4eedf0c..028e7a5ea 100644 --- a/web/pdf_outline_viewer.js +++ b/web/pdf_outline_viewer.js @@ -26,6 +26,8 @@ } }(this, function (exports, pdfjsLib) { +var PDFJS = pdfjsLib.PDFJS; + var DEFAULT_TITLE = '\u2013'; /** @@ -82,7 +84,10 @@ var PDFOutlineViewer = (function PDFOutlineViewerClosure() { */ _bindLink: function PDFOutlineViewer_bindLink(element, item) { if (item.url) { - pdfjsLib.addLinkAttributes(element, { url: item.url }); + pdfjsLib.addLinkAttributes(element, { + url: item.url, + target: (item.newWindow ? PDFJS.LinkTarget.BLANK : undefined), + }); return; } var linkService = this.linkService;