3

Is it possible to render pixel-by-pixel identical result with Graphics2D.drawString on all java platforms with same font? I tried to render text without antialiasing, but results differs on different machines. I use font from project resources, so font is system-independent.

Example of results of same code with same font on two different PCs:

Result of same code with same font on different PCs

I'm used very small font size (9 px) for clarity.

import java.awt.Color;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;

import javax.imageio.ImageIO;

public class Test {
    public static void main(String... args) throws FontFormatException, IOException {
        int x = 0;
        int y = 9;
        int width = 80;
        int height = 10;
        float fontSizeInPixels = 9f;
        String text = "PseudoText";

        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = image.createGraphics();
        graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);

        URL fontUrl = new URL(
                "https://github.com/indvd00m/graphics2d-drawstring-test/blob/master/src/test/resources/fonts/DejaVuSansMono/DejaVuSansMono.ttf?raw=true");
        Font font = Font.createFont(Font.TRUETYPE_FONT, fontUrl.openStream());
        font = font.deriveFont(fontSizeInPixels);

        Color fontColor = Color.BLACK;
        Color backgroundColor = Color.WHITE;

        graphics.setFont(font);
        graphics.setColor(backgroundColor);
        graphics.fillRect(0, 0, width, height);
        graphics.setColor(fontColor);
        graphics.drawString(text, x, y);

        ImageIO.write(image, "png", new File("/tmp/test.png"));
    }
}

I'm created project for test here:

https://github.com/indvd00m/graphics2d-drawstring-test

Failed build of this project:

https://travis-ci.org/indvd00m/graphics2d-drawstring-test/builds/178672466

Tests passed under openjdk6 and openjdk7 on linux but failed under oraclejdk7 and oraclejdk8 on linux and other OS and java versions.

David E. Veliev
  • 134
  • 1
  • 8
  • Are you positive the the `Graphics2D` object used has identical properties? For better help sooner, post a [MCVE] or [Short, Self Contained, Correct Example](http://www.sscce.org/). One approach you might try is scaling the width and height of the outline of the text to the same values. – Andrew Thompson Nov 23 '16 at 22:27
  • I do not use scaling. I'm added project for test, see original post. – David E. Veliev Nov 24 '16 at 19:21
  • *"I'm added project for test"* Not following an external link, and did not suggest you post a project, but an MCVE. If it's an MCVE, post it here, as an [edit] to the question, not at an external link that might go stale. *"I do not use scaling."* Well, if you're not willing to ***try it***.. good luck with it. – Andrew Thompson Nov 24 '16 at 19:35
  • How can I add MCVE if this task require font file (ttf file)? How is scaling can help me get identical results on all java versions? – David E. Veliev Nov 24 '16 at 19:43
  • Yes, the same font will be rendered differently on different Windows PCs, never mind Linux or OS X. If you need pixel perfect text, you'll have to pre-render ithe text into images on your development computer. – Gilbert Le Blanc Nov 24 '16 at 19:47
  • This is sad. But why results differ? – David E. Veliev Nov 24 '16 at 19:55
  • *"How can I add MCVE if this task require font file (ttf file)?"* Hot link to it, like I did in the `LoadFont` / `DisplayFont` code examples [here](http://stackoverflow.com/a/8365030/418556). *"How is scaling can help me get identical results on all java versions?"* I'm not *sure* it can, but I'd try it by scaling the width and height to the same values. It *might* also help to check that the `Graphics` object is using identical rendering hints. – Andrew Thompson Nov 24 '16 at 20:06
  • Ok, I'm added MCVE with external font. @AndrewThompson can you explain more exactly, what do you mean about scaling? – David E. Veliev Nov 24 '16 at 20:31
  • *".. can you explain more exactly"* Use an `AffineTransform`. Can you be more specific about what you *don't* understand? When asking for free help from others, it's a good idea to be more wordy, more specific, than less so. – Andrew Thompson Nov 25 '16 at 01:31

2 Answers2

2

Rendering text is extremely complex (evolving Opentype and Unicode specs, trying to fit small symbols on screens with too few pixels, work-arounding font bugs, etc).

It is so complex it is mostly system-dependent, with one remaining rendering engine per major OS. Those engines are continuously evolving to try to fix problems, support hardware changes and new spec revisions. They do not give the same results from system to system and OS version to OS versions. Sometimes apps like Oracle JDK add one more level of complexity by including their own rendering routines (for legacy compat, Open JDK uses the system rendering directly and gives better results on modern *nix systems).

You can try to erase some of the differences by rendering at very high pixel dimensions. That will remove all the lowdpi grid fitting black magic differences, and render close to the ideal high-quality paper printing. However the text will be very hard to read when scaled down to actual screen pixels.

In the meanwhile, differing interpretations on where and how put text pixels for better reading quality are a fact of life. You'd better not try to code anything that assumes otherwise. The text stack people will win, it's a very Darwinian process, the few text rendering engines that remain are survivors.

nim
  • 2,345
  • 14
  • 13
1

OK.. not sure this constitutes an 'answer' as such (more an experiment) but this is what I mean about scaling the text to fit the space. See comments in code.

enter image description here

import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import javax.imageio.ImageIO;

public class TestFontWidthScaling {

    private static Font font;
    private static float fontSizeInPixels = 36f;
    private static String text = "PseudoText";
    private static int width = 320;
    private static int height = 40;

    private static BufferedImage scaleImageToFit() {
        BufferedImage image = new BufferedImage(
                width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = image.createGraphics();
//        graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
//        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
        // we need line antialiasing here
        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        FontRenderContext frc = graphics.getFontRenderContext();
        // this is important for determining the *current* size of the 
        // text using this font.
        Area area = new Area(font.
                createGlyphVector(frc, text).
                getOutline());
        Rectangle2D bounds = area.getBounds2D();
        double w = bounds.getWidth();
        double h = bounds.getHeight();
        double scaleW = width*.95 / w;
        double scaleH = height*.9 / h;
        AffineTransform scale = AffineTransform.getScaleInstance(scaleW, scaleH);
        // we now have the shape of the text that will fill a fixed percentage
        // of the width and height of the assigned space.
        area = area.createTransformedArea(scale);

        // now to center it
        bounds = area.getBounds2D();
        double moveX = bounds.getCenterX() - width/2;
        double moveY = bounds.getCenterY() - height/2;
        AffineTransform move = AffineTransform.getTranslateInstance(-moveX, -moveY);
        // this should be both scaled to size AND centered in the space
        area = area.createTransformedArea(move);

        Color fontColor = Color.BLACK;
        // changed to make image bounds more obvious on white BG
        Color backgroundColor = Color.CYAN; 

        graphics.setFont(font);
        graphics.setColor(backgroundColor);
        graphics.fillRect(0, 0, width, height);

        graphics.setColor(fontColor);
        graphics.draw(area);
        graphics.fill(area);

        return image;
    }

    public static void main(String... args) throws FontFormatException, IOException {
        int x = 0;
        int y = 36;

        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = image.createGraphics();
        graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);

        URL fontUrl = new URL(
                "https://github.com/indvd00m/graphics2d-drawstring-test/blob/master/src/test/resources/fonts/DejaVuSansMono/DejaVuSansMono.ttf?raw=true");
        font = Font.createFont(Font.TRUETYPE_FONT, fontUrl.openStream());
        font = font.deriveFont(fontSizeInPixels);

        Color fontColor = Color.BLACK;
        Color backgroundColor = Color.WHITE;

        graphics.setFont(font);
        graphics.setColor(backgroundColor);
        graphics.fillRect(0, 0, width, height);
        graphics.setColor(fontColor);
        graphics.drawString(text, x, y);

        String userHome = System.getProperty("user.home");
        File f = new File(userHome);
        f = new File(f, "test.png");
        ImageIO.write(image, "png", f);
        Desktop.getDesktop().open(f);

        BufferedImage scaledImage = scaleImageToFit();
        f = new File(f.getParentFile(), "test-scaled.png");
        ImageIO.write(scaledImage, "png", f);
        Desktop.getDesktop().open(f);
    }
}
Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
  • "_scaling the text to fit the space_" Ok, now I understand what you mean. This can fix only full width of text. One of the reason of differences is a varied space between symbols, scaling can't help to fix this. Enable of antialiasing lead to very large differences in every symbol. – David E. Veliev Nov 25 '16 at 09:22
  • Perhaps belatedly, I'll ask.. why the need to get text *exactly* the same size on all systems anyway? When the text is in a component, it is layouts that ensure the GUI has robust sizing for elements. When writing the text to images, we can query the size of text as done above (ignore all the stuff about transforming it) & produce an image that is exactly the size needed to show the entire text. – Andrew Thompson Nov 25 '16 at 09:37
  • I'm wrote library for render graphical objects with ASCII-symbols. And when I tried render text in this manner, my unit tests failed in continious integration (but works well on my desktop). – David E. Veliev Nov 25 '16 at 11:25
  • *"..wrote library for render graphical objects with ASCII-symbols"* Seems like something well suited to using a fixed-width (mono spaced) font. Unless there's something I'm understanding incorrectly! – Andrew Thompson Nov 25 '16 at 11:27
  • Yes, I use monospace font. I tried render symbols like in this example: https://github.com/indvd00m/graphics2d-drawstring-test#running-tests – David E. Veliev Nov 25 '16 at 11:33