当前位置: 首页 > 知识库问答 >
问题:

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

宿丰
2023-03-14

出身背景

我在GitHub上有一个小项目,我试图在其中创建一个符合508节(section508.gov)的PDF,它在复杂的表结构中包含表单元素。推荐用于验证这些PDF的工具http://www.access-for-all.ch/en/pdf-lab/pdf-accessibility-checker-pac.html,我的程序的输出PDF确实通过了大部分检查。我还将知道每个字段在运行时的含义,因此向结构元素添加标签应该不是问题。

问题所在

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

但是,PAC 2确实将标记的内容标识为错误(即未标记的文本/路径对象)。此外,检测到单选按钮小部件,但似乎没有API将其添加到表单结构元素中。

我所尝试的

我已经看了这个网站上的几个问题和其他关于这个主题的问题,包括这个带有PDFBox的标记PDF,但是似乎几乎没有PDF/UA的例子,也很少有有用的留档(我找到的)。我发现的最有用的提示是在解释标记PDF规格的网站上,比如https://taggedpdf.com/508-pdf-help-center/object-not-tagged/.

问题是

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

旁注:这实际上是我的第一个StackExchange问题(尽管我已经广泛使用了该网站),我希望一切都井然有序!请随时添加任何必要的编辑,并询问我可能需要澄清的任何问题。此外,我在GitHub上有一个示例程序,它在以下位置生成我的PDF文档:https://github.com/chris271/UAPDFBox.

编辑1:直接链接到输出PDF文档

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

编辑3:并排原始PDF内容比较。

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

和兼容的PDF

编辑5:我已经设法修复了标记路径/文本对象的PAC 2错误,这部分要感谢TilmanHausherr的建议!如果我设法解决了“注释小部件未嵌套在表单结构元素中”的问题,我将添加一个答案。

共有1个答案

赫连彬炳
2023-03-14

在阅读了大量PDF规范和许多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()和addContentToP())设置了必要的COSDicpedia对象。

//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);
}

如果所有小部件注释和标记的内容都正确添加到结构树和父树中,那么您应该从PAC 2和PDFDebugger中获得类似的内容。

感谢Tilman Hausherr为我指明了解决这个问题的正确方向!我很可能会按照其他人的建议对这个答案进行一些编辑,以增加清晰度。

编辑1:

如果您想拥有像我生成的那样的表结构,您还需要添加正确的表标记以完全符合508标准。。。需要将“Scope”、“ColSpan”、“RowSpan”或“Headers”属性正确添加到与this或this类似的每个表单元格结构元素中。此标记的主要目的是允许像JAWS这样的屏幕读取软件以可理解的方式读取表内容。这些属性可以按如下类似的方式添加。。。

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);
}

确保执行类似于currentElem的操作。setAlternateDescription(currentCell.getText()) 以供JAWS读取文本。

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

 类似资料:
  • 使用 Dreamweaver 可生成可供有视觉、听觉、运动及其他障碍的人士使用的网站和 Web 产品。 注意:用户界面已经在 Dreamweaver CC 和更高版本中做了简化。因此,您可能在 Dreamweaver CC 和更高版本中找不到本文中描述的一些选项。有关详细信息,请参阅此文章。 关于辅助内容 辅助功能是指使 Web 站点和 Web 产品可供具有视觉、听觉、运动和其他障碍的人士使用。软

  • 问题内容: 使用H2, 如果数据库尚不存在,则创建数据库。 但是,在Postgres中,不会创建不存在的数据库,因此会引发异常,例如“ DB不存在”。有没有一种方法可以配置Postgres按需创建不存在的数据库? 以下配置文件可用于重现该问题: 使用H2可以正常工作: 使用Postgres失败 问题答案: 该工具只能为现有架构创建表,而不能为您创建架构。在运行该工具之前,数据库必须存在。这是因为数

  • 我正在考虑如何将包含大量数据的大型pdf文件转换为可供我的java应用程序使用的数据库。该pdf文件包含数字以及字符数据,例如与显示为列的数字相关的名称。

  • 我有一个CA提供的jks密钥库,用于对JAR签名。但是,我希望通过HTTPS托管一些内部应用程序,因此需要创建一个SSL证书,以便通过HTTPS加密数据。然而,为了避免浏览器中的不可信证书/未知主机警告,我想知道是否可以使用用于签名JAR的jks密钥库也对我的CSR进行签名,以便创建SSL证书。 到目前为止,我已经设法做到了以下几点: > 使用Java keytool生成证书和私钥对以创建JKS文

  • 我用iText创建了一个pdf,我想打开它,但是当我这样做时,Adobe阅读器说我“打开文档时出错。此文件已打开或由其他应用程序使用“。我该如何解决? 这是我的代码(抱歉休斯顿println异常;)):

  • null 身份验证工作流会是什么样子?有任何可用的例子吗?我们可以通过使用MSAL来实现吗? 假设我们已经创建了一个Azure密钥库,并在该库中保存了一些秘密。如何在Windows 10下运行的桌面应用程序中实现以下功能: 当前Windows用户是Azure AD组GroupA的一部分,位于Azure密钥库的同一承租人之下。 当当前用户从同一用户会话启动桌面应用程序时,应用程序可以访问所述密钥库中