0

I am using Apache POI to convert pptx slide to image. In the pptx slide, I have Japanese text in GE Inspira font that is not available in my system (comment out ge.registerFont(font) to simulate that). The generated image shows the Japanese text in a default font (see image here). What font is that and where is this default font set?

When I register the font, the Japanese text appears as boxes (see image here). This is because GE Inspira font does not support Japanese characters. Is there a way to force POI to use the default font for Japanese text?

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;

import org.apache.poi.sl.usermodel.TextParagraph.TextAlign;
import org.apache.poi.sl.usermodel.VerticalAlignment;
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.apache.poi.xslf.usermodel.XSLFTextBox;
import org.apache.poi.xslf.usermodel.XSLFTextParagraph;
import org.apache.poi.xslf.usermodel.XSLFTextRun;

public class UnicodePPT {
    public static void main(String[] args) throws Exception {
        // create a sample pptx
        XMLSlideShow ss = new XMLSlideShow();
        Dimension pgsize = ss.getPageSize();

        XSLFSlide slide = ss.createSlide();
        XSLFTextBox tb = slide.createTextBox();
        // tb.setShapeType(XSLFShapeType.HEART);
        int shapeSize = 150;
        tb.setAnchor(new Rectangle((int)(pgsize.getWidth() / 2 - shapeSize / 2), (int)(pgsize.getHeight()
                / 2
                - shapeSize
                / 2), shapeSize, shapeSize));
        tb.setLineWidth(2);
        tb.setLineColor(Color.BLACK);
        XSLFTextParagraph par = tb.addNewTextParagraph();
        tb.setVerticalAlignment(VerticalAlignment.DISTRIBUTED);
        par.setTextAlign(TextAlign.CENTER);
        XSLFTextRun run = par.addNewTextRun();
        run.setText("ゴミ箱");
        run.setFontFamily("GE Inspira");
        run.setFontSize(12.0);

        // set the font
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        InputStream is = new FileInputStream("src/test/resources/GEInspRg.TTF");
        Font font = Font.createFont(Font.TRUETYPE_FONT, is);
        is.close();
        ge.registerFont(font);

        // render it
        double zoom = 2; // magnify it by 2
        AffineTransform at = new AffineTransform();
        at.setToScale(zoom, zoom);

        BufferedImage img = new BufferedImage((int)Math.ceil(pgsize.width * zoom),
                (int)Math.ceil(pgsize.height * zoom), BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = img.createGraphics();
        graphics.setTransform(at);
        graphics.setPaint(Color.white);
        graphics.fill(new Rectangle2D.Float(0, 0, pgsize.width, pgsize.height));
        slide.draw(graphics);

        FileOutputStream fos = new FileOutputStream("src/test/resources/unicodeppt.png");
        javax.imageio.ImageIO.write(img, "png", fos);
        fos.close();
    }
}
nanoticket
  • 63
  • 6
  • I did some work on it in [#55902](https://bz.apache.org/bugzilla/show_bug.cgi?id=55902). Thank you for that example ... I see if I can figure out, where the rendering is going wrong ... – kiwiwings Dec 02 '16 at 14:29
  • The support of fallback fonts is gone :( ... I somehow missed it when moving to Common SL rendering. So to answer your question, you can't use the Inspira font for those characters, as it is not a unicode font (at least the one I've downloaded). Instead you need to [register a fallback font](https://svn.apache.org/viewvc/poi/trunk/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestFontRendering.java?view=markup&pathrev=1567455). I'll post, when the implementation is ready ... – kiwiwings Dec 03 '16 at 12:27
  • Since Lucida do not support CJK, how is the Japanese text displayed when I set the font to GE Inspira as shown [here](https://i.stack.imgur.com/yGCuM.png)? – nanoticket Dec 06 '16 at 06:07
  • Where is this image from? (code?) – kiwiwings Dec 06 '16 at 08:25
  • It was generated when I ran the above code with `ge.registerFont(font)` commented out. I do not have GE Inspira in my Windows fonts folder or in /jre/lib/fonts. – nanoticket Dec 06 '16 at 11:38

1 Answers1

2

As mentioned in the comments, the Inspira GE font is not an unicode font, therefore a fallback is needed. If no specific fallback font is specified, Font.SANS_SERIF will be used. For the logical fonts like SANS_SERIF there is java internal fallback configuration. Unfortunately the automatically added Lucida fonts do not support Chinese (Simplified), Chinese (Traditional), Japanese, and Korean.

So you should provide a unicode font, e.g. Mona, Code2000 and Arial Unicode MS are a good picks - wikipedia has also good collection. The mapping is provided as POI-specific rendering hint in the form of Map<String,String>, where the key is original font family and value the substitution font. You can also specify "*" as a key, to catch-all fonts. Beside the FONT_FALLBACK, there's also a FONT_MAP hint to map all the occurrences, i.e. not only the missing glyphs.

This will be available in POI 3.16-beta2 (ETA February 2017) or you temporarily use the trunk - another option is to provide your own DrawTextParagraph via a customized DrawFactory

To find out, if your font is capable of rendering your chars/glpyhs, you need to open it in the Windows character map tool or some other font tool like FontForge and check if there's a glyph at the unicode block.

@Test
public void unicodeRendering() throws Exception {
    // create a sample pptx
    XMLSlideShow ss = createSamplePPT();
    Dimension pgsize = ss.getPageSize();

    // set the font
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    for (String s : new String[]{"GEInspRg.ttf","mona.ttf"}) {
        Font font = Font.createFont(Font.TRUETYPE_FONT, new File(s));
        ge.registerFont(font);
    }
    Map<String,String> fallbackMap = new HashMap<String,String>();
    fallbackMap.put("GE Inspira", "Mona");

    // render it
    double zoom = 2; // magnify it by 2
    AffineTransform at = new AffineTransform();
    at.setToScale(zoom, zoom);

    BufferedImage img = new BufferedImage((int)Math.ceil(pgsize.width * zoom),
            (int)Math.ceil(pgsize.height * zoom), BufferedImage.TYPE_INT_RGB);
    Graphics2D graphics = img.createGraphics();
    graphics.setRenderingHint(Drawable.FONT_FALLBACK, fallbackMap);
    graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);

    graphics.setTransform(at);
    graphics.setPaint(Color.white);
    graphics.fill(new Rectangle2D.Float(0, 0, pgsize.width, pgsize.height));
    ss.getSlides().get(0).draw(graphics);

    javax.imageio.ImageIO.write(img, "png", new File("unicodeppt.png"));

    ss.close();
}

private XMLSlideShow createSamplePPT() {
    XMLSlideShow ss = new XMLSlideShow();
    Dimension pgsize = ss.getPageSize();

    XSLFSlide slide = ss.createSlide();
    XSLFTextBox tb = slide.createTextBox();
    // tb.setShapeType(XSLFShapeType.HEART);
    int shapeSize = 150;
    tb.setAnchor(new Rectangle((int)(pgsize.getWidth() / 2 - shapeSize / 2),
            (int)(pgsize.getHeight() / 2 - shapeSize / 2), shapeSize, shapeSize));
    tb.setLineWidth(2);
    tb.setLineColor(Color.BLACK);
    XSLFTextParagraph par = tb.addNewTextParagraph();
    tb.setVerticalAlignment(VerticalAlignment.DISTRIBUTED);
    par.setTextAlign(TextAlign.CENTER);
    XSLFTextRun run = par.addNewTextRun();
    run.setText("unicode ->\u30B4\u30DF\u7BB1<-");
    run.setFontFamily("GE Inspira");
    run.setFontSize(12.0);
    return ss;
}
kiwiwings
  • 3,386
  • 1
  • 21
  • 57
  • Does that mean that I have to add the fallback font for every font I register? – nanoticket Dec 05 '16 at 09:52
  • If every font you register is used for unicode characters ... then yes. So maybe I should add an "*" option? – kiwiwings Dec 05 '16 at 10:21
  • You said that GE Inspira is not an unicode font and Font.SANS_SERIF is not unicode-capable? Am I correct to say that unicode fonts or unicode-capable fonts provide glyphs for all the characters that are supported by unicode? Also, how to find out whether a font is a unicode font or not, and whether a character is a unicode character? – nanoticket Dec 05 '16 at 10:54
  • Another question: what is the difference between these 2: `graphics.setRenderingHint(Drawable.FONT_FALLBACK, fallbackMap);` `graphics.setRenderingHint(TextPainter.KEY_FONTFALLBACK, fallbackMap);` – nanoticket Dec 05 '16 at 10:55
  • I've added an explanation on how to identify unicode fonts - there are lots of subsets of unicode blocks, so there are some fonts like [Arial Unicode MS](https://en.wikipedia.org/wiki/Arial_Unicode_MS) which cover a lot of those blocks, but if you need to print anything exotic, you might want to check first ... And for the 2nd question: don't use the obsolete TextPainter anymore. Beside the `FONT_FALLBACK` it's also possible to replace fonts with `FONT_MAP` – kiwiwings Dec 05 '16 at 17:55
  • I've implemented the "*" option in POI and added more information about the jre fonts / fallback. – kiwiwings Dec 06 '16 at 00:27
  • Update: Regarding the default Japanese font set, I believe it is set in .../jre/lib/fontconfig.bfc – nanoticket Dec 12 '16 at 08:56
  • I haven't had time to validate it, as I switched to linux and haven't fully set up my windows image, but my guess is, that if you specify an unknown font family, it defaults to the logical font Dialog, which could fallback to Arial Unicode or similar ... but the above approach, i.e. falling back on glyphs and not the whole font, makes more sense in my point of view ... and btw. I've added the info about the jre font fallback a while ago – kiwiwings Dec 12 '16 at 09:22