Search code examples
javapdfitextpdf-generationbookmarks

Adding hyperlink to inner PDF files


I have to create a PDF file by adding two PDF files inside a generated PDF file as a tree structure using iText in Java.

I have to create bookmarks with PDF file names and add a hyperlink to the bookmark. When the bookmark is clicked, the respective PDF should be opened in that PDF file itself, not as a separate PDF.

PDFTREE

 pdf1 
 pdf2 

Solution

  • Such bookmarks are referred to as outline elements in the PDF specification (PDF 32000-1:2008, p.367):

    The outline consists of a tree-structured hierarchy of outline items (sometimes called bookmarks), which serve as a visual table of contents to display the document’s structure to the user.

    If you merge the documents with PdfMerger, the outlines are copied to the resulting PDF by default. However, you want a main-node per document and not a flat list of bookmarks. Since cloning and copying outlines in no trivial task, it is best to let iText handle this. Unfortunately, we have little direct control how outlines are being merged.

    We can build a SpecialMerger as a wrapper around PdfMerger to extract the cloned outlines (first step) and get them into a hierarchical structure afterwards (second step). The outline of each merged PDF is temporarily stored in the outlineList together with the desired name of the main node and its reference (page number in the merged PDF). After all the PDFs are merged, we can attach the temporarily stored outlines back to the root-node.

    public static class SpecialMerger {
    
        private final PdfDocument outputPdf;
        private final PdfMerger merger;
        private final PdfOutline rootOutline;
        private final List<DocumentOutline> outlineList = new ArrayList<>();
        private int nextPageNr = 1;
    
        public SpecialMerger(final PdfDocument outputPdf) {
            if (outputPdf.getNumberOfPages() != 0) {
                throw new IllegalArgumentException("PDF must be empty");
            }
            this.outputPdf = outputPdf;
            this.merger = new PdfMerger(outputPdf, true, true);
            this.rootOutline = outputPdf.getOutlines(false);
        }
    
        public void merge(PdfDocument from, int fromPage, int toPage, String filename) {
            merger.merge(from, fromPage, toPage); // merge with normal PdfMerger
    
            // extract and clone outline of merged document
            final List<PdfOutline> children = new ArrayList<>(rootOutline.getAllChildren());
            rootOutline.getAllChildren().clear(); // clear root outline
            outlineList.add(new DocumentOutline(filename, nextPageNr, children));
            nextPageNr = outputPdf.getNumberOfPages() + 1; // update next page number
        }
    
        public void writeOutline() {
            outlineList.forEach(o -> {
                final PdfOutline outline = rootOutline.addOutline(o.getName()); // bookmark with PDF name
                outline.addDestination(PdfExplicitDestination.createFit(outputPdf.getPage(o.getPageNr())));
                outline.setStyle(PdfOutline.FLAG_BOLD);
                o.getChildern().forEach(outline::addOutline); // add all extracted child bookmarks
            });
        }
    
        private static class DocumentOutline {
    
            private final String name;
            private final int pageNr;
            private final List<PdfOutline> childern;
    
            public DocumentOutline(final String pdfName, final int pageNr, final List<PdfOutline> childern) {
                this.name = pdfName;
                this.pageNr = pageNr;
                this.childern = childern;
            }
    
            public String getName() {
                return name;
            }
    
            public int getPageNr() {
                return pageNr;
            }
    
            public List<PdfOutline> getChildern() {
                return childern;
            }
        }
    }
    

    Now, we can use this custom merger to merge the PDFs and then add the outline with writeOutline:

    public static void main(String[] args) throws IOException {
        String filename1 = "pdf1.pdf";
        String filename2 = "pdf2.pdf";
        try (
                PdfDocument generatedPdf = new PdfDocument(new PdfWriter("output.pdf"));
                PdfDocument pdfDocument1 = new PdfDocument(new PdfReader(filename1));
                PdfDocument pdfDocument2 = new PdfDocument(new PdfReader(filename2))
        ) {
            final SpecialMerger merger = new SpecialMerger(generatedPdf);
            merger.merge(pdfDocument1, 1, pdfDocument1.getNumberOfPages(), filename1);
            merger.merge(pdfDocument2, 1, pdfDocument2.getNumberOfPages(), filename2);
            merger.writeOutline();
        }
    }
    

    The result looks like this (Preview and Adobe Acrobat Reader on macOS): result-merge


    Another option is to make a portfolio by embedding the PDFs. However, this is not supported by all PDF viewers and most users are not accustomed to these portfolios.

    public static void main(String[] args) throws IOException {
        String filename1 = "pdf1.pdf";
        String filename2 = "pdf2.pdf";
    
        try (PdfDocument generatedPdf = new PdfDocument(new PdfWriter("portfolio.pdf"))) {
            Document doc = new Document(generatedPdf);
            doc.add(new Paragraph("This PDF contains embedded documents."));
            doc.add(new Paragraph("Use a compatible PDF viewer if you cannot see them."));
    
            PdfCollection collection = new PdfCollection();
            collection.setView(PdfCollection.TILE);
            generatedPdf.getCatalog().setCollection(collection);
    
            addAttachment(generatedPdf, filename1, filename1);
            addAttachment(generatedPdf, filename2, filename2);
        }
    }
    
    private static void addAttachment(PdfDocument doc, String attachmentPath, String name) throws IOException {
        PdfFileSpec fileSpec = PdfFileSpec.createEmbeddedFileSpec(doc, attachmentPath, name, name, null, null);
        doc.addFileAttachment(name, fileSpec);
    }
    

    The result in Adobe Acrobat Reader on macOS:

    result-portfolio