Character encoding PDFBOX2.0:克服字典密钥编码
我使用ApachePDFBOx2.0.1从PDF表单中提取文本,提取AcroForm字段的详细信息。我从单选按钮字段中找到了外观词典。我对/N和/D条目(正常和“向下”外观)感兴趣。如下所示(交互式Bean shell): 输出是Character encoding PDFBOX2.0:克服字典密钥编码,character-encoding,pdfbox,Character Encoding,Pdfbox,我使用ApachePDFBOx2.0.1从PDF表单中提取文本,提取AcroForm字段的详细信息。我从单选按钮字段中找到了外观词典。我对/N和/D条目(正常和“向下”外观)感兴趣。如下所示(交互式Bean shell): 输出是 Field Name: Krematorier (6) Off|Skogskrem Off|R�cksta Off|Silverdal Off|Stork�llan Off|St Botvid Nyn�shamn|Off 问号斑点应该是瑞典字母“ä”或“å”。使用i
Field Name: Krematorier (6)
Off|Skogskrem
Off|R�cksta
Off|Silverdal
Off|Stork�llan
Off|St Botvid
Nyn�shamn|Off
问号斑点应该是瑞典字母“ä”或“å”。使用iText RUP,我可以看到字典键是用ISO-8859-1编码的,而PDFBox假设它们是Unicode,我猜
有没有办法用ISO-8859-1解码密钥?或者用其他方法正确检索钥匙
此PDF表格示例可在此处下载:
使用iText RUP,我可以看到字典键是用ISO-8859-1编码的,而PDFBox假设它们是Unicode,我猜
有没有办法用ISO-8859-1解码密钥?或者用其他方法正确检索钥匙
更改假定的编码
从源PDF读取名称时,PDFBox在BaseParser.parseCOSName()
中解释名称中字节的编码(在PDF中只有名称可以用作字典键):
/**
* 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类并替换底部命名的字符集
这里的PDFBox正确吗?
根据规范,当将名称对象视为文本时
字节序列(扩展数字符号序列后,如果有)应根据UTF-8进行解释,UTF-8是Unicode的可变长度字节编码表示法,其中可打印ASCII字符具有与ASCII相同的表示法
(第7.3.5节命名对象)
BaseParser.parseCOSName()
实现了这一点
不过,PDFBox的实现并不完全正确,因为不需要将名称解释为字符串的行为已经是错误的:
名称对象应被视为PDF文件中的原子对象。通常,组成名称的字节永远不会被视为文本,呈现给人类用户或一致性阅读器外部的应用程序。但是,有时需要将名称对象视为文本
因此,PDF库应该尽可能长地将名称作为字节数组处理,并且只有在明确需要时才能找到字符串表示形式,只有这样,上面的建议(假设为UTF-8)才应该发挥作用。规范甚至指出了这可能导致故障的地方:
PDF没有规定选择什么UTF-8序列来表示任何给定的外部指定文本作为名称对象。在某些情况下,多个UTF-8序列可能代表相同的逻辑文本。在PDF中,由不同字节序列定义的名称对象构成不同的名称对象,即使UTF-8序列可能具有相同的外部解释
另一种情况在手边的文档中很明显,如果字节序列不构成有效的UTF-8,它仍然是一个有效的名称。但是,通过上述方法更改这些名称,任何不可解析的字节或子序列都将替换为Unicode替换字符的名称�'. 因此,不同的名称可能会合并为一个名称
另一个问题是,在回写PDF时,PDFBox的作用不是对称的,而是使用纯US\u ASCII
,cf.COSName.writePDF(OutputStream)
,来解释名称的字符串
表示(如果从PDF读取,则检索为UTF-8解释):
public void writePDF(OutputStream output)引发IOException
{
output.write('/');
byte[]bytes=getName().getBytes(Charsets.US\u ASCII);
for(字节b:字节)
{
电流=(b+256)%256;
//要比PDF规范“命名对象”更严格,请参见PDFBOX-2073
如果(当前>='A'&¤t='A'&¤t='0'&¤t
使用iText RUP,我可以看到字典键是用ISO-8859-1编码的,而PDFBox假设它们是Unicode,我猜
是否有任何方法可以使用ISO-8859-1对钥匙进行解码?或者有任何其他方法可以正确检索钥匙
更改假定的编码
从源PDF读取名称时,PDFBox在BaseParser.parseCOSName()
中解释名称中字节的编码(在PDF中只有名称可以用作字典键):
/**
* 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类并替换底部命名的字符集
这里的PDFBox正确吗?
根据规范,当将名称对象视为文本时
字节序列(扩展数字符号序列后,如果有)应根据UTF-8进行解释,UTF-8是Unicode的可变长度字节编码表示法,其中可打印ASCII字符具有与ASCII相同的表示法
(第7.3.5节命名对象)
BaseParser.parseCOSName()
实现了这一点
不过,PDFBox的实现并不完全正确,因为不需要将名称解释为字符串的行为已经是错误的:
名称对象应被视为PDF文件中的原子对象。通常,组成名称的字节不会被视为文本,以呈现给人类用户或一致阅读器外部的应用程序。但是,有时需要将名称对象视为文本
因此,PDF库应该尽可能长时间地将名称作为字节数组处理,并且只有在明确需要时才能找到字符串表示,只有这样,上面的建议(假设为UTF-8)才应该发挥作用
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));
}
}
}