4

I am stuck (beyond the limits of fun) at trying to fix text quality with offscreen image double buffering. Screen capture worth a thousand words.

  • The ugly String is drawn to an offscreen image, and then copied to the paintComponent's Graphics argument.
  • The good looking String is written directly to the paintComponent's Graphics argument, bypassing the offscreen image.

Both Graphics instances (onscreen and offscreen) are identically setup in terms of rendering quality, antialiasing, and so on...

Thank you very much in advance for your wisdom.


The very simple code follows:

public class AcceleratedPanel extends JPanel {
    private Dimension     osd; //offscreen dimension
    private BufferedImage osi; //offscreen image
    private Graphics      osg; //offscreen graphic

    public AcceleratedPanel() {
        super();
    }

    @Override
    public final void paintComponent(Graphics g) {
        super.paintComponent(g); 

        // --------------------------------------
        //OffScreen painting

        Graphics2D osg2D = getOffscreenGraphics();
        setupGraphics(osg2D);
        osg2D.drawString("Offscreen painting", 10, 20);

        //Dump offscreen buffer to screen
        g.drawImage(osi, 0, 0, this);

        // --------------------------------------
        // OnScreen painting
        Graphics2D gg = (Graphics2D)g;
        setupGraphics(gg);
        gg.drawString("Direct painting", 10, 35);
    }

    /*
    To make sure same settings are used in different Graphics instances,
    a unique setup procedure is used.
    */
    private void setupGraphics(Graphics2D g) {
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    }

    private Graphics2D getOffscreenGraphics() {
        //Graphics Acceleration
        Dimension currentDimension = getSize();
        if (osi == null || !currentDimension.equals(osd)) {
            osi = (BufferedImage)createImage(currentDimension.width, currentDimension.height);
            osg = osi.createGraphics();
            osd = currentDimension;
        }
        return (Graphics2D) osg;
    }

} //End of mistery
  • 1
    "... beyond the limits of fun". Welcome to *working*. – Mad Physicist Jul 18 '17 at 20:15
  • The "direct" paint method benefits from the fact that the `Graphics` context has been configured for the screen device - it may represent a higher DPI, where as in contrast, your `BufferedImage` is probably operating between 72-92(?) DPI - Unfortunately, last time I checked (at that was a while ago), there's no easy way to get the DPI of the screen device (this might have changed recently), so you might spend a bit of time "guessing" what the best size to make your `BufferedImage` – MadProgrammer Jul 18 '17 at 22:30

2 Answers2

0

You are not drawing your two strings with the same color. The default color for the offscreen Graphics is rgb(0, 0, 0) (that is, pure black), while Swing will set the color of a Graphics object to the look-and-feel’s default color—which, for me on Windows 7, using the default theme, is rgb(51, 51, 51), or dark gray.

Try placing g.setColor(Color.BLACK); in your setupGraphics method, to ensure both strings are drawn with the same color.

VGR
  • 40,506
  • 4
  • 48
  • 63
0

Thanks for the replies.

With mentioning DPI, MadProgrammer has lead me to a working fix which I offer here more as workaround than as a 'clean' solution to be proud of. It solves the issue, anyway.

I noticed that while my screen resolution is 2880x1800 (Retina Display), MouseEvent's getPoint() method reads x=1440, y=900 at the lower right corner of the screen. Then, the JPanel size is half the screen resolution, although it covers the full screen.

This seen, the solution is as follows:

  • first, create an offscreen image matching the screen resolution, not the JPanel.getSize() as suggested in dozens of double buffering articles.

  • then, draw in the offscreen image applying a magnifying transform, bigger than needed, in particular scaling by r = screen dimension / panel dimension ratio.

  • finally, copy a down scaled version of the offscreen image back into the screen, applying a shrinking factor of r (or scaling factor 1/r).


The solution implementation is split into two methods:

  • An ammended version of the initial paintComponent posted earlier,
  • a helper method getDPIFactor() explained afterwards.

The ammended paintComponent method follows:

public final void paintComponent(Graphics g) {
    super.paintComponent(g); 
    double dpiFactor = getDPIFactor();
    // --------------------------------------
    //OffScreen painting

    Graphics2D osg2D = getOffscreenGraphics();
    setupGraphics(osg2D);
    //Paint stuff bigger than needed
    osg2D.setTransform(AffineTransform.getScaleInstance(dpiFactor, dpiFactor));
    //Custom painting
    performPainting(osg2D);

    //Shrink offscreen buffer to screen. 
    ((Graphics2D)g).drawImage(
            osi, 
            AffineTransform.getScaleInstance(1.0/dpiFactor, 1.0/dpiFactor), 
            this);

    // --------------------------------------
    // OnScreen painting
    Graphics2D gg = (Graphics2D)g;
    setupGraphics(gg);
    gg.drawString("Direct painting", 10, 35);
}

To complete the task, the screen resolution must be obtained.

A call to Toolkit.getDefaultToolkit().getScreenResolution() doesn't solve the problem, as it returns the size of a JPanel covering the whole screen. As seen above, this figure doesn't match the actual screen size in physical dots.

The way to get this datum is cleared by Sarge Bosch in this stackoverflow post. I have adapted his code to implement the last part of the puzzle, getDPIFactor().


/*
 * Adapted from Sarge Bosch post in StackOverflow.
 * https://stackoverflow.com/questions/40399643/how-to-find-real-display-density-dpi-from-java-code
 */
private Double getDPIFactor() {
    GraphicsDevice defaultScreenDevice = 
            GraphicsEnvironment.getLocalGraphicsEnvironment()
                    .getDefaultScreenDevice();

    // on OS X, it would be CGraphicsDevice
    if (defaultScreenDevice instanceof CGraphicsDevice) {
        CGraphicsDevice device = (CGraphicsDevice) defaultScreenDevice;

        // this is the missing correction factor.
        // It's equal to 2 on HiDPI a.k.a. Retina displays
        int scaleFactor = device.getScaleFactor();

        // now we can compute the real DPI of the screen
        return scaleFactor * (device.getXResolution() + device.getYResolution()) / 2
                / Toolkit.getDefaultToolkit().getScreenResolution();
    } else
        return 1.0;
}

This code solves the issue for Mac Retina displays, but I am affraid nowhere else, since CGraphicsDevice is an explicit mention to a proprietary implementation of GraphicsDevice.

I do not have other HDPI hardware with which to play around to have a chance to offer a wider solution.