我正在使用 Apache PDFBox 2.0.1 从 PDF 表单中提取文本,提取 AcroForm 字段的详细信息。我从单选按钮字段中挖掘出外观字典。我对 /N 和 /D 条目(正常和“向下”外观)感兴趣。像这样(交互式 Bean shell):
field = form.getField(fieldName);
widgets = field.getWidgets();
print("Field Name: " + field.getPartialName() + " (" + widgets.size() + ")");
for (annot : widgets) {
ap = annot.getAppearance();
keys = ap.getCOSObject().getDictionaryObject("N").keySet();
keyList = new ArrayList(keys.size());
for (cosKey : keys) {keyList.add(cosKey.getName());}
print(String.join("|", keyList));
}
输出是
Field Name: Krematorier (6)
Off|Skogskrem
Off|R�cksta
Off|Silverdal
Off|Stork�llan
Off|St Botvid
Nyn�shamn|Off
问号斑点应该是瑞典语字符“ä”或“å”。使用 iText RUPS 我可以看到字典键是用 ISO-8859-1 编码的,而 PDFBox 假设它们是 Unicode,我猜。
有没有办法使用 ISO-8859-1 解码密钥?或者有其他方法可以正确检索密钥吗?
此 PDF 表格示例可在此处下载:http://www.stockholm.se/PageFiles/85478/KYF%20211%20Best%C3%A4llning%202014.pdf
使用 iText RUPS 我可以看到字典键是用 ISO-8859-1 编码的,而 PDFBox 假设它们是 Unicode,我猜。
有没有办法使用 ISO-8859-1 解码密钥?或者有其他方法可以正确检索密钥吗?
PDFBox 对名称中字节编码的解释(只有名称可以用作 PDF 中的字典键)发生在
BaseParser.parseCOSName()
中:
/**
* This will parse a PDF name from the stream.
*
* @return The parsed PDF name.
* @throws IOException If there is an error reading from the stream.
*/
protected COSName parseCOSName() throws IOException
{
readExpectedChar('/');
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int c = seqSource.read();
while (c != -1)
{
int ch = c;
if (ch == '#')
{
int ch1 = seqSource.read();
int ch2 = seqSource.read();
if (isHexDigit((char)ch1) && isHexDigit((char)ch2))
{
String hex = "" + (char)ch1 + (char)ch2;
try
{
buffer.write(Integer.parseInt(hex, 16));
}
catch (NumberFormatException e)
{
throw new IOException("Error: expected hex digit, actual='" + hex + "'", e);
}
c = seqSource.read();
}
else
{
// check for premature EOF
if (ch2 == -1 || ch1 == -1)
{
LOG.error("Premature EOF in BaseParser#parseCOSName");
c = -1;
break;
}
seqSource.unread(ch2);
c = ch1;
buffer.write(ch);
}
}
else if (isEndOfName(ch))
{
break;
}
else
{
buffer.write(ch);
c = seqSource.read();
}
}
if (c != -1)
{
seqSource.unread(c);
}
String string = new String(buffer.toByteArray(), Charsets.UTF_8);
return COSName.getPDFName(string);
}
如您所见,在读取名称字节并解释 # 转义序列后,PDFBox 无条件地将结果字节解释为 UTF-8 编码。因此,要更改此设置,您必须修补此 PDFBox 类并替换底部命名的字符集。
根据规范,将名称对象视为文本时
字节序列(在扩展数字符号序列之后,如果有的话)应根据UTF-8进行解释,UTF-8是Unicode的可变长度字节编码表示形式,其中可打印的ASCII字符具有与ASCII相同的表示形式。
(第 7.3.5 节名称对象,ISO 32000-1)
BaseParser.parseCOSName()
就实现了这一点。
不过,PDFBox 的实现并不完全正确,因为无需将名称解释为字符串的行为就是错误的:
名称对象应被视为 PDF 文件中的原子对象。通常,组成名称的字节永远不会被视为呈现给人类用户或符合标准的阅读器外部应用程序的文本。然而,有时需要将名称对象视为文本因此,PDF 库应尽可能将名称作为字节数组进行处理,并且仅在明确需要时才查找字符串表示形式,只有这样,上面的建议(假设 UTF-8)才应发挥作用。该规范甚至指出了这可能会导致问题的地方:
PDF 没有规定选择什么 UTF-8 序列来将任何给定的外部指定文本片段表示为名称对象。在某些情况下,多个 UTF-8 序列可能表示相同的逻辑文本。由不同字节序列定义的名称对象在 PDF 中构成不同的名称对象,即使 UTF-8 序列可能具有相同的外部解释。另一种情况在手头的文档中变得很明显,如果字节序列不构成有效的 UTF-8,它仍然是一个有效的名称。但这些名称会通过上述方法进行
更改,任何无法解析的字节或子序列都会被 Unicode 替换字符“�”替换。因此,不同的名称可能会合并为一个名称。
另一个问题是,当写回 PDF 时,PDFBox不 对称地运行,而是使用纯 String
解释名称的
US_ASCII
表示(如果从 PDF 读取,则已将其检索为 UTF-8 解释) ,参见。
COSName.writePDF(OutputStream)
:
public void writePDF(OutputStream output) throws IOException
{
output.write('/');
byte[] bytes = getName().getBytes(Charsets.US_ASCII);
for (byte b : bytes)
{
int current = (b + 256) % 256;
// be more restrictive than the PDF spec, "Name Objects", see PDFBOX-2073
if (current >= 'A' && current <= 'Z' ||
current >= 'a' && current <= 'z' ||
current >= '0' && current <= '9' ||
current == '+' ||
current == '-' ||
current == '_' ||
current == '@' ||
current == '*' ||
current == '$' ||
current == ';' ||
current == '.')
{
output.write(current);
}
else
{
output.write('#');
output.write(String.format("%02X", current).getBytes(Charsets.US_ASCII));
}
}
}
因此,任何有趣的 Unicode 字符都会被替换为 US_ASCII 默认替换字符,我假设为“?”。因此,幸运的是 PDF 名称通常只包含 ASCII 字符...;)
历史上
在 Acrobat 4.0 及更早版本中,被视为文本的名称对象通常会以主机平台编码进行解释,这取决于操作系统和本地语言。对于亚洲语言,此编码可能类似于 Shift-JIS 或大五。因此,有必要区分以这种方式编码的名称和以 UTF-8 编码的名称。幸运的是,UTF-8 编码非常程式化,其使用通常可以被识别。发现不符合 UTF-8 编码规则的名称可以根据主机平台编码进行解释。因此,手头的示例文档似乎遵循 Acrobat 4 的约定,即上个世纪的约定。
源代码摘录自PDFBox 2.0.0,但乍一看似乎在2.0.1或开发主干中没有改变。