David E. Veliev
David E. Veliev

Reputation: 134

Graphics2D - identical text rendering on all platforms

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.

Upvotes: 3

Views: 2302

Answers (2)

nim
nim

Reputation: 2449

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.

Upvotes: 2

Andrew Thompson
Andrew Thompson

Reputation: 168845

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

Upvotes: 1

Related Questions