Creating smooth edges on a BufferedImage while using Graphics2D in java

Viewed 58

I created the following code example to demonstrate my problem. I want to draw into a BufferedImage by a Graphics2D object, but the edges are not sharp. I tried using different renderinghints, but it didn't help. I also tried a BufferedImageOp as you can see in the code, but I don't understand its meaning in drawImage and don't know its possibilites. Can you help me please?

import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;

public class SmoothGraphics extends JComponent{
    private BufferedImage image;
    private static JFrame frame;
    private int x = 100;
    private int y = 100;
    private int size = 200;

    public static void main(String[] args){
        frame = new JFrame();

        SmoothGraphics component = new SmoothGraphics();
        frame.add(component);

        frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        frame.setVisible(true);
        frame.pack();
    }

    public SmoothGraphics(){
        super();
        setPreferredSize(new Dimension(400,400));
        setBounds(100,100,400,400);
    }

    @Override 
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;

        int width = getWidth();
        int height = getHeight();

        GraphicsConfiguration gc = frame.getGraphicsConfiguration();
        image = gc.createCompatibleImage(width, height);

        Graphics2D imageGraphics = (Graphics2D) image.createGraphics();
        imageGraphics.setColor(Color.RED);
        /*imageGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                                         RenderingHints.VALUE_ANTIALIAS_ON);*/
        drawExample(imageGraphics);
        imageGraphics.dispose();  

        /*the following kernel helps with the smoothness of the rectangle, but not of the circle
        int kernelWidth = 3;
        int kernelHeight = 3;
        float[] data = new float[kernelWidth*kernelHeight];
        data[4]=1;
        Kernel kernel = new Kernel(kernelWidth, kernelHeight, data);
        BufferedImageOp op = new ConvolveOp(kernel);
        */
        
        //draw image
        BufferedImageOp op = null;
        g2d.drawImage(image,op,0,0);

        //I don't know if this line is necessary
        image.flush();
    }

    private void drawExample(Graphics2D g2d){
        //Square as path
        Path2D.Double path = new Path2D.Double();
        path.moveTo(x,y);
        path.lineTo(x+size,y);
        path.lineTo(x+size,y+size);
        path.lineTo(x,y+size);
        path.closePath();
        g2d.draw(path);

        //Circle
        g2d.fillOval(x+size/4,y+size/4,size/2,size/2);
    }
}

The output of my code. Notice that the edges are not very clean/sharp! The output of my code. Notice that the edges are not very clean/sharp!

My result with antialiasing and kernel usage:

My result with antialiasing and kernel usage

My wished result:

My wished result

1 Answers

Okay... I believe the real problem you are facing is initial scaling of the component graphics (g), which makes your image stretched during drawImage(...). I don't get the same problem here.

To get proper smooth (or "sharp" as you call it) rendering, you do want to enable antialiasing. But you also need to paint without pixel scaling (it's possible with image too, by creating a larger image, however, I don't see why you need the extra image in this case).

Here's your sample program rewritten:

import javax.swing.*;

import java.awt.*;

public class SmoothGraphics extends JComponent {
    private final int x = 100;
    private final int y = 100;
    private final int size = 200;

    public static void main(String[] args) {
        JFrame frame = new JFrame("Smooth");

        SmoothGraphics component = new SmoothGraphics();
        frame.getContentPane().add(component, BorderLayout.CENTER); // Add to content pane

        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.pack(); // Pack *before* setVisible(true)!
        frame.setLocationRelativeTo(null); // Center the frame after pack()
        frame.setVisible(true);
    }

    public SmoothGraphics() {
        setOpaque(true); // Minor optimization
        setPreferredSize(new Dimension(400, 400)); // No need to use setBounds, pack() will fix that for you
    }

    @Override
    public void paintComponent(Graphics g) {
        // No need to call super, as we repaint everything
        Graphics2D g2d = (Graphics2D) g;

        // Clear background to black
        g2d.setColor(Color.BLACK);
        g2d.fillRect(0, 0, getWidth(), getHeight());

        // Paint in read, with antialiasing 
        g2d.setColor(Color.RED);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        drawExample(g2d);
    }

    private void drawExample(Graphics2D g2d) {
        // Square (this way avoids the odd skewed lines)
        g2d.drawRect(x, y, size, size);

        // Circle
        g2d.fillOval(x + size / 4, y + size / 4, size / 2, size / 2);
    }
}

Result:

Looking good, only graphics


If you really want/need the BufferedImage, I found a way that works for me (MacOS). It might need some minor tweaks to work perfectly on other platforms.

    @Override
    public void paintComponent(Graphics g) {
        // No need to call super, as we repaint everything
        Graphics2D g2d = (Graphics2D) g;

        GraphicsConfiguration gc = g2d.getDeviceConfiguration();
        AffineTransform transform = gc.getNormalizingTransform();

        BufferedImage image = gc.createCompatibleImage((int) ceil(getWidth() * transform.getScaleX()), (int) ceil(getHeight() * transform.getScaleY()));

        Graphics2D graphics = image.createGraphics();
        try {
            graphics.setTransform(transform);
            graphics.setColor(Color.RED);
            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Need antialiasing on 
            drawExample(graphics);
        }
        finally {
            graphics.dispose();
        }

        try {
            AffineTransform originalTransform = g2d.getTransform();
            originalTransform.concatenate(transform.createInverse());
            g2d.setTransform(originalTransform);
        } 
        catch (NoninvertibleTransformException e) {
            throw new RuntimeException(e); // API oddity, this should have been a RuntimeException
        }


        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); // Need render QUALITY here, to avoid the stretching/tearing
//        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); // Don't need this on MacOS, but may be needed on other platforms

        g2d.drawImage(image, null, 0, 0);
    }

Result:

Still looking good, with BufferedImage

Related