代码之家  ›  专栏  ›  技术社区  ›  GurpusMaximus

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

  •  14
  • GurpusMaximus  · 技术社区  · 6 年前

    出身背景

    我在GitHub上有一个小项目,我试图创建一个符合section508(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 structure element 位于左上角单元格内,但不是 marked content –Ω

    然而,PAC 2确实确定了 marked content 错误(即未标记文本/路径对象)。 此外 radio button widgets 检测到,但似乎没有API将其添加到表单结构元素。

    我所尝试的

    我看了这个网站上的几个问题和其他关于这个主题的问题,包括这一个 Tagged PDF with PDFBox ,但似乎几乎没有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:直接链接到 Output PDF Document

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

    编辑3: Side-by-side 原始PDF内容比较。

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

    generated PDF

    和兼容的PDF

    compliant PDF

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

    1 回复  |  直到 6 年前
        1
  •  15
  •   GurpusMaximus    6 年前

    在经历了大量的 PDF Spec 还有许多PDFBox示例,我能够修复PAC 2报告的所有问题。创建经过验证的PDF(具有复杂的表结构)需要几个步骤,完整的源代码可用 here 在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);
    }
    

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

    Verified PDF

    Debugger

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

    编辑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示例,其中包含表标记和改进的表单字段!