使用PDFBox处理许多unicode字符

时间:2018-07-23 14:46:11

标签: java fonts pdfbox

我正在编写一个Java函数,该函数使用String作为参数,并通过 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字符。

我还可以使用处理所有这些Unicode字符的字体,例如Arial Unicode。但是,该字体受特定许可使用,因此我无法使用它,也找不到其他字体。

我找到了一些想要解决此问题的项目,例如Google NOTO project。 但是,该项目提供了多个字体文件。因此,我将不得不在运行时根据我的输入选择要加载的正确文件。

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

  1. 请继续搜索可处理几乎所有unicode字符的字体(我急切地希望在哪里找到它?!)

  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;
    }
}

2 个答案:

答案 0 :(得分:5)

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

您已经找到Google Noto Fonts,它是完成此任务的很好的字体基础。

但是,不幸的是,Google仅将Noto CJK字体发布为OpenType字体(.otf),而不发布为TrueType字体(.ttf),该政策不太可能更改,请参见。 the Noto fonts issue 249等。另一方面,PDFBox不支持OpenType字体,也不能在OpenType支持上积极工作,请参阅cf。 PDFBOX-2482

因此,必须将OpenType字体以某种方式转换为TrueType。我只是把djmilch共享的文件放在他的博客文章FREE FONT NOTO SANS CJK IN TTF中。

每个字符的字体选择

因此,您实质上需要一种方法来逐个字符检查文本字符并将其分解为大块,然后可以使用相同的字体进行绘制。

不幸的是,与实际尝试对该字符进行编码并认为PDFont为“否”相比,我没有更好的方法来询问PDFBox 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辅助方法)

我明白了

  • latinText = "This is latin text"

    Latin screen shot

  • japaneseText = "これは日本語です"

    Japanese screen shot

  • mixedText = "Tこhれiはs日 本i語sで すlatin text"

    Mixed screen shot

一些助手

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

  • 上面的字体选择机制可以很容易地与this answer中所示的换行机制及其在this answer中的对齐扩展

  • 结合使用

答案 1 :(得分:0)

下面是将纯文本拆分为TextWithFont对象块的另一种实现。算法会逐个字符地进行编码,并始终尝试使用主要字体进行编码,只有在失败的情况下,才会继续使用后备字体列表中的下一个字体。

具有属性的主类:

public class SplitByFontsProcessor {

  /** Text to be processed */
  private String text;

  /** List of fonts to be used for processing */
  private List<PDFont> fonts;

  /** Main font to be used for processing */
  private PDFont mainFont;

  /** List of fallback fonts to be used for processing. It does not contain the main font. */
  private List<PDFont> fallbackFonts;

........
}

同一类中的方法:

private List<TextWithFont> splitUsingFallbackFonts() throws IOException {

    final List<TextWithFont> fontifiedText = new ArrayList<>();

    final StringBuilder strBuilder = new StringBuilder();
    boolean isHandledByMainFont = false;

    // Iterator over Unicode codepoints in Java string
    final PrimitiveIterator.OfInt iterator = text.codePoints().iterator();
    while (iterator.hasNext()) {
      int codePoint = iterator.nextInt();
      final String stringCodePoint = new String(Character.toChars(codePoint));

      // try to encode Unicode codepoint
      try {
        // Multi-byte encoding with 1 to 4 bytes.
        mainFont.encode(stringCodePoint); // fails here if can not be handled by the font
        strBuilder.append(stringCodePoint); // append if succeeded to encode
        isHandledByMainFont = true;
      } catch(IllegalArgumentException ex) {
        // IllegalArgumentException is thrown if character can not be handled by a given Font
        // Adding successfully handled characters so far
        if (StringUtils.isNotEmpty(strBuilder.toString())) {
          fontifiedText.add(new TextWithFont(strBuilder.toString(), mainFont));
          strBuilder.setLength(0);// clear StringBuilder
        }

        handleByFallbackFonts(fontifiedText, stringCodePoint);
        isHandledByMainFont = false;
      } // end main font try-catch
    }

    // If this is the last successful run that was handled by main font, then add result
    if (isHandledByMainFont) {
      fontifiedText.add(new TextWithFont(strBuilder.toString(), mainFont));
    }

    return mergeAdjacents(fontifiedText);
  }

方法handleByFallbackFonts()

  private void handleByFallbackFonts(List<TextWithFont> fontifiedText, String stringCodePoint)
      throws IOException {

    final StringBuilder strBuilder = new StringBuilder();
    boolean isHandledByFallbackFont = false;
    // Retry with fallback fonts
    final Iterator<PDFont> fallbackFontsIterator = fallbackFonts.iterator();

    while(fallbackFontsIterator.hasNext()) {
      try {
        final PDFont fallbackFont = fallbackFontsIterator.next();
        fallbackFont.encode(stringCodePoint); // fails here if can not be handled by the font
        isHandledByFallbackFont = true;
        strBuilder.append(stringCodePoint);
        fontifiedText.add(new TextWithFont(strBuilder.toString(), fallbackFont));
        break; // if successfully handled - break the loop
      } catch(IllegalArgumentException exception) {
        // do nothing, proceed to the next font
      }
    } // end while 

    // If character was not handled and this is the last font - throw an exception
    if (!isHandledByFallbackFont) {
      final String fontNames = fonts.stream()
          .map(PDFont::getName)
          .collect(Collectors.joining(", "));

      int codePoint = stringCodePoint.codePointAt(0);

      throw new TextProcessingException(
          String.format("Unicode code point [%s] can not be handled by configured fonts: [%s]",
              codePoint, fontNames));
    }
  }

方法splitUsingFallbackFonts()返回一个TextWithFont对象的列表,其中具有相同字体的相邻对象不一定属于同一对象。发生这种情况是因为算法将始终首先尝试通过主字体渲染字符,如果算法失败,它将创建一个具有能够渲染字符的字体的新对象。因此,我们需要调用实用程序方法mergeAdjacents(),它将它们合并在一起。

 private static List<TextWithFont> mergeAdjacents(final List<TextWithFont> fontifiedText) {

    final Deque<TextWithFont> result = new LinkedList<>();

    for (TextWithFont elem : fontifiedText) {
      final TextWithFont resElem = result.peekLast();
      if (resElem == null || !resElem.getFont().equals(elem.getFont())) {
        result.addLast(elem);
      } else {
        result.addLast(merge(result.pollLast(), elem));
      }
    }

    return new ArrayList<>(result);
  }