9

我正在编写一个 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 字符。

我还可以使用处理所有这些 un​​icode 字符的字体,例如Arial Unicode。但是这种字体是在特定的许可证下,所以我不能使用它,我还没有找到另一个。

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

所以我面临 2 个选项,其中一个我不知道如何正确实施:

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

2 回答 2

14

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

您已经找到了Google Noto 字体,它是此任务的一个很好的基本字体集合。

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

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

每个字符的字体选择

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

不幸的是,我没有看到更好的方法来询问 PDFBoxPDFont是否知道给定字符的字形,而不是实际尝试对字符进行编码并考虑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"

    拉丁语屏幕截图

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

    日文截图

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

    混合屏幕截图

一些旁白

于 2018-07-30T14:37:31.687 回答
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);
  }
于 2020-06-04T20:10:45.873 回答