首页 文章

PDFBox 2.0:克服字典键编码

提问于
浏览
1

我使用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

1 回答

  • 3

    使用iText RUPS我可以看到字典键是用ISO-8859-1编码的,而PDFBox假设它们是Unicode,我猜 . 有没有办法使用ISO-8859-1解码密钥?或者任何其他正确检索密钥的方法?

    更改假定的编码

    当从源PDF中读取名称时,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类并替换底部命名的字符集 .

    PDFBox在这里是否正确?

    根据规范,将名称对象视为文本时

    字节序列(在NUMBER SIGN序列扩展之后,如果有的话)应该根据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不是对称的,而是使用纯 US_ASCII 来解释名称的 String 表示(如果从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字符...;)

    历史上

    根据PDF 1.4参考的实施说明,

    在Acrobat 4.0及更早版本中,被视为文本的名称对象通常将以主机平台编码进行解释,这取决于操作系统和本地语言 . 对于亚洲语言,此编码可能类似于Shift-JIS或Big Five . 因此,有必要区分以这种方式编码的名称和编码为UTF-8的名称 . 幸运的是,UTF-8编码非常风格化,通常可以识别它的使用 . 发现不符合UTF-8编码规则的名称可以根据主机进行解释平台编码 .

    因此,手头的样本文件似乎遵循Acrobat 4的惯例,即从上个世纪开始 .

    源代码摘录来自PDFBox 2.0.0,但乍一看似乎没有在2.0.1或开发中继中更改 .

相关问题