如何使用 Java PDFBox 2.0.8 库创建可访问的 PDF,该库也可使用 PAC 2 工具进行验证?
How can I create an accessible PDF with Java PDFBox 2.0.8 library that is also verifiable with PAC 2 tool?
背景
我在 GitHub 上有一个小项目,我正在尝试创建一个符合第 508 节的 (section508.gov) PDF,它在复杂的 table 结构中包含表单元素。推荐用于验证这些 PDF 的工具位于 http://www.access-for-all.ch/en/pdf-lab/pdf-accessibility-checker-pac.html,我程序的输出 PDF 确实通过了大部分检查。我还将知道每个字段在运行时的含义,因此向结构元素添加标签应该不是问题。
问题
PAC 2 工具似乎对输出 PDF 中的两个特定项目有问题。特别是,我的单选按钮的小部件注释没有嵌套在表单结构元素内,我标记的内容没有标记(文本和 Table 单元格)。
PAC 2 验证 P structure element that is within top-left cell but not the marked content…
但是,PAC 2 确实将 marked content 识别为错误(即 Text/Path 对象未标记)。
此外,检测到 radio button widgets,但似乎没有将它们添加到表单结构元素的 API。
我试过的
我已经查看了该网站上的几个问题以及其他与该主题相关的问题,包括这个 Tagged PDF with PDFBox, but it seems that there are almost no examples for PDF/UA and very little useful documentation (That I have found). The most useful tips that I have found have been at sites that explain specs for tagged PDFs like https://taggedpdf.com/508-pdf-help-center/object-not-tagged/。
问题
是否可以使用 Apache PDFBox 创建包含标记内容和单选按钮小部件注释的 PAC 2 可验证 PDF?如果可能,是否可以使用更高级别(未弃用)的 PDFBox API?
旁注:这实际上是我的第一个 StackExchange 问题(尽管我已经广泛使用该网站),我希望一切顺利!请随意添加任何必要的编辑,并提出任何我可能需要澄清的问题。此外,我在 GitHub 上有一个示例程序,它在 https://github.com/chris271/UAPDFBox.
生成我的 PDF 文档
编辑 1:将 link 指向 Output PDF Document
*EDIT 2:在使用一些较低级别的 PDFBox API 并使用 PDFDebugger 查看完全兼容的 PDF 的原始数据流后,我能够生成 PDF with nearly identical content structure compared to the compliant PDF's content structure...但是,同样的错误出现,文本对象没有被标记,我真的无法决定从这里去哪里...任何指导将不胜感激!
编辑 3:Side-by-side 原始 PDF 内容比较。
编辑4:生成PDF的内部结构
和兼容的 PDF
编辑 5: 我已经成功修复了标记 path/text 对象的 PAC 2 错误,这在一定程度上要感谢 Tilman Hausherr 的建议!如果我设法解决有关 'annotation widgets not being nested inside form structure elements'.
的问题,我会添加一个答案
在 github 上经历了大量 PDF Spec and many PDFBox examples I was able to fix all issues reported by PAC 2. There were several steps involved to create the verified PDF (with a complex table structure) and the full source code is available here 之后。我将尝试概述下面代码的主要部分。 (部分方法调用这里不再赘述!)
步骤 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(设置文档标签结构)
您需要将根结构元素和所有必需的结构元素作为 children 添加到根元素。
//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 和一个关联的标签,以便在 parent 树中引用,这将在第 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++;
}
屏幕不会检测到伪像(背景图形)reader。文本需要被检测table 所以这里在添加文本的时候使用了一个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();
注释小部件(在本例中为表单 objects)需要嵌套在表单结构元素中。
//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
将所有内容元素写入内容流并设置标签结构后,有必要返回并将parent树添加到结构树根。注意:上面代码中的一些方法调用(addWidgetContent() 和 addContentToParent())设置了必要的 COSDictionary objects.
//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);
}
如果所有小部件注释和标记内容都正确添加到结构树和 parent 树中,那么您应该从 PAC 2 和 PDFDebugger 中得到类似的东西。
感谢 Tilman Hausherr 为我指明了解决此问题的正确方向!我很可能会按照其他人的建议对该答案进行一些编辑,以提高清晰度。
编辑 1:
如果你想要一个像我生成的那样的 table 结构,你还需要添加正确的 table 标记以完全符合 508 标准... 'Scope'、'ColSpan'、'RowSpan' 或 'Headers' 属性需要正确添加到每个 table 类似于 this or this 的单元格结构元素。此标记的主要目的是允许像 JAWS 这样的屏幕阅读软件以可理解的方式阅读 table 内容。这些属性可以用类似下面的方式添加...
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,带有 table 标记和改进的表单字段!
背景
我在 GitHub 上有一个小项目,我正在尝试创建一个符合第 508 节的 (section508.gov) PDF,它在复杂的 table 结构中包含表单元素。推荐用于验证这些 PDF 的工具位于 http://www.access-for-all.ch/en/pdf-lab/pdf-accessibility-checker-pac.html,我程序的输出 PDF 确实通过了大部分检查。我还将知道每个字段在运行时的含义,因此向结构元素添加标签应该不是问题。
问题
PAC 2 工具似乎对输出 PDF 中的两个特定项目有问题。特别是,我的单选按钮的小部件注释没有嵌套在表单结构元素内,我标记的内容没有标记(文本和 Table 单元格)。 PAC 2 验证 P structure element that is within top-left cell but not the marked content…
但是,PAC 2 确实将 marked content 识别为错误(即 Text/Path 对象未标记)。 此外,检测到 radio button widgets,但似乎没有将它们添加到表单结构元素的 API。
我试过的
我已经查看了该网站上的几个问题以及其他与该主题相关的问题,包括这个 Tagged PDF with PDFBox, but it seems that there are almost no examples for PDF/UA and very little useful documentation (That I have found). The most useful tips that I have found have been at sites that explain specs for tagged PDFs like https://taggedpdf.com/508-pdf-help-center/object-not-tagged/。
问题
是否可以使用 Apache PDFBox 创建包含标记内容和单选按钮小部件注释的 PAC 2 可验证 PDF?如果可能,是否可以使用更高级别(未弃用)的 PDFBox API?
旁注:这实际上是我的第一个 StackExchange 问题(尽管我已经广泛使用该网站),我希望一切顺利!请随意添加任何必要的编辑,并提出任何我可能需要澄清的问题。此外,我在 GitHub 上有一个示例程序,它在 https://github.com/chris271/UAPDFBox.
生成我的 PDF 文档编辑 1:将 link 指向 Output PDF Document
*EDIT 2:在使用一些较低级别的 PDFBox API 并使用 PDFDebugger 查看完全兼容的 PDF 的原始数据流后,我能够生成 PDF with nearly identical content structure compared to the compliant PDF's content structure...但是,同样的错误出现,文本对象没有被标记,我真的无法决定从这里去哪里...任何指导将不胜感激!
编辑 3:Side-by-side 原始 PDF 内容比较。
编辑4:生成PDF的内部结构
和兼容的 PDF
编辑 5: 我已经成功修复了标记 path/text 对象的 PAC 2 错误,这在一定程度上要感谢 Tilman Hausherr 的建议!如果我设法解决有关 'annotation widgets not being nested inside form structure elements'.
的问题,我会添加一个答案在 github 上经历了大量 PDF Spec and many PDFBox examples I was able to fix all issues reported by PAC 2. There were several steps involved to create the verified PDF (with a complex table structure) and the full source code is available here 之后。我将尝试概述下面代码的主要部分。 (部分方法调用这里不再赘述!)
步骤 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(设置文档标签结构)
您需要将根结构元素和所有必需的结构元素作为 children 添加到根元素。
//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 和一个关联的标签,以便在 parent 树中引用,这将在第 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++;
}
屏幕不会检测到伪像(背景图形)reader。文本需要被检测table 所以这里在添加文本的时候使用了一个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();
注释小部件(在本例中为表单 objects)需要嵌套在表单结构元素中。
//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
将所有内容元素写入内容流并设置标签结构后,有必要返回并将parent树添加到结构树根。注意:上面代码中的一些方法调用(addWidgetContent() 和 addContentToParent())设置了必要的 COSDictionary objects.
//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);
}
如果所有小部件注释和标记内容都正确添加到结构树和 parent 树中,那么您应该从 PAC 2 和 PDFDebugger 中得到类似的东西。
感谢 Tilman Hausherr 为我指明了解决此问题的正确方向!我很可能会按照其他人的建议对该答案进行一些编辑,以提高清晰度。
编辑 1:
如果你想要一个像我生成的那样的 table 结构,你还需要添加正确的 table 标记以完全符合 508 标准... 'Scope'、'ColSpan'、'RowSpan' 或 'Headers' 属性需要正确添加到每个 table 类似于 this or this 的单元格结构元素。此标记的主要目的是允许像 JAWS 这样的屏幕阅读软件以可理解的方式阅读 table 内容。这些属性可以用类似下面的方式添加...
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,带有 table 标记和改进的表单字段!