为PDF文档添加目录也是经常会用到的功能,itext做目录的思路一般有2种。
第一种:先将正文文档生成出来,同时保存正文文件的每一个模块以及对应的页码,然后生成一个新的文档,同时读取正文文件,复制正文文件的每一页,并通过正文模块与对应的页码数生成目录
第二种:先将正文文档生成出来,同时保存正文文件的每一个模块以及对应的页码,然后通过移动页面的方式生成目录,此方式在实际过程中当目录超过2页时会有问题。
我的案例以第一种方式生成目录,简单起见,正文文件先生成好,总共有5个模块,6个页面,其中一个模块占了2页,模块与页面的对应关系如下代码。
public List<Map<String, Object>> getCatalogList() {
final List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
final Map<String, Object> catalogMap = new HashMap<String, Object>();
catalogMap.put("title", "关于公司");
catalogMap.put("number", 1);
list.add(catalogMap);
final Map<String, Object> catalogMap2 = new HashMap<String, Object>();
catalogMap2.put("title", "阅读指南");
catalogMap2.put("number", 2);
list.add(catalogMap2);
final Map<String, Object> catalogMap4 = new HashMap<String, Object>();
catalogMap4.put("title", "测评目的");
catalogMap4.put("number", 4);
list.add(catalogMap4);
final Map<String, Object> catalogMap5 = new HashMap<String, Object>();
catalogMap5.put("title", "综合评价");
catalogMap5.put("number", 5);
list.add(catalogMap5);
final Map<String, Object> catalogMap6 = new HashMap<String, Object>();
catalogMap6.put("title", "关于报告");
catalogMap6.put("number", 6);
list.add(catalogMap6);
return list;
}
正文源文件页面如下图 (source.pdf)

生成目录代码
@Test
public void test() {
final List<Map<String, Object>> list = this.getCatalogList();
PdfDocument sourcePdf = null;
Document document = null;
try {
final PdfFont heiTi = this.getHeiTiFont();
// ***源文件:也即需要添加目录的文件
final String sourcePdfPath = "D:/itext-pdf/source.pdf";
sourcePdf = new PdfDocument(new PdfReader(sourcePdfPath));
final int totalPageNumber1 = sourcePdf.getNumberOfPages();
System.out.println("totalPageNumber****" + totalPageNumber1);
// ***添加目录后重新生成的文件
final String dest = "D:/itext-pdf/004.pdf";
final PdfDocument pdfDoc = new PdfDocument(new PdfWriter(dest));
document = new Document(pdfDoc);
document.add(new Paragraph(new Text("目录").setFont(heiTi)).setTextAlignment(TextAlignment.CENTER));
// 用于做书签使用
final PdfOutline root = pdfDoc.getOutlines(true);
// 生成的目录需要几页,在实际过程中是需要根据目录的条数计算得出
final int catalogNumber = 1;
if (catalogNumber > 1) {
for (int i = 1; i < catalogNumber; i++) {
pdfDoc.addNewPage();
}
}
// 上一页的页数,这个主要是因为在实际的文档中,会存在一个条目占多页的情况,例如本例中“阅读指南”就占2页
int upPageNumber = 0;
for (final Map<String, Object> map : list) {
final String title = map.get("title").toString();
final int curTotalPageNumber = pdfDoc.getNumberOfPages();
final Integer pageNumber = Integer.valueOf(map.get("number").toString());
// Copy page
System.out.println(title + "**" + pageNumber + "**" + upPageNumber + "********" + curTotalPageNumber);
// 当本页的页码与上一页页码直接多余1的时候,说明上一页后面还存在其它页,例如本例中的复制"测评目的"时要把 “阅读指南”的第2页先复制到新文档中。
if (pageNumber - upPageNumber > 1) {
for (int startNum = upPageNumber + 1; startNum < pageNumber; startNum++) {
final PdfPage page = sourcePdf.getPage(startNum).copyTo(pdfDoc);
pdfDoc.addPage(page);
}
}
upPageNumber = pageNumber;
final PdfPage page = sourcePdf.getPage(pageNumber).copyTo(pdfDoc);
pdfDoc.addPage(page);
System.out.println("page *****" + page + " ** " + page.getPdfObject());
// 用于点击目录条目做跳转的处理,类始于锚点定位的功能
final String destinationKey = "p" + pdfDoc.getNumberOfPages();
final PdfArray destinationArray = new PdfArray();
destinationArray.add(page.getPdfObject());
destinationArray.add(PdfName.XYZ);
destinationArray.add(new PdfNumber(0));
destinationArray.add(new PdfNumber(page.getMediaBox().getHeight()));
destinationArray.add(new PdfNumber(1));
pdfDoc.addNamedDestination(destinationKey, destinationArray);
// 书签
final Text text = new Text(title);
text.setFont(this.getSongTiFont());
final TOCTextRenderer renderer = new TOCTextRenderer(root, text);
text.setNextRenderer(renderer);
// 生成目录
final Paragraph p = new Paragraph();
p.setFont(heiTi);
p.addTabStops(new TabStop(540, TabAlignment.RIGHT, new DottedLine()));
p.add(text);
p.add(new Tab());
p.add(String.valueOf(pdfDoc.getNumberOfPages() - catalogNumber));
p.setProperty(Property.ACTION, PdfAction.createGoTo(destinationKey));
document.add(p);
}
} catch (final Exception e) {
e.printStackTrace();
} finally {
sourcePdf.close();
document.close();
}
}
TOCTextRenderer.java
public class TOCTextRenderer extends TextRenderer {
protected PdfOutline parentOutline;
public TOCTextRenderer(final PdfOutline parentOutline, final Text modelElement) {
super(modelElement);
this.parentOutline = parentOutline;
}
@Override
public void draw(final DrawContext drawContext) {
super.draw(drawContext);
Text t = (Text) this.modelElement;
Rectangle rect = this.getOccupiedAreaBBox();
PdfDestination dest = PdfExplicitDestination.createXYZ(drawContext.getDocument().getLastPage(), rect.getLeft(),
rect.getTop(), 0);
PdfOutline curOutline = this.parentOutline.addOutline(t.getText());
curOutline.addDestination(dest);
// PdfOutline secOutline = curOutline.addOutline(t.getText() + " - 2");
// secOutline.addDestination(dest);
// System.out.println(curOutline.getTitle());
}
}
最终生成的文档如下图

在实际生成带目录的PDF文档时,则要比上面的例子复杂一些,例如目录是有层级的,目录可能超过一页,目录可能也需要有页眉页脚等等,那就要做特殊的一些处理了,这些在接下来的搭建PDF脚手架里说明了。
本文已参与「新人创作礼」活动,一起开启掘金创作之路。