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;