Character encoding PDFBOX2.0:克服字典密钥编码

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

我使用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
问号斑点应该是瑞典字母“ä”或“å”。使用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));
        }
    }
}