Skip to content

Commit 8c986f1

Browse files
committed
Construct PdfOutline iteratively
Prevents stackoverflow from occuring for documents with a large amount of bookmarks. DEVSIX-1707
1 parent 4e4cfb4 commit 8c986f1

File tree

3 files changed

+96
-19
lines changed

3 files changed

+96
-19
lines changed

kernel/src/main/java/com/itextpdf/kernel/pdf/PdfCatalog.java

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,9 @@ public void setLang(PdfString lang) {
248248
put(PdfName.Lang, lang);
249249
}
250250

251-
public PdfString getLang(){ return getPdfObject().getAsString(PdfName.Lang);}
251+
public PdfString getLang() {
252+
return getPdfObject().getAsString(PdfName.Lang);
253+
}
252254

253255
public void addDeveloperExtension(PdfDeveloperExtension extension) {
254256
PdfDictionary extensions = getPdfObject().getAsDictionary(PdfName.Extensions);
@@ -381,8 +383,7 @@ PdfOutline getOutlines(boolean updateOutlines) {
381383
}
382384
outlines = new PdfOutline(getDocument());
383385
} else {
384-
outlines = new PdfOutline(OutlineRoot, outlineRoot, getDocument());
385-
getNextItem(outlineRoot.getAsDictionary(PdfName.First), outlines, destsTree.getNames());
386+
constructOutlines(outlineRoot, destsTree.getNames());
386387
}
387388

388389
return outlines;
@@ -504,26 +505,62 @@ private void addOutlineToPage(PdfOutline outline, Map<String, PdfObject> names)
504505
}
505506
}
506507

507-
private void getNextItem(PdfDictionary item, PdfOutline parent, Map<String, PdfObject> names) {
508-
if (null == item) {
509-
return;
508+
/**
509+
* Get the next outline of the current node in the outline tree by looking for a child or sibling node.
510+
* If there is no child or sibling of the current node {@link PdfCatalog#getParentNextOutline(PdfDictionary)} is called to get a hierarchical parent's next node. {@code null} is returned if one does not exist.
511+
*
512+
* @return the {@link PdfDictionary} object of the next outline if one exists, {@code null} otherwise.
513+
*/
514+
private PdfDictionary getNextOutline(PdfDictionary first, PdfDictionary next, PdfDictionary parent) {
515+
if (first != null) {
516+
return first;
517+
} else if (next != null) {
518+
return next;
519+
} else {
520+
return getParentNextOutline(parent);
521+
}
522+
523+
}
524+
525+
/**
526+
* Gets the parent's next outline of the current node.
527+
* If the parent does not have a next we look at the grand parent, great-grand parent, etc until we find a next node or reach the root at which point {@code null} is returned to signify there is no next node present.
528+
*
529+
* @return the {@link PdfDictionary} object of the next outline if one exists, {@code null} otherwise.
530+
*/
531+
private PdfDictionary getParentNextOutline(PdfDictionary parent) {
532+
if (parent == null) {
533+
return null;
510534
}
511-
PdfOutline outline = new PdfOutline(item.getAsString(PdfName.Title).toUnicodeString(), item, parent);
535+
PdfDictionary current = null;
536+
while (current == null) {
537+
current = parent.getAsDictionary(PdfName.Next);
538+
if (current == null) {
539+
parent = parent.getAsDictionary(PdfName.Parent);
540+
if (parent == null) {
541+
return null;
542+
}
543+
}
544+
}
545+
return current;
546+
}
547+
548+
private void addOutlineToPage(PdfOutline outline, PdfDictionary item, Map<String, PdfObject> names) {
512549
PdfObject dest = item.get(PdfName.Dest);
513550
if (dest != null) {
514551
PdfDestination destination = PdfDestination.makeDestination(dest);
515552
outline.setDestination(destination);
516553
addOutlineToPage(outline, names);
517-
}else {
554+
} else {
518555
//Take into account outlines that specify their destination through an action
519556
PdfDictionary action = item.getAsDictionary(PdfName.A);
520-
if(action != null){
557+
if (action != null) {
521558
PdfName actionType = action.getAsName(PdfName.S);
522-
//Check if it a go to action
523-
if(PdfName.GoTo.equals(actionType)) {
559+
//Check if it is a go to action
560+
if (PdfName.GoTo.equals(actionType)) {
524561
//Retrieve destination if it is.
525562
PdfObject destObject = action.get(PdfName.D);
526-
if(destObject != null){
563+
if (destObject != null) {
527564
//Page is always the first object
528565
PdfDestination destination = PdfDestination.makeDestination(destObject);
529566
outline.setDestination(destination);
@@ -532,15 +569,43 @@ private void getNextItem(PdfDictionary item, PdfOutline parent, Map<String, PdfO
532569
}
533570
}
534571
}
535-
parent.getAllChildren().add(outline);
572+
}
536573

537-
PdfDictionary processItem = item.getAsDictionary(PdfName.First);
538-
if (processItem != null) {
539-
getNextItem(processItem, outline, names);
574+
/**
575+
* Constructs {@link PdfCatalog#outlines} iteratively
576+
*/
577+
private void constructOutlines(PdfDictionary outlineRoot, Map<String, PdfObject> names) {
578+
if (outlineRoot == null) {
579+
return;
540580
}
541-
processItem = item.getAsDictionary(PdfName.Next);
542-
if (processItem != null) {
543-
getNextItem(processItem, parent, names);
581+
PdfDictionary first = outlineRoot.getAsDictionary(PdfName.First);
582+
PdfDictionary current = first;
583+
PdfDictionary next;
584+
PdfDictionary parent;
585+
HashMap<PdfDictionary, PdfOutline> parentOutlineMap = new HashMap<>();
586+
587+
outlines = new PdfOutline(OutlineRoot, outlineRoot, getDocument());
588+
PdfOutline parentOutline = outlines;
589+
parentOutlineMap.put(outlineRoot, parentOutline);
590+
591+
while (current != null) {
592+
first = current.getAsDictionary(PdfName.First);
593+
next = current.getAsDictionary(PdfName.Next);
594+
parent = current.getAsDictionary(PdfName.Parent);
595+
596+
parentOutline = parentOutlineMap.get(parent);
597+
PdfOutline currentOutline = new PdfOutline(current.getAsString(PdfName.Title).toUnicodeString(), current, parentOutline);
598+
addOutlineToPage(currentOutline, current, names);
599+
parentOutline.getAllChildren().add(currentOutline);
600+
601+
if (first != null) {
602+
parentOutlineMap.put(current, currentOutline);
603+
} else if (current == parent.getAsDictionary(PdfName.Last)) {
604+
parentOutlineMap.remove(parent);
605+
}
606+
current = getNextOutline(first, next, parent);
607+
544608
}
545609
}
610+
546611
}

kernel/src/test/java/com/itextpdf/kernel/pdf/PdfOutlineTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,4 +392,16 @@ public void addOutlinesWithNamedDestinations02() throws IOException, Interrupted
392392

393393
Assert.assertNull(new CompareTool().compareByContent(filename, sourceFolder + "cmp_outlinesWithNamedDestinations02.pdf", destinationFolder, "diff_"));
394394
}
395+
396+
@Test
397+
public void outlineStackOverflowTest01() throws IOException {
398+
PdfReader reader = new PdfReader(sourceFolder + "outlineStackOverflowTest01.pdf");
399+
PdfDocument pdfDoc = new PdfDocument(reader);
400+
401+
try {
402+
pdfDoc.getOutlines(true);
403+
} catch (StackOverflowError e) {
404+
Assert.fail("StackOverflow thrown when reading document with a large number of outlines.");
405+
}
406+
}
395407
}

0 commit comments

Comments
 (0)