java - 使用 PDFBox 处理许多 unicode 字符

标签 java fonts pdfbox

我正在编写一个 Java 函数,它将一个字符串作为参数并使用 PDFBox 生成一个 PDF 作为输出。

只要我使用拉丁字符,一切正常。 但是,我事先并不知道输入的是什么,它可能是一些英文以及中文或日文字符。

在非拉丁字符的情况下,这是我得到的错误:

Exception in thread "main" java.lang.IllegalArgumentException: U+3053 ('kohiragana') is not available in this font Helvetica encoding: WinAnsiEncoding
at org.apache.pdfbox.pdmodel.font.PDType1Font.encode(PDType1Font.java:426)
at org.apache.pdfbox.pdmodel.font.PDFont.encode(PDFont.java:324)
at org.apache.pdfbox.pdmodel.PDPageContentStream.showTextInternal(PDPageContentStream.java:509)
at org.apache.pdfbox.pdmodel.PDPageContentStream.showText(PDPageContentStream.java:471)
at com.mylib.pdf.PDFBuilder.generatePdfFromString(PDFBuilder.java:122)
at com.mylib.pdf.PDFBuilder.main(PDFBuilder.java:111)

如果我理解正确的话,我必须为日文使用一种特定的字体,为中文等使用另一种字体,因为我使用的字体 (Helvetiva) 无法处理所有必需的 unicode 字符。

我还可以使用一种字体来处理所有这些 un​​icode 字符,例如 Arial Unicode .然而,这种字体有特定的许可,所以我不能使用它,而且我还没有找到另一种。

我发现了一些想要克服这个问题的项目,比如 Google NOTO project . 然而,这个项目提供了多个字体文件。因此,我必须在运行时根据我的输入选择要加载的正确文件。

所以我面临两种选择,其中一种我不知道如何正确实现:

  1. 继续寻找一种能够处理几乎所有 unicode 字符的字体(我拼命寻找的 chalice 在哪里?!)

  2. 尝试检测使用的语言并根据它选择字体。 尽管我(还)不知道该怎么做,但我不认为它是一个干净的实现,因为输入和字体文件之间的映射将被硬编码,这意味着我将不得不硬编码所有可能的映射。

  3. 还有其他解决方案吗?

  4. 我完全偏离轨道了吗?

在此先感谢您的帮助和指导!

这是我用来生成 PDF 的代码:

public static void main(String args[]) throws IOException {
    String latinText = "This is latin text";
    String japaneseText = "これは日本語です";

    // This works good
    generatePdfFromString(latinText);

    // This generate an error
    generatePdfFromString(japaneseText);
}

private static OutputStream generatePdfFromString(String content) throws IOException {
    PDPage page = new PDPage();

    try (PDDocument doc = new PDDocument();
         PDPageContentStream contentStream = new PDPageContentStream(doc, page)) {
        doc.addPage(page);
        contentStream.setFont(PDType1Font.HELVETICA, 12);

        // Or load a specific font from a file
        // contentStream.setFont(PDType0Font.load(this.doc, new File("/fontPath.ttf")), 12);

        contentStream.beginText();
        contentStream.showText(content);
        contentStream.endText();
        contentStream.close();
        OutputStream os = new ByteArrayOutputStream();
        doc.save(os);
        return os;
    }
}

最佳答案

比等待字体或猜测文本语言更好的解决方案是拥有多种字体并在逐个字形的基础上选择正确的字体。

您已经找到了 Google Noto Fonts这是完成此任务的良好基础字体集合。

不幸的是,Google 仅将 Noto CJK 字体发布为 OpenType 字体 (.otf),而不是 TrueType 字体 (.ttf),这一政策不太可能改变,请参见。 the Noto fonts issue 249和别的。另一方面,PDFBox 不支持 OpenType 字体,也没有积极致力于 OpenType 支持,请参见。 PDFBOX-2482 .

因此,必须以某种方式将 OpenType 字体转换为 TrueType。我只是把djmilch在他的博文FREE FONT NOTO SANS CJK IN TTF分享的文件拿来了.

每个字符的字体选择

因此,您本质上需要一种方法来逐个字符地检查您的文本并将其分解成可以使用相同字体绘制的 block 。

不幸的是,我没有看到更好的方法来询问 PDFBox PDFont 是否知道给定字符的字形,而不是实际尝试对字符进行编码并考虑 IllegalArgumentException一个“否”。

因此,我使用以下辅助类 TextWithFont 和方法 fontify 实现了该功能:

class TextWithFont {
    final String text;
    final PDFont font;

    TextWithFont(String text, PDFont font) {
        this.text = text;
        this.font = font;
    }

    public void show(PDPageContentStream canvas, float fontSize) throws IOException {
        canvas.setFont(font, fontSize);
        canvas.showText(text);
    }
}

( AddTextWithDynamicFonts 内部类)

List<TextWithFont> fontify(List<PDFont> fonts, String text) throws IOException {
    List<TextWithFont> result = new ArrayList<>();
    if (text.length() > 0) {
        PDFont currentFont = null;
        int start = 0;
        for (int i = 0; i < text.length(); ) {
            int codePoint = text.codePointAt(i);
            int codeChars = Character.charCount(codePoint);
            String codePointString = text.substring(i, i + codeChars);
            boolean canEncode = false;
            for (PDFont font : fonts) {
                try {
                    font.encode(codePointString);
                    canEncode = true;
                    if (font != currentFont) {
                        if (currentFont != null) {
                            result.add(new TextWithFont(text.substring(start, i), currentFont));
                        }
                        currentFont = font;
                        start = i;
                    }
                    break;
                } catch (Exception ioe) {
                    // font cannot encode codepoint
                }
            }
            if (!canEncode) {
                throw new IOException("Cannot encode '" + codePointString + "'.");
            }
            i += codeChars;
        }
        result.add(new TextWithFont(text.substring(start, text.length()), currentFont));
    }
    return result;
}

(AddTextWithDynamicFonts 方法)

使用示例

像这样使用上面的方法和类

String latinText = "This is latin text";
String japaneseText = "これは日本語です";
String mixedText = "Tこhれiはs日 本i語sで すlatin text";

generatePdfFromStringImproved(latinText).writeTo(new FileOutputStream("Cccompany-Latin-Improved.pdf"));
generatePdfFromStringImproved(japaneseText).writeTo(new FileOutputStream("Cccompany-Japanese-Improved.pdf"));
generatePdfFromStringImproved(mixedText).writeTo(new FileOutputStream("Cccompany-Mixed-Improved.pdf"));

( AddTextWithDynamicFonts 测试 testAddLikeCccompanyImproved)

ByteArrayOutputStream generatePdfFromStringImproved(String content) throws IOException {
    try (   PDDocument doc = new PDDocument();
            InputStream notoSansRegularResource = AddTextWithDynamicFonts.class.getResourceAsStream("NotoSans-Regular.ttf");
            InputStream notoSansCjkRegularResource = AddTextWithDynamicFonts.class.getResourceAsStream("NotoSansCJKtc-Regular.ttf")   ) {
        PDType0Font notoSansRegular = PDType0Font.load(doc, notoSansRegularResource);
        PDType0Font notoSansCjkRegular = PDType0Font.load(doc, notoSansCjkRegularResource);
        List<PDFont> fonts = Arrays.asList(notoSansRegular, notoSansCjkRegular);

        List<TextWithFont> fontifiedContent = fontify(fonts, content);

        PDPage page = new PDPage();
        doc.addPage(page);
        try (   PDPageContentStream contentStream = new PDPageContentStream(doc, page)) {
            contentStream.beginText();
            for (TextWithFont textWithFont : fontifiedContent) {
                textWithFont.show(contentStream, 12);
            }
            contentStream.endText();
        }
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        doc.save(os);
        return os;
    }
}

( AddTextWithDynamicFonts 辅助方法)

我明白了

  • for latinText = "This is latin text"

    Latin screen shot

  • for japaneseText = "これは日本语desu"

    Japanese screen shot

  • and for mixedText = "Tこhれiはs日本i语sです拉丁文字"

    Mixed screen shot

一些旁白

  • 我将字体检索为 Java 资源,但您可以为它们使用任何类型的 InputStream

  • 上面的字体选择机制可以很容易地与 this answer 中所示的换行机制相结合。及其在 this answer 中的理由扩展

关于java - 使用 PDFBox 处理许多 unicode 字符,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51481600/

相关文章:

fonts - 如何在 Smalltalk Squeak/Pharo 中轻松更改为原生字体

linux - 由于字体的原因,Windows 和 Linux 上的网页布局有所不同

java - 使用java读取pdf文件中的表格或单元格值?

python - 尝试使用Python或ImageMagick提取字体

java - spring resttemplate url编码

java - 更新 Maven 版本中源文件中的变量

java - 在超链接文本中显示特殊字符

java - Apache pdfbox 声称 PDF 文档已加密(但实际上没有!)- 修复了吗?

pdfbox 文件被加密错误

java - Struts2 Ognl 标签中的日期比较