From 42f2d36d1f0b6c7c3be921bfde900f1748183643 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 25 Aug 2017 13:40:50 +0200 Subject: [PATCH] Account for broken outlines/annotations, where the destination dictionary contains an invalid `/Dest` entry According to the specification, see http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/PDF32000_2008.pdf#page=377, a `Dest` entry in an outline item should *not* contain a dictionary. Unsurprisingly there's PDF generators that completely ignore this, treating is an `A` entry instead. The patch also adds a little bit more validation code in `Catalog.parseDestDictionary`. --- src/core/obj.js | 25 ++++++++++++++++++------- test/unit/annotation_spec.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/core/obj.js b/src/core/obj.js index dd302d10b..95924a6fe 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -640,20 +640,32 @@ var Catalog = (function CatalogClosure() { var destDict = params.destDict; if (!isDict(destDict)) { - warn('Catalog_parseDestDictionary: "destDict" must be a dictionary.'); + warn('parseDestDictionary: "destDict" must be a dictionary.'); return; } var resultObj = params.resultObj; if (typeof resultObj !== 'object') { - warn('Catalog_parseDestDictionary: "resultObj" must be an object.'); + warn('parseDestDictionary: "resultObj" must be an object.'); return; } var docBaseUrl = params.docBaseUrl || null; var action = destDict.get('A'), url, dest; + if (!isDict(action) && destDict.has('Dest')) { + // A /Dest entry should *only* contain a Name or an Array, but some bad + // PDF generators ignore that and treat it as an /A entry. + action = destDict.get('Dest'); + } + if (isDict(action)) { - var linkType = action.get('S').name; - switch (linkType) { + let actionType = action.get('S'); + if (!isName(actionType)) { + warn('parseDestDictionary: Invalid type in Action dictionary.'); + return; + } + let actionName = actionType.name; + + switch (actionName) { case 'URI': url = action.get('URI'); if (isName(url)) { @@ -748,11 +760,10 @@ var Catalog = (function CatalogClosure() { } /* falls through */ default: - warn('Catalog_parseDestDictionary: Unrecognized link type "' + - linkType + '".'); + warn(`parseDestDictionary: Unsupported Action type "${actionName}".`); break; } - } else if (destDict.has('Dest')) { // Simple destination link. + } else if (destDict.has('Dest')) { // Simple destination. dest = destDict.get('Dest'); } diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index b51f2e690..15975f9f0 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -689,6 +689,36 @@ describe('annotation', function() { expect(data.dest).toEqual([{ num: 17, gen: 0, }, { name: 'XYZ', }, 0, 841.89, null]); }); + + it('should correctly parse a Dest, which violates the specification ' + + 'by containing a dictionary', function() { + let destDict = new Dict(); + destDict.set('Type', Name.get('Action')); + destDict.set('S', Name.get('GoTo')); + destDict.set('D', 'page.157'); + + let annotationDict = new Dict(); + annotationDict.set('Type', Name.get('Annot')); + annotationDict.set('Subtype', Name.get('Link')); + // The /Dest must be a Name or an Array, refer to ISO 32000-1:2008 + // section 12.3.3, but there are PDF files where it's a dictionary. + annotationDict.set('Dest', destDict); + + let annotationRef = new Ref(798, 0); + let xref = new XRefMock([ + { ref: annotationRef, data: annotationDict, } + ]); + + let annotation = annotationFactory.create(xref, annotationRef, + pdfManagerMock, idFactoryMock); + let data = annotation.data; + expect(data.annotationType).toEqual(AnnotationType.LINK); + + expect(data.url).toBeUndefined(); + expect(data.unsafeUrl).toBeUndefined(); + expect(data.dest).toEqual('page.157'); + }); + }); describe('WidgetAnnotation', function() {