如何使用Java PDFBox 2.0.8库创建可访问的PDF,该库也可以使用PAC 2工具进行验证?

如何使用Java PDFBox 2.0.8库创建可访问的PDF,该库也可以使用PAC 2工具进行验证?,java,pdf,accessibility,pdfbox,section508,Java,Pdf,Accessibility,Pdfbox,Section508,背景 我在GitHub上有一个小项目,我试图创建一个符合section 508的PDF(section508.gov),它在一个复杂的表结构中包含表单元素。推荐用于验证这些PDF的工具位于,并且我的程序的输出PDF确实通过了大多数检查。我还将知道每个字段在运行时的用途,所以向结构元素添加标记不应该是一个问题 问题 PAC2工具似乎在输出PDF中的两个特定项上存在问题。特别是,我的单选按钮的小部件注释没有嵌套在表单结构元素中,我标记的内容也没有标记(文本和表格单元格)。 PAC2验证左上角单元格中

背景

我在GitHub上有一个小项目,我试图创建一个符合section 508的PDF(section508.gov),它在一个复杂的表结构中包含表单元素。推荐用于验证这些PDF的工具位于,并且我的程序的输出PDF确实通过了大多数检查。我还将知道每个字段在运行时的用途,所以向结构元素添加标记不应该是一个问题

问题

PAC2工具似乎在输出PDF中的两个特定项上存在问题。特别是,我的单选按钮的小部件注释没有嵌套在表单结构元素中,我标记的内容也没有标记(文本和表格单元格)。 PAC2验证左上角单元格中的P,但不验证

但是,PAC 2确实将该对象标识为错误(即未标记文本/路径对象)。 此外,会检测到这些元素,但似乎没有API将它们添加到表单结构元素中

我尝试过的

我已经看过这个网站上的几个问题,以及其他关于这个主题的问题,包括这一个,但似乎几乎没有PDF/UA的例子,也几乎没有有用的文档(我发现)。我所发现的最有用的技巧是在一些网站上,这些网站解释了标签PDF的规格,比如

问题

是否可以使用ApachePDFBox创建包含标记内容和单选按钮小部件注释的PAC2可验证PDF?如果可能,是否可以使用更高级别(未弃用)的PDFBox API

旁注:这实际上是我的第一个StackExchange问题(尽管我已经广泛使用了该网站),我希望一切正常!请随意添加任何必要的编辑,并询问我可能需要澄清的任何问题。另外,我在GitHub上有一个示例程序,它在生成我的PDF文档

编辑1:直接链接到

*编辑2:在使用一些较低级别的PDFBox API并使用PDFDebugger查看完全兼容的PDF的原始数据流后,我能够生成一个与之相比的。。。然而,同样的错误出现了,文本对象没有标记,我真的不能决定从这里去哪里。。。任何指导都将不胜感激

编辑3:原始PDF内容比较

编辑4:生成的PDF的内部结构

和兼容的PDF


编辑5:多亏了Tilman Hausherr的建议,我成功地修复了标记路径/文本对象的PAC 2错误!如果我能够解决“注释小部件未嵌套在表单结构元素中”的问题,我将添加一个答案。

在经历了大量的PDFBox示例之后,我能够解决PAC 2报告的所有问题。创建经过验证的PDF(具有复杂的表结构)需要几个步骤,完整的源代码可在github上获得。我将尝试对下面代码的主要部分进行概述。(此处将不解释某些方法调用!)

步骤1(设置元数据)

各种设置信息,如文档标题和语言

//Setup new document
    pdf = new PDDocument();
    acroForm = new PDAcroForm(pdf);
    pdf.getDocumentInformation().setTitle(title);
    //Adjust other document metadata
    PDDocumentCatalog documentCatalog = pdf.getDocumentCatalog();
    documentCatalog.setLanguage("English");
    documentCatalog.setViewerPreferences(new PDViewerPreferences(new COSDictionary()));
    documentCatalog.getViewerPreferences().setDisplayDocTitle(true);
    documentCatalog.setAcroForm(acroForm);
    documentCatalog.setStructureTreeRoot(structureTreeRoot);
    PDMarkInfo markInfo = new PDMarkInfo();
    markInfo.setMarked(true);
    documentCatalog.setMarkInfo(markInfo);
将所有字体直接嵌入到资源中

//Set AcroForm Appearance Characteristics
    PDResources resources = new PDResources();
    defaultFont = PDType0Font.load(pdf,
            new PDTrueTypeFont(PDType1Font.HELVETICA.getCOSObject()).getTrueTypeFont(), true);
    resources.put(COSName.getPDFName("Helv"), defaultFont);
    acroForm.setNeedAppearances(true);
    acroForm.setXFA(null);
    acroForm.setDefaultResources(resources);
    acroForm.setDefaultAppearance(DEFAULT_APPEARANCE);
为PDF/UA规范添加XMP元数据

//Add UA XMP metadata based on specs at https://taggedpdf.com/508-pdf-help-center/pdfua-identifier-missing/
    XMPMetadata xmp = XMPMetadata.createXMPMetadata();
    xmp.createAndAddDublinCoreSchema();
    xmp.getDublinCoreSchema().setTitle(title);
    xmp.getDublinCoreSchema().setDescription(title);
    xmp.createAndAddPDFAExtensionSchemaWithDefaultNS();
    xmp.getPDFExtensionSchema().addNamespace("http://www.aiim.org/pdfa/ns/schema#", "pdfaSchema");
    xmp.getPDFExtensionSchema().addNamespace("http://www.aiim.org/pdfa/ns/property#", "pdfaProperty");
    xmp.getPDFExtensionSchema().addNamespace("http://www.aiim.org/pdfua/ns/id/", "pdfuaid");
    XMPSchema uaSchema = new XMPSchema(XMPMetadata.createXMPMetadata(),
            "pdfaSchema", "pdfaSchema", "pdfaSchema");
    uaSchema.setTextPropertyValue("schema", "PDF/UA Universal Accessibility Schema");
    uaSchema.setTextPropertyValue("namespaceURI", "http://www.aiim.org/pdfua/ns/id/");
    uaSchema.setTextPropertyValue("prefix", "pdfuaid");
    XMPSchema uaProp = new XMPSchema(XMPMetadata.createXMPMetadata(),
            "pdfaProperty", "pdfaProperty", "pdfaProperty");
    uaProp.setTextPropertyValue("name", "part");
    uaProp.setTextPropertyValue("valueType", "Integer");
    uaProp.setTextPropertyValue("category", "internal");
    uaProp.setTextPropertyValue("description", "Indicates, which part of ISO 14289 standard is followed");
    uaSchema.addUnqualifiedSequenceValue("property", uaProp);
    xmp.getPDFExtensionSchema().addBagValue("schemas", uaSchema);
    xmp.getPDFExtensionSchema().setPrefix("pdfuaid");
    xmp.getPDFExtensionSchema().setTextPropertyValue("part", "1");
    XmpSerializer serializer = new XmpSerializer();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    serializer.serialize(xmp, baos, true);
    PDMetadata metadata = new PDMetadata(pdf);
    metadata.importXMPMetadata(baos.toByteArray());
    pdf.getDocumentCatalog().setMetadata(metadata);
步骤2(设置文档标签结构)

您需要将根结构元素和所有必需的结构元素作为子元素添加到根元素中

//Adds a DOCUMENT structure element as the structure tree root.
void addRoot() {
    PDStructureElement root = new PDStructureElement(StandardStructureTypes.DOCUMENT, null);
    root.setAlternateDescription("The document's root structure element.");
    root.setTitle("PDF Document");
    pdf.getDocumentCatalog().getStructureTreeRoot().appendKid(root);
    currentElem = root;
    rootElem = root;
}
每个标记的内容元素(文本和背景图形)都需要有一个MCID和一个关联的标记,以便在父树中引用,这将在步骤3中解释

//Assign an id for the next marked content element.
private void setNextMarkedContentDictionary(String tag) {
    currentMarkedContentDictionary = new COSDictionary();
    currentMarkedContentDictionary.setName("Tag", tag);
    currentMarkedContentDictionary.setInt(COSName.MCID, currentMCID);
    currentMCID++;
}
屏幕阅读器不会检测到工件(背景图形)。文本需要是可检测的,因此在添加文本时这里使用P结构元素

            //Set up the next marked content element with an MCID and create the containing TD structure element.
            PDPageContentStream contents = new PDPageContentStream(
                    pdf, pages.get(pageIndex), PDPageContentStream.AppendMode.APPEND, false);
            currentElem = addContentToParent(null, StandardStructureTypes.TD, pages.get(pageIndex), currentRow);

            //Make the actual cell rectangle and set as artifact to avoid detection.
            setNextMarkedContentDictionary(COSName.ARTIFACT.getName());
            contents.beginMarkedContent(COSName.ARTIFACT, PDPropertyList.create(currentMarkedContentDictionary));

            //Draws the cell itself with the given colors and location.
            drawDataCell(table.getCell(i, j).getCellColor(), table.getCell(i, j).getBorderColor(),
                    x + table.getRows().get(i).getCellPosition(j),
                    y + table.getRowPosition(i),
                    table.getCell(i, j).getWidth(), table.getRows().get(i).getHeight(), contents);
            contents.endMarkedContent();
            currentElem = addContentToParent(COSName.ARTIFACT, StandardStructureTypes.P, pages.get(pageIndex), currentElem);
            contents.close();
            //Draw the cell's text as a P structure element
            contents = new PDPageContentStream(
                    pdf, pages.get(pageIndex), PDPageContentStream.AppendMode.APPEND, false);
            setNextMarkedContentDictionary(COSName.P.getName());
            contents.beginMarkedContent(COSName.P, PDPropertyList.create(currentMarkedContentDictionary));
            //... Code to draw actual text...//
            //End the marked content and append it's P structure element to the containing TD structure element.
            contents.endMarkedContent();
            addContentToParent(COSName.P, null, pages.get(pageIndex), currentElem);
            contents.close();
注释小部件(本例中为表单对象)需要嵌套在表单结构元素中

//Add a radio button widget.
            if (!table.getCell(i, j).getRbVal().isEmpty()) {
                PDStructureElement fieldElem = new PDStructureElement(StandardStructureTypes.FORM, currentElem);
                radioWidgets.add(addRadioButton(
                        x + table.getRows().get(i).getCellPosition(j) -
                                radioWidgets.size() * 10 + table.getCell(i, j).getWidth() / 4,
                        y + table.getRowPosition(i),
                        table.getCell(i, j).getWidth() * 1.5f, 20,
                        radioValues, pageIndex, radioWidgets.size()));
                fieldElem.setPage(pages.get(pageIndex));
                COSArray kArray = new COSArray();
                kArray.add(COSInteger.get(currentMCID));
                fieldElem.getCOSObject().setItem(COSName.K, kArray);
                addWidgetContent(annotationRefs.get(annotationRefs.size() - 1), fieldElem, StandardStructureTypes.FORM, pageIndex);
            }

//Add a text field in the current cell.
            if (!table.getCell(i, j).getTextVal().isEmpty()) {
                PDStructureElement fieldElem = new PDStructureElement(StandardStructureTypes.FORM, currentElem);
                addTextField(x + table.getRows().get(i).getCellPosition(j),
                        y + table.getRowPosition(i),
                        table.getCell(i, j).getWidth(), table.getRows().get(i).getHeight(),
                        table.getCell(i, j).getTextVal(), pageIndex);
                fieldElem.setPage(pages.get(pageIndex));
                COSArray kArray = new COSArray();
                kArray.add(COSInteger.get(currentMCID));
                fieldElem.getCOSObject().setItem(COSName.K, kArray);
                addWidgetContent(annotationRefs.get(annotationRefs.size() - 1), fieldElem, StandardStructureTypes.FORM, pageIndex);
            }
步骤3

将所有内容元素写入内容流并设置标记结构后,需要返回并将父树添加到结构树根中。注意:上述代码中的一些方法调用(addWidgetContent()和addContentToParent())会设置必要的COSDictionary对象

//Adds the parent tree to root struct element to identify tagged content
void addParentTree() {
    COSDictionary dict = new COSDictionary();
    nums.add(numDictionaries);
    for (int i = 1; i < currentStructParent; i++) {
        nums.add(COSInteger.get(i));
        nums.add(annotDicts.get(i - 1));
    }
    dict.setItem(COSName.NUMS, nums);
    PDNumberTreeNode numberTreeNode = new PDNumberTreeNode(dict, dict.getClass());
    pdf.getDocumentCatalog().getStructureTreeRoot().setParentTreeNextKey(currentStructParent);
    pdf.getDocumentCatalog().getStructureTreeRoot().setParentTree(numberTreeNode);
}
确保执行类似于
currentElem.setAlternateDescription(currentCell.getText())的操作,以便JAWS读取文本


注意:每个字段(单选按钮和文本框)都需要一个唯一的名称,以避免设置多个字段值。GitHub已经更新为更复杂的示例PDF,带有表标记和改进的表单字段

我曾经为单选按钮编写过一些代码,但它不是高级别的,并且不包含任何标记。PDFBox的级别非常低。@tilmahausher嘿,谢谢!是的,我在之前的研究中查看了你的答案,它实际上帮助我解决了另一个问题。另外,我注意到您在我链接的问题中提到了BMC、BDC、EMC、MP和DP运营商。在看了他们在这里做了什么之后,我想知道这些对标记的内容有多有用。主要问题是访问由PDPageContentStream生成的内容并对其进行标记…我建议查看源代码下载中的RemoveAllText.java示例,或者也可以查看这个显示如何操作内容流的解决方案。接下来要了解的是结构树是否已相应更改。为此,请将PDFDebugger切换到“显示内部结构”,然后转到结构树。另一件事是,你提到了单选按钮,但在并排比较中,你显示了主文本。哇,我不敢相信我错过了调试器中的选项卡,谢谢!至于并列,我现在不太关心单选按钮,但我
private void addTableCellMarkup(Cell cell, int pageIndex, PDStructureElement currentRow) {
    COSDictionary cellAttr = new COSDictionary();
    cellAttr.setName(COSName.O, "Table");
    if (cell.getCellMarkup().isHeader()) {
        currentElem = addContentToParent(null, StandardStructureTypes.TH, pages.get(pageIndex), currentRow);
        currentElem.getCOSObject().setString(COSName.ID, cell.getCellMarkup().getId());
        if (cell.getCellMarkup().getScope().length() > 0) {
            cellAttr.setName(COSName.getPDFName("Scope"), cell.getCellMarkup().getScope());
        }
        if (cell.getCellMarkup().getColspan() > 1) {
            cellAttr.setInt(COSName.getPDFName("ColSpan"), cell.getCellMarkup().getColspan());
        }
        if (cell.getCellMarkup().getRowSpan() > 1) {
            cellAttr.setInt(COSName.getPDFName("RowSpan"), cell.getCellMarkup().getRowSpan());
        }
    } else {
        currentElem = addContentToParent(null, StandardStructureTypes.TD, pages.get(pageIndex), currentRow);
    }
    if (cell.getCellMarkup().getHeaders().length > 0) {
        COSArray headerA = new COSArray();
        for (String s : cell.getCellMarkup().getHeaders()) {
            headerA.add(new COSString(s));
        }
        cellAttr.setItem(COSName.getPDFName("Headers"), headerA);
    }
    currentElem.getCOSObject().setItem(COSName.A, cellAttr);
}