Easy table of content feature with go to links and page numbers
Is your feature request related to a problem? Please describe. I need to generate a PDF containing a table of content with go to links and page numbers at the beginning of the document.
Describe the solution you'd like It would be amazing to have a way to insert a page number of a bookmark that was not currently known. Maybe something like this could be done to fill the table of content with page number:
final Paragraph par = new Paragraph();
final Chunk c = new Chunk(chapTitle));
c.setLocalGoto(bookmark);
par.add(c);
par.add(new Chunk(new DottedLineSeparator()));
par.add(new Chunk(new BookmarkPageNumber(bookmark));
summaryChapter.add(par);
I imagine a class BookmarkPageNumber taking a bookmark name as parameter and will write the page number when the document is closed.
Describe alternatives you've considered The only way I found is to insert the table of content at the end of the book.
Your real name no
Additional context
This kind of feature does seem like it would be handy, as it seems like it is common request. Regardless, this can already be made using the tools OpenPDF provides. For those that are attempting to create a table of contents, this kind of thing can currently be done using anchors and ~~templates~~ (Templates would work but require keeping track of the current height of each element and becomes a mess quickly. A better solution is to create the whole pdf, keeping track of each new chapter's pages, and then ultimately merge a new pdf containing just the index with the pdf containing your content). I will try to create a demonstration of this in action when I get some time, but for now, here's the two vital parts to this puzzle
Go to links
The 'Go to links' can be performed using references like in the code snippet below
Anchor click = new Anchor("Click me to go to the Target");
click.setReference("#target");
Paragraph p1 = new Paragraph();
p1.add(click);
doc.add(p1);
doc.newPage();
Anchor target = new Anchor("Target");
target.setName("target");
doc.add(target);
This code snippet is from here
Page number
The page number can be determined using templates along with some magic. The templates will be initialized to blank, but filled in after once you know the location of the section (Just be sure to record the page number when you start a new section). See the PageNumbersWatermark example for this kind of setup
I'm terribly sorry for the incredibly delayed response. I have been fairly busy, but I have finally gotten around to implementing this in a demo of my own. If anyone else is able to implement this in a simpler fashion, I would love to hear about it.
Why chunks over anchors
I decided upon using chunks rather than anchors (Partially because I believe chunks to have better syntax for linking somewhere within the pdf), although both should work in theory.
Linking to chapters
I utilize the merging logic mentioned above, however, I had to create a separate ByteArrayOutputStream (PDF) for each of the chapters that I wanted, and then separately merge them together to allow for the chapter system to work properly. Otherwise, OpenPDF had no way of knowing where the chapters began/ended in the newly merged PDF.
Page Number
Because I utilize a BAOS for each of the sections, I can get the page number for each of the sections, and then write this in. The hardest part is determining how large the index will be (assuming you want to include the index in page numbers). This can be done, however, by writing a 'dummy' index to a 'dummy' document, which will contain all of the same information and formatting of the true index, just without the page number to allow for us to get an idea for how large the index will be.
Result
Code
private static PdfPTable createIndex(List<ByteArrayOutputStream> chapterStreams, List<Integer> chapterPageSizes, int indexSize) {
PdfPTable table = new PdfPTable(2);
table.setWidthPercentage(100f);
table.getDefaultCell().setBorder(PdfPCell.NO_BORDER);
//index
Paragraph indexTitleParagraph = new Paragraph("Index\n\n", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18));
PdfPCell indexTitleCell = new PdfPCell(indexTitleParagraph);
indexTitleCell.setColspan(2);
indexTitleCell.setBorder(PdfPCell.NO_BORDER);
table.addCell(indexTitleCell);
int documentPageOffset = 0;
for(int i = 1; i <= chapterStreams.size(); i++) {
String destName = "chapter" + i;
Chunk link = new Chunk("Go to Chapter " + i);
link.setRemoteGoto("TEMP", destName);
PdfPCell nameCell = new PdfPCell(new Phrase(link));
nameCell.setBorder(PdfPCell.NO_BORDER);
String pageNumber = String.valueOf(documentPageOffset + indexSize + 1);
PdfPCell pageCell = new PdfPCell(new Phrase(pageNumber, indexFont));
pageCell.setHorizontalAlignment(PdfPCell.ALIGN_RIGHT);
pageCell.setBorder(PdfPCell.NO_BORDER);
table.addCell(nameCell);
table.addCell(pageCell);
documentPageOffset += chapterPageSizes.get(i - 1);
}
return table;
}
public static ByteArrayOutputStream createPdf() {
try {
Map<String, Integer> chapterPages = new LinkedHashMap<>();
List<ByteArrayOutputStream> chapterStreams = new ArrayList<>();
List<Integer> pageSizes = new ArrayList<>();
//Create the chapters
for(int i = 1; i <= 5; i++) {
ByteArrayOutputStream chapterBaos = new ByteArrayOutputStream();
Document chapterDoc = new Document();
PdfWriter chapterWriter = PdfWriter.getInstance(chapterDoc, chapterBaos);
chapterDoc.open();
String destName = "chapter" + i;
Chunk title = new Chunk("Chapter " + i + "\n", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16));
title.setLocalDestination(destName);
chapterDoc.add(new Paragraph(title));
for(int j = 0; j < 29; j++) {
chapterDoc.add(new Paragraph("This is line " + j + " of Chapter " + i));
}
pageSizes.add(chapterWriter.getCurrentPageNumber());
chapterDoc.close();
chapterStreams.add(chapterBaos);
}
Document finalDoc = new Document();
ByteArrayOutputStream finalBaos = new ByteArrayOutputStream();
PdfSmartCopy copy = new PdfSmartCopy(finalDoc, finalBaos);
finalDoc.open();
Document indexDoc = new Document();
ByteArrayOutputStream indexBaos = new ByteArrayOutputStream();
PdfWriter indexWriter = PdfWriter.getInstance(indexDoc, indexBaos);
//Setup the index
indexDoc.open();
//Setup a dummy index so that we know how many pages the index itself will take up
PdfPTable indexTableDummy = createIndex(chapterStreams, pageSizes, 0);
Document dummyDoc = new Document();
ByteArrayOutputStream dummyBaos = new ByteArrayOutputStream();
PdfWriter dummyWriter = PdfWriter.getInstance(dummyDoc, dummyBaos);
dummyDoc.open();
dummyDoc.add(indexTableDummy);
int indexSize = dummyWriter.getCurrentPageNumber();
System.out.println(indexSize);
dummyDoc.close();
PdfPTable indexTable = createIndex(chapterStreams, pageSizes, indexSize);
indexDoc.add(indexTable);
indexDoc.close();
//Merge the index into the final pdf
PdfReader indexReader = new PdfReader(indexBaos.toByteArray());
for(int i = 1; i <= indexReader.getNumberOfPages(); i++) {
copy.addPage(copy.getImportedPage(indexReader, i));
}
int currentPage = copy.getCurrentPageNumber();
for(int i = 0; i < chapterStreams.size(); i++) {
PdfReader chapterReader = new PdfReader(chapterStreams.get(i).toByteArray());
for(int j = 1; j <= chapterReader.getNumberOfPages(); j++) {
PdfImportedPage page = copy.getImportedPage(chapterReader, j);
copy.addPage(page);
if(j == 1) {
String destName = "chapter" + (i + 1);
PdfDestination dest = new PdfDestination(PdfDestination.FIT);
copy.addNamedDestination(destName, currentPage, dest);
chapterPages.put(destName, currentPage);
}
currentPage++;
}
}
finalDoc.close();
return new ByteArrayInputStream(finalBaos.toByteArray());
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
Please prepare this for inclusion in OpenPDF.
I would be happy to. Are you looking for this to simply be reformatted, added to a class among other change, or a more generalized approach to generating the index? For example, having to keep track of each of the sections and then passing it into a class seems like a lot to ask of the users for an index.
Pull requests for this is welcome!
Are you looking for this to simply be reformatted, added to a class among other change, or a more generalized approach to generating the index?
I suggest it is up to you to decide how to design this feature, as a contributor to OpenPDF. Then we will evaluate the pull request.
See the code below, this could be one way to generate table of contents for a PDF file.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Document doc = new Document(PageSize.A4, 50, 50, 60, 60);
PdfWriter writer = PdfWriter.getInstance(doc, baos);
DefaultTocCollector collector = new DefaultTocCollector();
writer.setPageEvent(collector);
doc.open();
collector.markHeading(writer, "Chapter 1 – Getting Started", "ch1", 1);
doc.add(new Paragraph("Chapter 1 – Getting Started", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18)));
doc.add(new Paragraph("Intro text for chapter 1."));
collector.markHeading(writer, "Section 1.1 – Installation", "ch1_sec1", 2);
doc.add(new Paragraph("Section 1.1 – Installation", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 14)));
doc.add(new Paragraph("Steps to install..."));
doc.newPage();
collector.markHeading(writer, "Chapter 2 – Deep Dive", "ch2", 1);
doc.add(new Paragraph("Chapter 2 – Deep Dive", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18)));
doc.add(new Paragraph("Intro text for chapter 2."));
collector.markHeading(writer, "Section 2.1 – Styling", "ch2_sec1", 2);
doc.add(new Paragraph("Section 2.1 – Styling", FontFactory.getFont(FontFactory.HELVETICA_BOLD, 14)));
doc.add(new Paragraph("Details on styling..."));
doc.newPage();
TocOptions options = TocOptions.builder()
.titleText("Contents")
.dotLeaders(true)
.placement(TocOptions.PagePlacement.PREPEND)
.build();
TocHelper.generateAndPrepend(doc, writer, collector);
doc.close();
try (FileOutputStream fos = new FileOutputStream(Path.of("toc-demo.pdf").toFile())) {
fos.write(baos.toByteArray());
}