【问题标题】:How to heal inconsistent parent tree mappings in a PDF created by pdfBox如何修复由 pdfBox 创建的 PDF 中不一致的父树映射
【发布时间】:2020-04-10 04:01:30
【问题描述】:

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

请查找example file here

当我们使用 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 预检语法错误检查

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

现在我的问题是:

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

PDF 调试器

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

非常感谢您的帮助。

【问题讨论】:

  • 请分享PDF进行分析。
  • @mkl 请查找示例文件here
  • 您的 PDF 中的问题非常提醒您上次在 this answer 中讨论的问题到 fascinating coder 的问题 “Find Tag from Selection” is not working in tagged pdf?:在您的父树中,您没有引用的实际父结构元素MCID,但您引用了一个新的结构树节点,该节点声称将结构层次结构中的实际父节点作为其自己的父节点,并将有问题的 MCID 作为孩子。
  • 相反,您应该简单地引用 MCID 的实际父结构元素。
  • @mkl 感谢您的 cmets。我认为,您正在将我们推向正确的方向。

标签: pdf pdfbox preflight pac accessible


【解决方案1】:

您的 PDF 中的问题非常类似于 this answer 中的上一节“父树条目的另一个问题”中讨论的问题,fascinating coder 的问题 “Find Tag from Selection” is not working in tagged 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 中)全是绿色的:

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

【讨论】:

  • 嗨@mkl,感谢您的出色回答。就在您发布它时,我们提出了一个自己的解决方案,该解决方案生成了一个有效的 ParentTree。所以没有必要再重建它了。但同样,您的回答指向了正确的方向,对我们帮助很大。我们没有自己测试树重建器,但我会将您的答案标记为解决方案。
  • 如果您不介意,我稍后会发布另一个答案,其中包含我们所做的更改以及生成的 pdf。我们通过将 ParentTree 与有效文档的 ParentTree(PAC3 报告文档 btw)进行比较,或多或少地找到了这个解决方案。但是我们仍然不明白,我们在文档中遗漏了哪个部分导致了从有效到无效的 ParentTree 的区别。
  • @rsr03 “如果您不介意,我稍后会发布另一个答案,其中包含我们对生成的 pdf 所做的更改。” - 是的,当然,这样做!您的问题的解决方案必须是对现有代码的修复,而不是通过先前步骤修复损坏的输出的后处理步骤。在可以部署最终修复程序之前,我的答案中的代码只能作为一种解决方法。
【解决方案2】:

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

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

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

与有效 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);
}

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

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

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

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

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

【讨论】:

  • “我们仍然找不到丢失某些东西的点。” - 参见表 322 – 结构树根中的条目 –,ParentTree:对于包含作为内容项的标记内容序列的页面对象或内容流,该值应是对这些标记内容序列的父元素的引用数组。 这里“父元素”表示结构树中的父元素,而不是结构树中不存在的一些单独的父元素。另请阅读14.7.4.4的第一段,父树的目的是一种从MCID获取结构树父的方式。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-20
  • 2020-11-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-16
相关资源
最近更新 更多