Hairline strokes under fractional display metrics

Kirill Grouchnikov kirill.grouchnikov at gmail.com
Tue May 24 00:14:41 UTC 2022


I'm trying to figure out what is the right way to draw paths such
rectangles or rounded rectangles, such that horizontal and vertical parts
always fall on a full pixel and are displayed at hairline stroke width.

A long time ago there was an option to specify stroke width of 0 for that,
but then it was quickly reverted.

My understanding is that I need to query scale x and scale y of the
device's config's transform, and use that to compute the stroke width.
Here's my code:

import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

abstract class Base extends JComponent {
    protected float strokeWidth;

    public Base() {
        this.setOpaque(false);
        this.setPreferredSize(new Dimension(80, 24));
        this.strokeWidth = 1.0f / (float) getScaleFactor();
    }

    private static double getScaleFactor(GraphicsDevice device) {
        GraphicsConfiguration graphicsConfig = device.getDefaultConfiguration();

        AffineTransform tx = graphicsConfig.getDefaultTransform();
        double scaleX = tx.getScaleX();
        double scaleY = tx.getScaleY();
        return Math.max(scaleX, scaleY);
    }

    private static double getScaleFactor() {
        double result = 1.0;
        GraphicsEnvironment e =
GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice[] devices = e.getScreenDevices();

        // now get the configurations for each device
        for (GraphicsDevice device : devices) {
            result = Math.max(result, getScaleFactor(device));
        }

        return result;
    }
}

class Version1 extends Base {

    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                RenderingHints.VALUE_STROKE_PURE);
        g2d.setStroke(new BasicStroke(this.strokeWidth,
BasicStroke.JOIN_ROUND, BasicStroke.CAP_BUTT));
        g2d.setColor(Color.BLACK);
        g2d.draw(new Rectangle2D.Float(this.strokeWidth,
this.strokeWidth, this.getWidth() - 2 * this.strokeWidth,
                this.getHeight() - 2 * this.strokeWidth));

        for (int i = 1; i < 5; i++) {
            float inset = 2 * i;
            g2d.draw(new Rectangle2D.Float(this.strokeWidth + inset,
this.strokeWidth + inset,
                    this.getWidth() - 2 * this.strokeWidth - 2 * inset,
                    this.getHeight() - 2 * this.strokeWidth - 2 * inset));
        }

        g2d.dispose();
    }
}

public class Hairlines {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Hairlines");

            frame.setLayout(new FlowLayout());
            JComponent version1 = new Version1();

            frame.add(version1);

            frame.setVisible(true);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        });
    }
}


Now, running this on a Windows 10 laptop with recommended / default 250%
scale factor gets the correct value of 2.5 as scale. But the visuals switch
between hairline and "smudged" as I horizontally resize the frame one pixel
at a time and the component shifts horizontally:

[image: image.png]

(and the horizontal lines are also not hairline)

How do I make this work in Java2D across all display scale factors so that
I get consistent hairlines? I see the same behavior on Java 9 and Java 17

Thanks
Kirill



More information about the client-libs-dev mailing list