活动介绍

package org.example.databaseword.util; import com.itextpdf.kernel.colors.ColorConstants; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.layout.Document; import com.itextpdf.layout.element.*; import com.itextpdf.layout.properties.TabAlignment; import com.itextpdf.layout.properties.TextAlignment; import com.itextpdf.layout.properties.UnitValue; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.example.databaseword.entity.Catalogue; import org.example.databaseword.mapper.TableMapper; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.*; import java.util.List; @Slf4j @Component public class TableUtil { @Resource TableMapper tableMapper; // 配置常量 private static final float[] TABLE_COLUMN_WIDTHS = {1, 2, 1, 1, 1, 2}; private static final String[] TABLE_HEADERS = {"字段名", "类型", "长度", "NULL", "默认值", "注释"}; public void toPdf(List<Map<String, Object>> tables, String fileName, String title) { validateParameters(tables, fileName, title); try (PdfWriter writer = new PdfWriter(fileName); PdfDocument pdf = new PdfDocument(writer); Document document = new Document(pdf)) { // 初始化字体 PdfFont contentFont = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H"); // 添加封面页 addCoverPage(document, title, contentFont); //预生成目录数据 List<Catalogue> tocEntries = preGenerateTocEntries(tables); //在封面后插入目录 insertTableOfContents(document, tocEntries); // 生成表结构内容 generateTableContentWithToc(document, tables, contentFont); log.info("PDF生成成功: {}", fileName); } catch (IOException e) { log.error("PDF生成失败", e); throw new RuntimeException("PDF生成失败: " + e.getMessage(), e); } } // 预生成目录条目 private List<Catalogue> preGenerateTocEntries(List<Map<String, Object>> tables) { List<Catalogue> entries = new ArrayList<>(); int currentPage = 3; // 封面(1) + 目录(2) = 内容从第3页开始 for (Map<String, Object> tableMap : tables) { String tableName = getStringValue(tableMap, "TABLE_NAME"); String tableComment = getStringValue(tableMap, "TABLE_COMMENT"); entries.add(new Catalogue(tableName + (tableComment.isEmpty() ? "" : " (" + tableComment + ")"), currentPage )); } return entries; } //在封面后添加目录 private void insertTableOfContents(Document document, List<Catalogue> tocEntries) throws IOException { PdfFont CatalogueTitleFont = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H"); // 添加目录标题 Paragraph tocTitle = new Paragraph("目录") .setFont(CatalogueTitleFont) .setFontSize(16) .setBold() .setTextAlignment(TextAlignment.CENTER) .setMarginBottom(20); document.add(tocTitle); // 添加目录内容 for (Catalogue entry : tocEntries) { Paragraph tocItem = new Paragraph() .add(entry.getTitle()) .addTabStops(new TabStop(500, TabAlignment.RIGHT)) .add(new Tab()) .add(String.valueOf(entry.getPageNumber())) .setMarginBottom(5); document.add(tocItem); } document.add(new AreaBreak()); } /* * 生成表内容 */ private void generateTableContentWithToc(Document document, List<Map<String, Object>> tables, PdfFont contentFont) { int pageNum = 3; for (Map<String, Object> tableMap : tables) { String tableName = getStringValue(tableMap, "TABLE_NAME"); String tableComment = getStringValue(tableMap, "TABLE_COMMENT"); // 添加表标题 addTableTitle(document, tableName, tableComment, contentFont); // 创建并填充表格 Table pdfTable = createTableWithHeader(contentFont); fillTableContent(pdfTable, tableName, contentFont); document.add(pdfTable); } } /** * 验证输入参数 */ private void validateParameters(List<Map<String, Object>> tables, String fileName, String title) { Objects.requireNonNull(tables, "表信息列表不能为null"); Objects.requireNonNull(fileName, "文件名不能为null"); Objects.requireNonNull(title, "标题不能为null"); if (tables.isEmpty()) { throw new IllegalArgumentException("表信息列表不能为空"); } if (fileName.trim().isEmpty()) { throw new IllegalArgumentException("文件名不能为空"); } } /** * 添加封面页 */ private void addCoverPage(Document document, String title, PdfFont titleFont) { Paragraph titleParagraph = new Paragraph(title) .setFont(titleFont) .setFontSize(24) .setTextAlignment(TextAlignment.CENTER) .setMarginBottom(20); document.add(titleParagraph); document.add(new Paragraph("\n")); // 添加空行 document.add(new AreaBreak()); } /** * 添加表标题 */ private void addTableTitle(Document document, String tableName, String tableComment, PdfFont font) { Paragraph tableTitle = new Paragraph("表名: " + tableName + " (" + tableComment + ")") .setFont(font) .setFontSize(12) .setTextAlignment(TextAlignment.CENTER) .setMarginBottom(10); document.add(tableTitle); } /** * 创建带表头的表格 */ private Table createTableWithHeader(PdfFont headerFont) { Table table = new Table(UnitValue.createPercentArray(TABLE_COLUMN_WIDTHS)); table.setWidth(UnitValue.createPercentValue(100)); // 添加表头 for (String header : TABLE_HEADERS) { Cell cell = new Cell() .add(new Paragraph(header).setFont(headerFont).setFontSize(10)) .setBackgroundColor(ColorConstants.LIGHT_GRAY) .setTextAlignment(TextAlignment.CENTER); table.addHeaderCell(cell); } return table; } /** * 填充表格内容 */ private void fillTableContent(Table table, String tableName, PdfFont contentFont) { List<Map<String, Object>> fields = getTable(tableName); for (Map<String, Object> field : fields) { table.addCell(createContentCell(getStringValue(field, "COLUMN_NAME"), contentFont)); table.addCell(createContentCell(getStringValue(field, "DATA_TYPE"), contentFont)); table.addCell(createContentCell(getStringValue(field, "CHARACTER_MAXIMUM_LENGTH"), contentFont)); table.addCell(createContentCell(getStringValue(field, "IS_NULLABLE"), contentFont)); table.addCell(createContentCell(getStringValue(field, "COLUMN_DEFAULT"), contentFont)); table.addCell(createContentCell(getStringValue(field, "COLUMN_COMMENT"), contentFont)); } } /** * 安全获取字符串值 */ private String getStringValue(Map<String, Object> map, String key) { Object valueObj = map.getOrDefault(key, ""); return valueObj instanceof String ? (String) valueObj : (valueObj == null ? "" : valueObj.toString()); } /** * 从数据库中获取表字段信息 */ private List<Map<String, Object>> getTable(String tableName) { try { return tableMapper.getTable(tableName); } catch (Exception e) { log.error("查询表结构失败: {}", tableName, e); throw new RuntimeException("查询表结构失败: " + tableName, e); } } /** * 创建内容单元格 */ private Cell createContentCell(String text, PdfFont font) { return new Cell() .add(new Paragraph(text).setFont(font).setFontSize(9)) .setTextAlignment(TextAlignment.CENTER); } } 使用itext7语法添加目录

时间: 2025-07-31 20:10:33 浏览: 9
<think>我们正在使用iText7来生成PDF,并添加目录(TableofContents)。目录通常是一个可点击的链接列表,指向文档中的各个章节。步骤:1.创建PdfDocument和Document对象。2.创建目录条目:我们需要为每个章节创建目录条目,包括标题和页码。同时,我们需要在文档中设置锚点(目的地)以便目录中的链接可以跳转到指定位置。3.在生成文档内容时,为每个章节标题设置一个目的地(Destination),并在目录中添加指向该目的地的链接。4.在文档开头(或指定位置)添加目录。具体实现:-使用`PdfDestination`创建目的地(通常是一个具体的页面位置)。-使用`PdfOutline`或`PdfAction`来创建链接动作。-但是,更简单的方法是使用`PdfPage`的`createDestination`方法创建目的地,然后创建链接(`Link`)对象,并将该链接添加到段落(或其他元素)中作为目录项。另外,iText7提供了`TocEventHandler`类(在com.itextpdf.kernel.pdf.navigation包中)来帮助生成目录,但更常用的方法是手动创建目录,因为这样更灵活。以下是一个基本的示例步骤:1.初始化文档:PdfDocumentpdf=newPdfDocument(newPdfWriter(dest));Documentdocument=newDocument(pdf);2.在文档开始处,我们留出目录的位置(可以先写一个标题“目录”,然后后面再添加具体的目录项)。但是,由于目录的页码需要在生成文档内容后才能确定,因此我们通常采用两步法:-首先,生成文档内容,并记录每个章节的标题、目的地和所在的页码。-然后,在文档开头生成目录,使用记录的信息。然而,iText7允许我们在生成内容的同时收集目录信息,然后在最后再添加目录。但是,目录的页码需要知道内容生成后的页码,所以我们可以先创建文档内容,然后再回到第一页添加目录。但是,如果我们希望目录有正确的页码,并且目录本身也占用页码,那么我们需要在生成内容时记录章节的起始页码(注意目录页会改变后续内容的页码)。因此,更常见的做法是:-先生成目录(但此时不知道后续内容的页码),然后生成内容,再更新目录的页码。这比较麻烦。另一种做法:在生成文档内容时,为每个章节设置一个锚点(目的地),并记录这些锚点(目的地)和章节标题。在内容生成完成后,再在文档开头插入目录,目录中的页码可以通过目的地的页码获得。但是,由于在插入目录时,目录会占用一些页面,导致后面的页码都发生偏移,所以我们需要重新调整目录中的页码(加上目录页数)?这样很复杂。因此,我们可以使用iText7的`PageOffset`特性:即记录绝对页码,然后考虑目录的页数偏移。但是,iText7提供了一个更高级的工具:`PdfPage`的`createDestination`方法创建的目的地是绝对位置,不会因为前面添加内容而改变。因此,我们可以在生成内容时记录每个章节的绝对页码(即它在整个文档中的页码,不考虑目录插入的位置),然后当我们插入目录时,目录会插入在文档开头,那么后面的页码都会增加(比如目录有2页,那么原来第1章在绝对页码3,现在变成了5?)。但是,如果我们使用书签(outline)则不会,因为书签是记录绝对页码。然而,目录中的链接需要跳转到绝对页码的位置,所以即使我们在目录后添加内容导致页码偏移,链接的绝对页码仍然正确。因此,我们可以这样操作:a.先写文档内容(不写目录),为每个章节创建目的地(使用`PdfExplicitDestination`),并记录每个章节的标题和目的地(或绝对页码)。b.然后,在文档开头插入一个新页面(用于目录),在这个页面上,我们根据记录的信息创建目录(每个目录项是一个链接,链接到之前创建的目的地)。c.由于目录页的插入,文档内容会向后移动,但目录项中的链接指向的绝对页码并没有改变(还是之前记录的那个页码),所以点击目录项会跳转到正确的页面(因为PDF阅读器会跳转到绝对页码)。但是,这样目录项中显示的页码是绝对页码,而目录页插入后,用户看到的章节页码实际上是目录页数+原来的页码。例如,目录有2页,那么原来第1章在绝对页码3,用户看到的页码应该是5(如果目录页是1和2,那么第1章从第3页开始,但用户看到的是第3页,因为目录页是1和2,第3页就是第3页?)不对,页码是连续的,目录页会占用页码。所以,我们在目录项中显示的页码应该是绝对页码(即用户看到的页码,因为目录页插入后,页码已经重新编排了?)实际上,PDF的页码是连续的,从第一页开始为1,第二页为2,等等。当我们先写内容时,第一章从第1页开始,然后我们插入目录到开头,那么第一章就变成了第3页(如果目录占2页)。但是,我们在写目录时,目录项指向的绝对页码是3(即第一章所在的第一页在文档中的第3页),而我们在目录项中应该显示3(即用户看到的页码是3)。所以,我们可以在生成内容时记录每个章节的绝对页码(即最终在文档中的位置),然后在目录中显示这个页码。但是,在生成内容时我们并不知道目录会有多少页,所以无法预先知道章节的绝对页码。因此,我们需要分两步:第一步:生成所有内容(包括章节标题和锚点),并记录每个章节的标题和锚点(目的地),但不包括页码(因为页码会变)。第二步:在文档开头生成目录,此时目录的每一页都会导致后面内容的页码增加。但是,我们可以通过记录每个章节的目的地所在的页码(在生成内容后,我们可以得到目的地的页码,这个页码是已经包含了目录页的页码,因为目录页是后来插入的?不对,因为目录页是在内容之前插入的,所以内容的页码会往后移,但是我们在内容生成时记录的页码是内容在文档中的位置(此时目录页还没有插入),所以这个页码是内容在文档中的起始页码(即如果没有目录页,第一章从第1页开始,有了目录页,第一章就从第1+n页开始)。所以我们需要在插入目录页之后,再更新目录项中的页码?这很困难。为了解决这个问题,我们可以采用以下方法:1.先创建一个临时的PdfDocument,用于计算目录的页数?这样太复杂。实际上,iText7提供了`DocumentRenderer`,我们可以利用它来计算目录需要多少页,然后预留出这些页面。但是,更简单的方法是在最后添加目录,然后使用书签(outline)来创建目录,但这样目录不会显示在页面中(书签是PDF的书签导航窗格中的,不是页面上的内容)。根据用户需求,是要在PDF页面上显示目录,而不是书签。因此,我们采用以下步骤:1.创建一个PdfDocument和一个Document对象。2.我们首先创建目录的占位符(比如一个`Div`),但不立即填充内容,而是先记录每个章节的标题和目的地(锚点)。同时,我们记录每个章节的起始位置(在文档中的位置,即创建章节标题时的位置)。3.在文档内容生成完毕后,我们知道每个章节的起始页码(此时这些页码已经包含了目录的页码,因为目录占位符在文档开头,但是占位符还没有内容,所以还不知道具体占多少页)。4.然后,我们为目录占位符填充内容(即生成目录项,每个目录项是一个指向之前记录的目的地的链接,并显示页码)。但是,此时目录占位符的内容可能会多页,而我们在生成内容时并不知道目录会占多少页,所以之前记录的章节页码可能不准确(因为目录占位符在生成内容时还没有内容,所以它不占页,而当我们填充目录内容时,它会导致后面的内容往下移动,从而改变章节的页码)。这似乎是一个循环问题。为了解决这个问题,我们可以使用iText7的延迟布局(delayedlayout)功能:先创建一个区域(Area)用于目录,然后计算这个区域需要多少页,再调整后续内容的页码偏移。但是,这需要较高级的布局技巧。另一种常见的做法是:先生成整个文档内容(不包括目录),同时记录每个章节的标题和绝对页码(即没有目录时的页码)。然后,计算目录需要多少页(例如,通过创建一个临时的Document对象来模拟目录的布局)。接着,我们重新开始创建实际的文档:-先添加目录(此时我们知道目录页数,比如n页)。-然后,添加文档内容(此时,每个章节的页码就是n+1,n+2,...)。-在添加文档内容时,为每个章节设置目的地(锚点)。-在目录中,每个目录项的链接指向对应的锚点,并显示页码(即n+原来的页码)。但是,这种方法需要两次生成内容,效率不高。鉴于iText7的复杂性,这里提供一个简化的方法:我们不显示目录项的实际页码,只显示标题和链接。或者,如果目录页数不多,我们可以先预留一页(或几页)作为目录页,然后生成内容。如果目录超出一页,我们可以在后面再调整(这需要复杂的页面操作)。在iText7中,有一个更直接的方法:使用`PdfPage`的`createDestination`创建目的地,然后使用`PdfLinkAnnotation`创建链接。我们可以在生成内容后,再在文档开头添加一个目录页面,目录页面上的链接指向各个章节的目的地(此时目的地的页码是已经确定的,即使目录页插入在开头,但目的地的页码是生成内容时的页码,而目录页的插入会导致页码整体偏移,但目的地的页码已经包含了目录页的偏移,因为目录页是先添加的?)具体步骤:1.创建PdfDocument和Document。2.在文档的开头,我们添加一个标题“目录”,然后我们添加一个占位段落(或什么都不加,因为目录内容稍后添加)。但是,这样目录的页码会从1开始,而内容从第2页开始。3.然后,我们生成文档的各个章节,为每个章节的标题创建一个目的地(使用`PdfExplicitDestination`),并记录(标题,目的地,实际页码)【注意:这里的实际页码是包含了目录页的】。4.在生成完所有内容后,我们回到目录页(第一页),用记录的信息生成目录(每个目录项是一个链接,指向目的地,并显示页码)。但是,在步骤3中,生成章节时,页码是从2开始的(因为目录页是第一页)。所以,我们记录的目的地页码就是章节所在的页码(用户看到的页码)。然而,在步骤4中,我们回到第一页(目录页)添加目录内容时,我们需要知道每个章节的页码(已经记录下来了)。所以,我们可以这样:示例代码:注意:由于我们是在生成内容后才在目录页上添加目录,所以我们需要使用`PdfCanvas`在目录页上绘制内容,或者使用`Canvas`对象。但是,更简单的方法是:在生成内容前,我们在目录页上添加一个`Div`,然后生成内容后,我们再在这个`Div`中添加目录项。但是,iText7的`Document`是顺序添加的,不能回头修改已经添加的元素。因此,我们只能使用`PdfCanvas`来在已有的页面上绘制目录。这需要手动计算位置,比较麻烦。另一种思路:我们不在生成内容前添加目录页,而是先生成所有内容(包括章节和锚点),记录每个章节的标题和目的地(以及绝对页码)。然后,在文档的开头插入新的页面(目录页),并在这些页面上绘制目录(链接到之前记录的目的地)。这样,目录页的插入会导致后面所有内容的页码增加,但是我们记录的目的地是绝对页码(即章节在文档中的实际页码,包括目录页插入后的页码),所以链接仍然有效?不对,因为插入新页面后,PDF的页面顺序改变了,原来的页面页码增加了,所以之前记录的目的地(绝对页码)指向的位置已经不对了(它指向的是插入目录页之前的位置)。所以,我们需要更新目的地?这很困难。鉴于以上复杂性,我建议采用两步法:第一步:计算目录所需的页数-先创建一个临时的PdfDocument和一个临时的Document,模拟生成目录(使用记录的所有章节标题),但不添加链接。然后,让Document进行布局,计算目录占用的页数(假设为tocPages)。第二步:正式生成PDF-创建真正的PdfDocument和Document。-添加tocPages个空白页面(作为目录页的占位)。-生成文档内容,同时为每个章节标题创建目的地,并记录目的地和章节标题。注意,此时章节的页码是从tocPages+1开始的。-在生成内容后,我们得到每个章节的绝对页码(即从文档第一页开始算起,目录页占用了前tocPages页,所以第一章的页码是tocPages+1)。-然后,我们回到前tocPages页,用Canvas在这些页面上绘制目录(包括链接和页码)。这样,目录中的页码就是章节的绝对页码(即用户看到的页码),而链接指向的目的地也是正确的(因为目的地在生成章节时创建,其页码已经是包含目录页的)。示例代码框架:```java//第一步:计算目录页数List<String>titles=...//章节标题列表(模拟)inttocPages=calculateTocPages(titles);//计算目录需要几页//第二步:正式生成PdfDocumentpdfDoc=newPdfDocument(newPdfWriter(dest));Documentdoc=newDocument(pdfDoc);//添加tocPages个空白页(作为目录页)for(inti=0;i<tocPages;i++){doc.add(newAreaBreak());//添加新页面}//现在,我们回到第一页(目录页的第一页)准备绘制目录?但是,现在doc.getPageNumber()是1,但内容还没有添加,我们添加了tocPages个空白页,所以当前页是第tocPages页?然后我们添加内容会在第tocPages页之后。//但是,我们希望在正式内容开始前,先预留tocPages页,然后添加内容。所以,我们先添加空白页,然后添加内容,最后再回到前面的空白页绘制目录。//记录章节信息classChapterInfo{Stringtitle;PdfDestinationdestination;intpageNum;}List<ChapterInfo>chapters=newArrayList<>();//生成内容//注意:当前页面已经是tocPages+1了for(inti=0;i<numChapters;i++){//添加章节标题ParagraphchapterTitle=newParagraph("Chapter"+(i+1));//创建目的地PdfDestinationdest=PdfExplicitDestination.createFit(doc.getPdfDocument().getLastPage());//或者:在添加标题时,我们还没有页面,所以我们可以这样://在添加章节标题之前,记录当前位置,然后添加标题,再获取当前页面//但是,我们可以在添加标题后,再创建目的地,因为标题在页面上,我们使用该页面创建目的地。//添加标题doc.add(chapterTitle);//现在,标题所在的页面是当前页(即doc.getPdfDocument().getNumberOfPages())PdfPagepage=doc.getPdfDocument().getLastPage();//创建目的地PdfDestinationdestination=PdfExplicitDestination.createFit(page);ChapterInfoinfo=newChapterInfo();info.title="Chapter"+(i+1);info.destination=destination;info.pageNum=doc.getPdfDocument().getPageNumber(page);//得到该页的页码chapters.add(info);//添加章节内容...}//现在,我们回到前面的目录页(第1页到第tocPages页)绘制目录for(inti=1;i<=tocPages;i++){PdfPagetocPage=pdfDoc.getPage(i);Canvascanvas=newCanvas(tocPage,tocPage.getPageSize());//在canvas上绘制目录,根据章节信息,注意:一页可能放不下所有目录,所以我们需要分页绘制,这里简化处理//假设我们只在第一页绘制目录if(i==1){for(ChapterInfochapter:chapters){//创建链接PdfActionaction=PdfAction.createGoTo(chapter.destination);Linklink=newLink(chapter.title,action);Paragraphp=newParagraph(link).add("............"+chapter.pageNum);//标题和页码canvas.add(p);}}canvas.close();}doc.close();```但是,计算目录页数(calculateTocPages)的函数需要模拟目录的生成,以确定需要多少页。我们可以这样做:```javaprivateintcalculateTocPages(List<String>titles)throwsIOException{ByteArrayOutputStreambaos=newByteArrayOutputStream();PdfDocumenttempPdf=newPdfDocument(newPdfWriter(baos));DocumenttempDoc=newDocument(tempPdf);//模拟添加目录项for(Stringtitle:titles){tempDoc.add(newParagraph(title));}tempDoc.close();//获取总页数intpages=tempPdf.getNumberOfPages();tempPdf.close();returnpages;}```但是,这个模拟可能不准确,因为目录项可能包含链接(但链接不影响布局),而且目录项通常有前导点(如点线)和页码,我们这里只添加了标题。所以,我们最好按照实际要添加的目录项样式来模拟。另外,目录项通常有缩进,我们可以在模拟时使用相同的样式。由于目录页数的计算可能不精确,我们可以稍微多预留一页,或者在生成目录时动态添加页面(如果目录内容超出预留页面,则再添加一页)。以上方法比较复杂,但可以实现。然而,iText7有一个更高级的组件`TOC`(目录)生成器,在商业版中可能有更好的支持。在开源版中,我们可以使用`PdfOutline`创建书签,但书签不会显示在页面上。如果用户要求页面上的目录,那么上述方法是一种可行的方案。考虑到实现的复杂性,这里提供一个简化版:假设目录只有一页,且章节数量不多。简化版代码示例:```java//假设目录只有一页PdfDocumentpdfDoc=newPdfDocument(newPdfWriter(dest));Documentdoc=newDocument(pdfDoc);//先添加一个空白页作为目录页doc.add(newAreaBreak());//添加一个空白页(第1页)//记录目录页的页码(第1页)//添加内容(从第2页开始)List<ChapterInfo>chapters=newArrayList<>();//添加内容,记录章节信息for(inti=0;i<numChapters;i++){ParagraphchapterTitle=newParagraph("Chapter"+(i+1));doc.add(chapterTitle);PdfPagepage=pdfDoc.getLastPage();PdfDestinationdestination=PdfExplicitDestination.createFit(page);ChapterInfoinfo=newChapterInfo();info.title="Chapter"+(i+1);info.destination=destination;info.pageNum=pdfDoc.getPageNumber(page);//注意:此时页码从2开始chapters.add(info);//添加内容...}//现在,回到第1页(目录页)绘制目录PdfPagetocPage=pdfDoc.getPage(1);Canvascanvas=newCanvas(tocPage,tocPage.getPageSize());//添加目录标题canvas.add(newParagraph("TableofContents"));for(ChapterInfochapter:chapters){PdfActionaction=PdfAction.createGoTo(chapter.destination);Linklink=newLink(chapter.title,action);Paragraphp=newParagraph(link).add("............"+chapter.pageNum);//注意:这里chapter.pageNum是从2开始的页码canvas.add(p);}canvas.close();doc.close();```这个简化版假设目录只有一页,且章节标题不太多。如果目录超过一页,则无法显示完整。因此,在实际应用中,我们需要动态计算目录页数,并预留足够的页面。由于时间限制,这里不展开复杂实现。最后,注意:在添加章节标题时,我们使用`PdfExplicitDestination.createFit(page)`创建了一个适合整个页面的目的地。你也可以使用`createXYZ`等方法创建到具体位置的目的地。另外,目录中的链接也可以使用`PdfAction.createGoTo`来创建。希望这个解答能帮助你。由于iText7的目录生成相对复杂,可能需要根据实际需求进行调整。相关问题:1.如何在使用iText7生成PDF时添加书签(即导航窗格中的目录)?2.iText7中如何计算文本占用的页面数?3.如何在iText7中创建内部链接(超链接)?</think>### 如何使用iText7添加目录到PDF文件 在iText7中添加目录(Table of Contents)需要结合**目的地(Destination)** 和**链接(Link)** 功能实现。以下是具体步骤和示例代码: #### 实现步骤 1. **创建章节目的地** 为每个章节标题创建锚点(`PdfDestination`),作为目录项的跳转目标。 2. **生成目录结构** 在文档开头添加目录项(标题+页码),每个项链接到对应的章节目的地。 3. **处理页码偏移** 因目录本身占用页码,需在生成内容后计算实际页码。 #### 完整示例代码 ```java import com.itextpdf.kernel.pdf.*; import com.itextpdf.kernel.pdf.action.PdfAction; import com.itextpdf.kernel.pdf.navigation.PdfDestination; import com.itextpdf.layout.Document; import com.itextpdf.layout.element.*; import com.itextpdf.layout.properties.TextAlignment; import java.util.ArrayList; import java.util.List; public class PdfWithTOC { static class ChapterInfo { String title; PdfDestination destination; int pageNumber; } public static void main(String[] args) throws Exception { // 1. 初始化PDF文档 PdfDocument pdf = new PdfDocument(new PdfWriter("output.pdf")); Document doc = new Document(pdf); // 存储目录信息 List<ChapterInfo> tocEntries = new ArrayList<>(); // 2. 添加目录标题 Paragraph tocHeader = new Paragraph("目录") .setTextAlignment(TextAlignment.CENTER) .setFontSize(16); doc.add(tocHeader); // 3. 生成内容章节(实际场景中替换为您的真实内容) addChapter(doc, "第一章:引言", "这是引言内容...", tocEntries); addChapter(doc, "第二章:方法", "这是方法部分...", tocEntries); addChapter(doc, "第三章:结果", "数据分析结果...", tocEntries); // 4. 在预留位置填充目录 for (ChapterInfo entry : tocEntries) { // 创建指向章节的链接 PdfAction action = PdfAction.createGoTo(entry.destination); Link link = new Link(entry.title, action); // 添加目录项(标题 + 页码) Paragraph tocItem = new Paragraph() .add(link) .add(" ................................ ") .add(String.valueOf(entry.pageNumber)); doc.add(tocItem); } doc.close(); } private static void addChapter(Document doc, String title, String content, List<ChapterInfo> tocEntries) { // 添加章节标题(创建目的地) Paragraph chapterTitle = new Paragraph(title).setFontSize(14); doc.add(chapterTitle); // 记录章节信息(当前页面作为目的地) PdfDestination dest = PdfDestination.makeDestination(doc.getPdfDocument().getLastPage()); ChapterInfo info = new ChapterInfo(); info.title = title; info.destination = dest; info.pageNumber = doc.getPdfDocument().getNumberOfPages(); // 记录实际页码 tocEntries.add(info); // 添加章节内容 Paragraph body = new Paragraph(content); doc.add(body); // 添加分页符 doc.add(new AreaBreak()); } } ``` #### 关键说明 1. **目的地(Destination)** 使用`PdfDestination.makeDestination()`创建页面锚点,目录项通过`PdfAction.createGoTo()`链接到这些锚点。 2. **页码处理** `doc.getPdfDocument().getNumberOfPages()`动态获取章节的实际页码(已包含目录页的页码偏移)。 3. **目录项格式** 每个目录项包含: - 可点击的章节标题(`Link`元素) - 视觉引导线(点线或空格) - 实际页码 4. **内容顺序** 先添加目录标题占位 → 生成所有内容章节 → 最后回填目录项(确保页码正确) #### 优化建议 1. **自动分页** 目录较长时使用`AreaBreak`分页: ```java if (tocEntries.size() > 10) { doc.add(new AreaBreak()); } ``` 2. **样式美化** - 添加前导点:`tocItem.addTabStops(new TabStop(500, TabAlignment.RIGHT, new DottedLine()));` - 设置目录缩进:`tocItem.setMarginLeft(20);` 3. **多级目录** 嵌套使用`Div`容器实现层级结构: ```java Div subSection = new Div().setMarginLeft(15); subSection.add(new Paragraph("1.1 子章节")); doc.add(subSection); ``` #### 注意事项 - **页码计算**:必须在所有内容生成后填充目录页码 - **链接范围**:确保目的地页面在文档关闭前未修改 - **特殊字符**:标题含特殊符号时使用`PdfString`编码 > 此方案通过动态记录章节位置实现目录生成,避免了静态页码计算的误差。实际应用中可根据文档复杂度扩展多级目录支持[^1][^2]。
阅读全文

相关推荐

最新推荐

recommend-type

Android程序报错程序包org.apache.http不存在问题的解决方法

在Android开发过程中,有时会遇到一个常见的错误,即“程序包org.apache.http不存在”。这个问题主要出现在Android 6.0(API级别23)及更高版本,因为从Android M开始,官方已经移除了对Apache HTTP客户端库的支持。...
recommend-type

微信小程序提取公用函数到util.js及使用方法示例

这篇内容主要讲解如何将公用函数提取到`util.js`文件,并在其他小程序页面中进行调用。下面我们将详细探讨这个过程。 首先,创建`util.js`文件。这通常是在项目的一个独立目录下,比如`common/utils`,这样方便管理...
recommend-type

解决 java.lang.NoSuchMethodError的错误

解决 java.lang.NoSuchMethodError 的错误 Java.lang.NoSuchMethodError 错误是一种常见的 Java 异常,它发生在 Java 虚拟机 (JVM) 无法找到某个类的特定方法时。这种错误可能是由于项目依赖比较复杂、Java 运行...
recommend-type

java.lang.NoClassDefFoundError错误解决办法

"java.lang.NoClassDefFoundError错误解决办法" java.lang.NoClassDefFoundError错误是一种常见的Java错误,它发生在Java虚拟机在编译时能找到合适的类,而在运行时不能找到合适的类导致的错误。...
recommend-type

Java Collections.sort()实现List排序的默认方法和自定义方法

Collections.sort(list, Collator.getInstance(java.util.Locale.CHINA)); for (int i = 0; i &lt; list.size(); i++) { System.out.print(list.get(i)); } ``` 在上面的示例代码中,我们首先创建了一个ArrayList对象...
recommend-type

Python程序TXLWizard生成TXL文件及转换工具介绍

### 知识点详细说明: #### 1. 图形旋转与TXL向导 图形旋转是图形学领域的一个基本操作,用于改变图形的方向。在本上下文中,TXL向导(TXLWizard)是由Esteban Marin编写的Python程序,它实现了特定的图形旋转功能,主要用于电子束光刻掩模的生成。光刻掩模是半导体制造过程中非常关键的一个环节,它确定了在硅片上沉积材料的精确位置。TXL向导通过生成特定格式的TXL文件来辅助这一过程。 #### 2. TXL文件格式与用途 TXL文件格式是一种基于文本的文件格式,它设计得易于使用,并且可以通过各种脚本语言如Python和Matlab生成。这种格式通常用于电子束光刻中,因为它的文本形式使得它可以通过编程快速创建复杂的掩模设计。TXL文件格式支持引用对象和复制对象数组(如SREF和AREF),这些特性可以用于优化电子束光刻设备的性能。 #### 3. TXLWizard的特性与优势 - **结构化的Python脚本:** TXLWizard 使用结构良好的脚本来创建遮罩,这有助于开发者创建清晰、易于维护的代码。 - **灵活的Python脚本:** 作为Python程序,TXLWizard 可以利用Python语言的灵活性和强大的库集合来编写复杂的掩模生成逻辑。 - **可读性和可重用性:** 生成的掩码代码易于阅读,开发者可以轻松地重用和修改以适应不同的需求。 - **自动标签生成:** TXLWizard 还包括自动为图形对象生成标签的功能,这在管理复杂图形时非常有用。 #### 4. TXL转换器的功能 - **查看.TXL文件:** TXL转换器(TXLConverter)允许用户将TXL文件转换成HTML或SVG格式,这样用户就可以使用任何现代浏览器或矢量图形应用程序来查看文件。 - **缩放和平移:** 转换后的文件支持缩放和平移功能,这使得用户在图形界面中更容易查看细节和整体结构。 - **快速转换:** TXL转换器还提供快速的文件转换功能,以实现有效的蒙版开发工作流程。 #### 5. 应用场景与技术参考 TXLWizard的应用场景主要集中在电子束光刻技术中,特别是用于设计和制作半导体器件时所需的掩模。TXLWizard作为一个向导,不仅提供了生成TXL文件的基础框架,还提供了一种方式来优化掩模设计,提高光刻过程的效率和精度。对于需要进行光刻掩模设计的工程师和研究人员来说,TXLWizard提供了一种有效的方法来实现他们的设计目标。 #### 6. 系统开源特性 标签“系统开源”表明TXLWizard遵循开放源代码的原则,这意味着源代码对所有人开放,允许用户自由地查看、修改和分发软件。开源项目通常拥有活跃的社区,社区成员可以合作改进软件,添加新功能,或帮助解决遇到的问题。这种开放性促进了技术创新,并允许用户根据自己的需求定制软件。 #### 7. 压缩包子文件的文件名称列表 文件名称列表中的“txlwizard-master”可能指的是TXLWizard项目的主版本库或主分支。这个名称表明了这是项目源代码的中心点,其他开发者会从这个主分支拉取代码进行合作开发或部署。以“-master”结尾通常是版本控制系统中表示主要开发线路的常见约定,例如Git中的master(现在更常被称为main)分支。 通过这些知识点的详细解释,我们可以看到TXLWizard不仅是一个用于生成TXL文件的工具,它还整合了一系列的功能,使得电子束光刻掩模的设计工作更为高效和直观。同时,作为一个开源项目,它能够借助社区的力量不断进步,为用户带来更多的便利和创新。
recommend-type

【创新图生成:扣子平台的技术前沿与创新思维】:引领图像生成技术的新潮流

# 1. 扣子平台的图像生成技术概述 在当今数字化时代,图像生成技术正成为推动技术创新和社会变革的关键力量。扣子平台正是这一技术革新的代表。本章将对扣子平台的图像生成技术进行一个全面概述,探究它如何将理论技术转化为实际应用,并带来显著的社会影响。 扣子平台运用了先进的图像生成技术,将现实世界和数字空间无缝融合,打造了沉浸式的视觉体验。其技术的核心是通过高度复杂的算法模型,快速生成高质
recommend-type

海康威视机器视觉工程师考核

<think>好的,我现在需要帮助用户了解海康威视机器视觉工程师的考核内容和要求。根据用户提供的引用资料,特别是引用[1]和[2],里面提到了考核素材包分为初级和中级,涵盖理论、算法、应用案例等。首先,我要整理这些信息,确保结构清晰,符合用户要求的格式。 接下来,我需要确认素材包的具体内容,比如初级和中级的不同点。引用[2]提到初级包含基础理论、算法实现和实际案例,中级则增加复杂算法和项目分析。这部分需要分点说明,方便用户理解层次。 另外,用户可能想知道如何准备考核,比如下载素材、学习顺序、模拟考核等,引用[2]中有使用说明和注意事项,这部分也要涵盖进去。同时要注意提醒用户考核窗口已关闭,
recommend-type

Linux环境下Docker Hub公共容器映像检测工具集

在给出的知识点中,我们需要详细解释有关Docker Hub、公共容器映像、容器编排器以及如何与这些工具交互的详细信息。同时,我们会涵盖Linux系统下的相关操作和工具使用,以及如何在ECS和Kubernetes等容器编排工具中运用这些检测工具。 ### Docker Hub 和公共容器映像 Docker Hub是Docker公司提供的一项服务,它允许用户存储、管理以及分享Docker镜像。Docker镜像可以视为应用程序或服务的“快照”,包含了运行特定软件所需的所有必要文件和配置。公共容器映像指的是那些被标记为公开可见的Docker镜像,任何用户都可以拉取并使用这些镜像。 ### 静态和动态标识工具 静态和动态标识工具在Docker Hub上用于识别和分析公共容器映像。静态标识通常指的是在不运行镜像的情况下分析镜像的元数据和内容,例如检查Dockerfile中的指令、环境变量、端口映射等。动态标识则需要在容器运行时对容器的行为和性能进行监控和分析,如资源使用率、网络通信等。 ### 容器编排器与Docker映像 容器编排器是用于自动化容器部署、管理和扩展的工具。在Docker环境中,容器编排器能够自动化地启动、停止以及管理容器的生命周期。常见的容器编排器包括ECS和Kubernetes。 - **ECS (Elastic Container Service)**:是由亚马逊提供的容器编排服务,支持Docker容器,并提供了一种简单的方式来运行、停止以及管理容器化应用程序。 - **Kubernetes**:是一个开源平台,用于自动化容器化应用程序的部署、扩展和操作。它已经成为容器编排领域的事实标准。 ### 如何使用静态和动态标识工具 要使用这些静态和动态标识工具,首先需要获取并安装它们。从给定信息中了解到,可以通过克隆仓库或下载压缩包并解压到本地系统中。之后,根据需要针对不同的容器编排环境(如Dockerfile、ECS、Kubernetes)编写配置,以集成和使用这些检测工具。 ### Dockerfile中的工具使用 在Dockerfile中使用工具意味着将检测工具的指令嵌入到构建过程中。这可能包括安装检测工具的命令、运行容器扫描的步骤,以及将扫描结果集成到镜像构建流程中,确保只有通过安全和合规检查的容器镜像才能被构建和部署。 ### ECS与Kubernetes中的工具集成 在ECS或Kubernetes环境中,工具的集成可能涉及到创建特定的配置文件、定义服务和部署策略,以及编写脚本或控制器来自动执行检测任务。这样可以在容器编排的过程中实现实时监控,确保容器编排器只使用符合预期的、安全的容器镜像。 ### Linux系统下的操作 在Linux系统下操作这些工具,用户可能需要具备一定的系统管理和配置能力。这包括使用Linux命令行工具、管理文件系统权限、配置网络以及安装和配置软件包等。 ### 总结 综上所述,Docker Hub上的静态和动态标识工具提供了一种方法来检测和分析公共容器映像,确保这些镜像的安全性和可靠性。这些工具在Linux开发环境中尤为重要,因为它们帮助开发人员和运维人员确保他们的容器映像满足安全要求。通过在Dockerfile、ECS和Kubernetes中正确使用这些工具,可以提高应用程序的安全性,减少由于使用不安全的容器镜像带来的风险。此外,掌握Linux系统下的操作技能,可以更好地管理和维护这些工具,确保它们能够有效地发挥作用。
recommend-type

【扣子平台图像艺术探究:理论与实践的完美结合】:深入学习图像生成的艺术

# 1. 图像艺术的理论基础 艺术领域的每一个流派和技巧都有其理论基础。在图像艺术中,理论基础不仅是对艺术表现形式的认知,也是掌握艺术创作内在逻辑的关键。深入理解图像艺术的理论基础,能够帮助艺术家们在创作过程中更加明确地表达自己的艺术意图,以及更好地与观众沟通。 图像艺术的理论