1

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

请在此处查找示例文件

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

在此处输入图像描述

Adobe prefight 语法错误检查中的结果相同但更多详细信息。错误信息是

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

Adobe 预检语法错误检查 Adobe 预检语法错误检查

当我尝试在 pdfBox 调试器中遵循该遍历路径时,我看到一个引用 ID 22 的元素

现在我的问题是:

  1. StructTreeRoot 和 ParentTree 之间有什么联系?
  2. 在 StructTreeRoot/ParentTree 中哪里可以找到节点 K->K-> 2 ->K-> 4 ->K-> 4中引用的 ID 为 22 的项目?查看图片PDF 调试器
  3. Preflight 错误消息中的父树元素 0 是什么?参见图像Adob​​e 预检语法错误检查

PDF 调试器 PDF 调试器

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

非常感谢你的帮助。

4

2 回答 2

1

您的 PDF 中的问题非常类似于上一节“父树条目的另一个问题”中讨论的问题,问题“从选择中查找标签”在标记的 pdf 中不起作用?通过迷人的编码器

在您的父树中,您没有引用 MCID 的实际父结构元素,但您引用了一个新的结构树节点,该节点声称将结构层次结构中的实际父节点作为其自己的父节点(实际上不是其子节点之一)和还声称小时候有问题的 MCID。

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

正如您的问题标题询问如何在由 pdfBox 创建的 PDF 中修复不一致的父树映射时,这里是一种通过从结构树中重建父树来修复父树的方法。

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

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 中)全部变为绿色,结果如下:

PAC3 截图

Adobe Preflight 屏幕截图

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

于 2019-12-18T17:20:05.253 回答
1

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

addContentToParent 方法

在最后一步中,我们按照本文第 3 步中的建议添加numDictionaries了。ParentTree

addParentTree 方法

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

与有效 PDF 的父树(PAC3 报告 pdf)的比较表明,只有一个扁平树结构,它只保存对每个MarkedContent元素的父结构元素或父树元素的引用。

我们改成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);
}

现在,我们的示例文件更改为类似这样的内容。

我们一遍又一遍地阅读 pdf参考中的第 14.7.4.4 章,但我们仍然找不到遗漏某些内容的地方。

父树是数字树(见 7.9.7,“数字树”),从文档结构树根(表 322)中的 ParentTree 条目访问。对于作为至少一个结构元素的内容项的每个对象和每个包含至少一个作为内容项的标记内容序列的内容流,该树都应包含一个条目。每个条目的键应该是一个整数,作为对象中的 StructParent 或 StructParents 条目的值(见表 326)。

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

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

于 2019-12-19T12:56:45.757 回答