itext7 合并 HtmlConverter.convertToDocument 创建的文档并保留大纲

itext7 merging documents created by HtmlConverter.convertToDocument and perserving outlines

我的两个文档是使用 HtmlConverter.convertToDocument 创建的,然后合并为一个 PDF:

PdfDocument pdf = new PdfDocument(new PdfWriter(pdfDest));

PdfMerger merger = new PdfMerger(pdf, false, true).setCloseSourceDocuments(true);

// Convert
ConverterProperties converterProperties = new ConverterProperties().setBaseUri(resourceFolder);
OutlineHandler outlineHandler = OutlineHandler.createStandardHandler();
converterProperties.setBaseUri(".");
converterProperties.setOutlineHandler(outlineHandler);

第一个文档包含 "HTML Ipsum Presents" 书签,第二个文档包含 "Plastic_parts_Basic" 和 "Amo"(带有 children)。

注意大纲处理程序的使用。合并后,书签好像混在一起了。考虑到每个文档的 OutlineHandler 按照相同的模式创建目的地是有道理的:

OutlineHandler addOutline(ITagWorker tagWorker, IElementNode element, ProcessorContext context) {
    String tagName = element.name();
    if (null != tagWorker && hasTagPriorityMapping(tagName) && context.getPdfDocument() != null) {
        int level = (int) getTagPriorityMapping(tagName);
        if (null == currentOutline) {
            currentOutline = context.getPdfDocument().getOutlines(false);
        }
        PdfOutline parent = currentOutline;
        while (!levelsInProcess.isEmpty() && level <= levelsInProcess.getFirst()) {
            parent = parent.getParent();
            levelsInProcess.pop();
        }
        String content = ((JsoupElementNode) element).text();
        if (content.isEmpty()) {
            content = getUniqueID(tagName);
        }
        PdfOutline outline = parent.addOutline(content);
        String destination = DESTINATION_PREFIX + getUniqueID(DESTINATION_PREFIX);
        outline.addDestination(PdfDestination.makeDestination(new PdfString(destination)));

        destinationsInProcess.push(destination);

        levelsInProcess.push(level);
        currentOutline = outline;
    }
    return this;
}

点击书签中的 "Header level 2" 将指向最后合并文档中的第二个 header ("Amo"):

我试图扩展 class OutlineHandler,但我需要更改的方法 (getUniqueID) 是私有的,因此在 superclass 中是不可见的。

有没有办法在 html 创建的多个文档中获得唯一的目的地?

源文件(java 和 html)和生成的 PDF(查看 RFQMerge.pdf)位于此处: the source code, files and result

已接受的答案对我不起作用,我一直在这段代码的第二行收到 NullPointerException:

PdfDictionary names = targetPdf.getCatalog().getPdfObject().getAsDictionary(PdfName.Names); names.put(PdfName.Dests, replaceDict);

这是源代码和 input/source 代码文件:https://www.dropbox.com/s/kg7vsb0j3hbkfca/WhosebugClarification.zip?dl=0

您的问题如下:iText 生成具有相同大纲名称的 pdf,并且在合并期间不解析它们(iText 记录警告并将旧目标替换为新目标)。

有两种方法可以处理所描述的情况:

1) 创建具有唯一大纲名称的 pdf。 不幸的是,现在 OutlineHandler 的实现过于私密,无法正确覆盖它。但是,您可以根据需要构建自定义版本的 pdfHTML。 repo 位于 https://github.com/itext/i7j-pdfhtml 并且您对 OutlineHandler 的 reset 方法感兴趣:

 /**
 * Resets the current state so that this {@link OutlineHandler} is ready to process new document
 */
public void reset() {
    currentOutline = null;
    destinationsInProcess.clear();
    levelsInProcess.clear();
    uniqueIDs.clear();
}

只需注释它的最后一行并构建 jar。

2) 如果您知道文档的目的地会引起一些麻烦,请重命名它们。 即使 PdfMerger 只是用新目的地替换旧目的地,它也会记录有关它的警告。您可以获得已被覆盖的目标名称,并在合并前手动重命名它们。

要遵循这种方式,应该: a) 更新目的地名称:

    PdfNameTree destsTree = updateDestNamesDocument.getCatalog().getNameTree(PdfName.Dests);
    PdfNameTree newNameTree = new PdfNameTree(updateDestNamesDocument.getCatalog(), PdfName.Dests);
    for (Map.Entry<String, PdfObject> entry : destsTree.getNames().entrySet()) {
        newNameTree.addEntry(prefix + entry.getKey(), entry.getValue());
    }
    PdfDictionary replaceDict = newNameTree.buildTree();
    replaceDict.makeIndirect(updateDestNamesDocument);

    PdfDictionary names = updateDestNamesDocument.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
    names.put(PdfName.Dests, replaceDict);

b) 更新大纲:

    PdfOutline rootOutline = updateDestNamesDocument.getOutlines(false);
    updateOutlines(rootOutline, prefix);

    private void updateOutlines(PdfOutline parentOutline, String prefix) {
    for (PdfOutline outline : parentOutline.getAllChildren()) {
        updateOutlines(outline, prefix);
    }
    if (parentOutline.getDestination() instanceof PdfStringDestination) {
        parentOutline.addDestination(new PdfStringDestination(prefix + ((PdfString)parentOutline.getDestination().getPdfObject()).getValue()));
    }
}

然后就可以成功合并pdf了。

谢谢 Uladzimir Asipchuk,您的回答对我很有用。

我根据我的要求做了些微改动,我的要求是合并两个或多个可以从 html 或已经创建的 pdf 或两者创建的 pdf。

我在调用 rebuild() 方法时遇到了一些问题

PdfDictionary replaceDict = newNameTree.buildTree();
replaceDict.makeIndirect(updateDestNamesDocument);

com.itextpdf.kernel.PdfException: 没有关联的 PdfWriter 用于制作 indire cts.

所以,我刚刚将其添加为具有自定义名称的新条目,并在大纲上也进行了更新。正在运行

try {
    String prefix = "cus-" + (index) + "-";
    PdfNameTree destsTree = pdf.getCatalog().getNameTree(PdfName.Dests);
    PdfNameTree newNameTree = new PdfNameTree(pdf.getCatalog(), PdfName.Dests);
    for (Map.Entry<String, PdfObject> entry : destsTree.getNames().entrySet()) {
        newNameTree.addEntry(prefix + entry.getKey(), entry.getValue());
    }

    for (Map.Entry<String, PdfObject> entry : newNameTree.getNames().entrySet()) {
        destsTree.addEntry(prefix + entry.getKey(), entry.getValue());
        System.out.println(entry.getKey() +"==>>"+ entry.getValue());
    }


    PdfOutline rootOutline = pdf.getOutlines(false);
    updateOutlines(rootOutline, prefix);
} catch (Exception e) {
    e.printStackTrace();
}

将新添加的目的地映射到轮廓

public static void updateOutlines(PdfOutline parentOutline, String prefix) {
    for (PdfOutline outline : parentOutline.getAllChildren()) {
        updateOutlines(outline, prefix);
    }
    if (parentOutline.getDestination() instanceof PdfStringDestination) {
        parentOutline.addDestination(new PdfStringDestination(prefix + ((PdfString) parentOutline.getDestination().getPdfObject()).getValue()));
    }
}

这对我来说一直正常工作在此更改后进行合并,因此您的所有目的地看起来都是独一无二的,并且在合并时不会被覆盖。