如何修复由 pdfBox 创建的 PDF 中不一致的父树映射

How to heal inconsistent parent tree mappings in a PDF created by pdfBox

我们正在使用 pdfBox 在 Java 中创建 pdf 文档。由于屏幕阅读器应该可以访问它们,因此我们正在使用标签并设置一个父树并将其添加到文档目录中。

请找一个example file here

当我们使用 PAC3 验证器检查生成的 pdf 时,我们得到 25 个错误,因为结构父树中的条目不一致。

相同的结果,但在 Adob​​e prefight 语法错误检查中有更多详细信息。错误信息是

Inconsistent ParentTree mapping (ParentTree element 0) for structure element 
Traversal Path:->StructTreeRoot->K->K->[1]->K->[3]->K->[4]

Adobe 预检语法错误检查

当我尝试在 pdfBox 调试器中遵循该遍历路径时,我看到 element referencing the ID 22。

现在我的问题是:

  1. StructTreeRoot 和 ParentTree 之间有什么联系?
  2. 我可以在 StructTreeRoot/ParentTree 的哪个位置找到节点 K->K->2->K->4->K->4? See image PDF Debugger
  3. 中引用的 ID 为 22 的项目
  4. 预检错误消息中的父树元素 0 是什么?见图 Adobe preflight syntax error check

PDF 调试器

我认为,使用 pdfBox 构建可访问的 pdf 以及来自常见验证工具的错误消息的记录相当少。或者我在哪里可以找到更多相关信息?

非常感谢您的帮助。

您的 PDF 中的问题让我们想起了 this answer to the question “Find Tag from Selection” is not working in tagged pdf? by fascinating coder 中上一节 "Yet another issue with parent tree entries" 中讨论的问题:

In your parent tree you do not reference the actual parent structure element of the MCID but you reference a new structure tree node which claims to have the actual parent node from the structure hierarchy as its own parent (not actually being one of its kids) and also claims to have the MCID in question as kid.

相反,您应该简单地引用 MCID 的实际 parent 结构元素。

由于您的问题标题询问 如何修复由 pdfBox 创建的 PDF 中不一致的 parent 树映射,这里有一种修复 parent 树的方法通过从结构树中重建 parent 树。

首先按页面递归收集 MCID 及其 parent 结构树元素,例如使用这样的方法:

void collect(PDPage page, PDStructureNode node, Map<PDPage, Map<Integer, PDStructureNode>> parentsByPage) {
    COSDictionary pageDictionary = node.getCOSObject().getCOSDictionary(COSName.PG);
    if (pageDictionary != null) {
        page = new PDPage(pageDictionary);
    }

    for (Object object : node.getKids()) {
        if (object instanceof COSArray) {
            for (COSBase base : (COSArray) object) {
                if (base instanceof COSDictionary) {
                    collect(page, PDStructureNode.create((COSDictionary) base), parentsByPage);
                } else if (base instanceof COSNumber) {
                    setParent(page, node, ((COSNumber)base).intValue(), parentsByPage);
                } else {
                    System.out.printf("?%s\n", base);
                }
            }
        } else if (object instanceof PDStructureNode) {
            collect(page, (PDStructureNode) object, parentsByPage);
        } else if (object instanceof Integer) {
            setParent(page, node, (Integer)object, parentsByPage);
        } else {
            System.out.printf("?%s\n", object);
        }
    }
}

(RebuildParentTreeFromStructure方法)

使用这个辅助方法

void setParent(PDPage page, PDStructureNode node, int mcid, Map<PDPage, Map<Integer, PDStructureNode>> parentsByPage) {
    if (node == null) {
        System.err.printf("Cannot set null as parent of MCID %s.\n", mcid);
    } else if (page == null) {
        System.err.printf("Cannot set parent of MCID %s for null page.\n", mcid);
    } else {
        Map<Integer, PDStructureNode> parents = parentsByPage.get(page);
        if (parents == null) {
            parents = new HashMap<>();
            parentsByPage.put(page, parents);
        }
        if (parents.containsKey(mcid)) {
            System.err.printf("MCID %s already has a parent. New parent rejected.\n", mcid);
        } else {
            parents.put(mcid, node);
        }
    }
}

(RebuildParentTreeFromStructure辅助方法)

然后根据收集到的信息重建:

void rebuildParentTreeFromData(PDStructureTreeRoot root, Map<PDPage, Map<Integer, PDStructureNode>> parentsByPage) {
    int parentTreeMaxkey = -1;
    Map<Integer, COSArray> numbers = new HashMap<>();

    for (Map.Entry<PDPage, Map<Integer, PDStructureNode>> entry : parentsByPage.entrySet()) {
        int parentsId = entry.getKey().getCOSObject().getInt(COSName.STRUCT_PARENTS);
        if (parentsId < 0) {
            System.err.printf("Page without StructsParents. Ignoring %s MCIDs.\n", entry.getValue().size());
        } else {
            if (parentTreeMaxkey < parentsId)
                parentTreeMaxkey = parentsId;
            COSArray array = new COSArray();
            for (Map.Entry<Integer, PDStructureNode> subEntry : entry.getValue().entrySet()) {
                array.growToSize(subEntry.getKey() + 1);
                array.set(subEntry.getKey(), subEntry.getValue());
            }
            numbers.put(parentsId, array);
        }
    }

    PDNumberTreeNode numberTreeNode = new PDNumberTreeNode(PDParentTreeValue.class);
    numberTreeNode.setNumbers(numbers);
    root.setParentTree(numberTreeNode);
    root.setParentTreeNextKey(parentTreeMaxkey + 1);
}

(RebuildParentTreeFromStructure方法)

像这样应用

PDDocument document = PDDocument.load(SOURCE));
rebuildParentTree(document);
document.save(RESULT);

(RebuildParentTreeFromStructure 测试 testTestdatei)

PAC3 和 Adob​​e Preflight(至少是我的旧 Acrobat 9.5)全部变为绿色,结果:

注意:这还不是通用的 parent 树重建器。 它适用于位于以下位置的测试文件仅在页面内容流中处理特定类型的结构树节点和内容。对于通用工具,它也必须学会应对其他类型,并且还必须处理例如嵌入式 XObjects 中标记的内容。

感谢@mkl 的评论,我们一遍又一遍地分析了我们的解决方案。在我们的第一种方法中,我们遵循了来自@GurpusMaximus 的 和他的 GitHub 存储库的示例。还要感谢@GurpusMaximus 提供完整的示例代码!但是显然我们没有找到在PDFormBuilder.addContentToParent(...)方法中为我们的数据创建父树的正确策略。在第 206 行中,为每个 MarkedContent 元素添加了一个新的 COSDictionary。这导致我们创建了一个深度分支的结构树,其中父树中也有一个结构。

在最后一步中,我们将 numDictionaries 添加到 ParentTree,如 的第 3 步中所建议的那样。

这导致在我们的第一个示例文件中看到奇怪的父树。

与有效 PDF 的父树(PAC3 报告 pdf)的比较表明,只有一个平面树结构,它只包含对每个 [=13 的父结构元素或父树元素的引用=] 元素。

我们将addContentToParent改成了下面的形式:

public PDStructureElement addContentToParent(COSName name, String type,
        PDStructureElement parent) {

    PDStructureElement parentElem = parent;
    if (parentElem == null) {
        parentElem = currentElem;
    }

    PDStructureElement structureElement = null;
    if (type != null) {
        structureElement = new PDStructureElement(type, parentElem);
        structureElement.setPage(qrbill.getPage(0));
    }

    if (name != null) {
        if (structureElement != null) {
            if (!COSName.ARTIFACT.equals(name)) {
                structureElement.appendKid(new PDMarkedContent(name,
                        currentMarkedContentDictionary));
            } else {
                structureElement.appendKid(new PDArtifactMarkedContent(
                        currentMarkedContentDictionary));
            }
            numDictionaries.add(structureElement.getCOSObject());
        } else {
            if (!COSName.ARTIFACT.equals(name)) {
                parentElem.appendKid(new PDMarkedContent(name,
                        currentMarkedContentDictionary));
            } else {
                parentElem.appendKid(new PDArtifactMarkedContent(
                        currentMarkedContentDictionary));
            }
            numDictionaries.add(parentElem.getCOSObject());
        }
        currentStructParent++;
    }

    if (structureElement != null) {
        parentElem.appendKid(structureElement);
        if (name == null && !type.matches("H[1-9]?")) {
            currentElem = structureElement;
        }
    }

    return structureElement;
}

您可以看到,如果我们标记了直接位于结构元素或父元素内的内容,我们只会向 numDictionaries 添加一个元素。正如@mkl 在接受的答案中所建议的那样,这为我们提供了一个平面层次结构,元素之间没有不必要的元素。

在我们这样做之后,我们在 PAC3 检查中不再有错误。预检检查仍然抱怨数组大小错误,我们通过像这样更改 addParentTree 方法来修复:

public void addParentTree() {
    final COSDictionary dict = new COSDictionary();
    nums.add(numDictionaries);
    dict.setItem(COSName.NUMS, nums);

    final PDNumberTreeNode numberTreeNode = new PDNumberTreeNode(dict,
            dict.getClass());
    qrbill.getDocumentCatalog().getStructureTreeRoot()
            .setParentTreeNextKey(currentStructParent);
    qrbill.getDocumentCatalog().getStructureTreeRoot()
            .setParentTree(numberTreeNode);
    qrbill.getDocumentCatalog().getStructureTreeRoot().appendKid(rootElem);
}

现在,我们的示例文件更改为 this.

我们一遍又一遍地阅读 pdf reference 中的第 14.7.4.4 章,但我们仍然找不到遗漏的地方。

The parent tree is a number tree (see 7.9.7, “Number Trees”), accessed from the ParentTree entry in a document’s structure tree root (Table 322). The tree shall contain an entry for each object that is a content item of at least one structure element and for each content stream containing at least one marked-content sequence that is a content item. The key for each entry shall be an integer given as the value of the StructParent or StructParents entry in the object (see Table 326).

也许这只是我的英语不好,但我不明白为什么深度结构化的父树不好。

再次感谢您的帮助@mkl 和示例实现@GurpusMaximus!!