Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scale images as a single surface in Java 2D API

There is a method called scale(double sx, double sy) in Graphics2D in Java. But this method seems like to scale images as separate surfaces rather than a single surface. As a result, scaled images have sharp corners if original images have no extra width and height. The following screenshot demonstrates the problem:

enter image description here

Here is the code:

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public class TestJava {
    static int scale = 10;

    public static class Test extends JPanel {
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g;
            g2.scale(scale, scale);
            g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

            BufferedImage img = null;
            try {
                img = ImageIO.read(new File("Sprite.png"));
            } catch (IOException e) {
                e.printStackTrace();
            }
            g2.drawImage(img, null, 5, 5);
        }
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame("Testing");
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        Test test = new Test();
        test.setBackground(Color.WHITE);
        frame.add(test);
        frame.setSize(300, 350);
        frame.setVisible(true);
    }
}

One possible solution to the problem is original images having extra width and height (in this case "Sprite.png"). But this does not seem to be a good way to eliminate the problem. So I am seeking for a programmatic way in Java to solve this problem rather than using an image editor. What is the way to do so?

like image 292
arnobpl Avatar asked Sep 27 '14 11:09

arnobpl


People also ask

How do you scale an image in Java?

The simplest way to scale an image in Java is to use the AffineTransformOp class. You can load an image into Java as a BufferedImage and then apply the scaling operation to generate a new BufferedImage. You can use Java's ImageIO or a third-party image library such as JDeli to load and save the image.

What is BufferedImage in Java 2D?

The BufferedImage class is a cornerstone of the Java 2D immediate-mode imaging API. It manages the image in memory and provides methods for storing, interpreting, and obtaining pixel data.

How use drawImage method in Java?

drawImage method draws an image at a specific location: boolean Graphics. drawImage(Image img, int x, int y, ImageObserver observer); The x,y location specifies the position for the top-left of the image.

What does Graphics2D do in Java?

This Graphics2D class extends the Graphics class to provide more sophisticated control over geometry, coordinate transformations, color management, and text layout. This is the fundamental class for rendering 2-dimensional shapes, text and images on the Java(tm) platform.


2 Answers

In your example it's not the image you scale, but you set a scaling transformation on the Graphics2D object which will be applied on all operations performed on that graphics context.

If you want to scale an image, you have 2 options. All I write below uses java.awt.Image, but since BufferedImage extends Image, all this applies to BufferedImage as well.

1. Image.getScaledInstance()

You can use the Image.getScaledInstance(int width, int height, int hints) method. The 3rd parameter (the hints) tells what scaling algorithm you want to use which will affect the "quality" of the scaled image. Possible values are:

SCALE_DEFAULT, SCALE_FAST, SCALE_SMOOTH, SCALE_REPLICATE, SCALE_AREA_AVERAGING

Try the SCALE_AREA_AVERAGING and the SCALE_SMOOTH for nicer scaled images.

// Scaled 3 times:
Image img2 = img.getScaledInstance(img.getWidth(null)*3, img.getHeight(null)*3,
    Image.SCALE_AREA_AVERAGING);
// Tip: you should cache the scaled image and not scale it in the paint() method!

// To draw it at x=100, y=200
g2.drawImage(img2, 100, 200, null);

2. Graphics.drawImage()

You can use different Graphics.drawImage() overloads where you can specify the size of the scaled image. You can "control" the image quality with the KEY_INTERPOLATION rendering hint. It has 3 possible values:

VALUE_INTERPOLATION_NEAREST_NEIGHBOR, VALUE_INTERPOLATION_BILINEAR,
VALUE_INTERPOLATION_BICUBIC

The VALUE_INTERPOLATION_BILINEAR uses a bilinear interpolation algorithm of the 4 nearest pixels. The VALUE_INTERPOLATION_BICUBIC uses a cubic interpolation of the 9 nearby pixels.

g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
    RenderingHints.VALUE_INTERPOLATION_BICUBIC);

// To draw image scaled 3 times, x=100, y=200:
g2.drawImage(img, 100, 200, img.getWidth(null)*3, img.getHeight(null)*3, null);

Removing sharp edges

If you want to avoid sharp edges around the image, you should write a loop to go over the pixels at the edge of the image, and set some kind of transparency, e.g. alpha=0.5 (or alpha=128). You might also do this on multiple rows/columns, e.g. 0.8 alpha for the edge, 0.5 alpha for the 2nd line and 0.3 alpha for the 3rd line.

like image 120
icza Avatar answered Sep 28 '22 05:09

icza


An interesting question (+1). I think that it is not trivial to find a good solution for this: The interpolation when scaling up the image always happens inside the image, and I can not imagine a way to make it blur the scaled pixels outside the image.

This leads to fairly simple solution: One could add a 1-pixel-margin around the whole image. In fact, this is the programmatic way of the solution that you proposed yourself. The resuld would look like this:

ScaledImage

(the left one is the original, and the right one has the additional 1-pixel-border)

Here as a MCVE, based on your example

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class ScaledPaint
{
    static int scale = 10;

    public static class Test extends JPanel
    {
        BufferedImage image = createTestImage();
        BufferedImage imageWithMargin = addMargin(image);

        @Override
        public void paintComponent(Graphics g)
        {
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g;
            g2.scale(scale, scale);
            g2.setRenderingHint(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);

            g2.drawImage(image, 5, 5, null);
            g2.drawImage(imageWithMargin, 30, 5, null);
        }
    }

    private static BufferedImage createTestImage()
    {
        BufferedImage image =
            new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.RED);
        g.drawOval(0, 0, 19, 19);
        g.dispose();
        return image;
    }

    private static BufferedImage addMargin(BufferedImage image)
    {
        return addMargin(image, 1, 1, 1, 1);
    }

    private static BufferedImage addMargin(BufferedImage image, 
        int left, int right, int top, int bottom)
    {
        BufferedImage newImage =
            new BufferedImage(
                image.getWidth() + left + right,
                image.getHeight() + top + bottom, 
                BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = newImage.createGraphics();
        g.drawImage(image, left, top, null);
        g.dispose();
        return newImage;
    }

    private static BufferedImage convertToARGB(BufferedImage image)
    {
        BufferedImage newImage =
            new BufferedImage(image.getWidth(), image.getHeight(),
                BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = newImage.createGraphics();
        g.drawImage(image, 0, 0, null);
        g.dispose();
        return newImage;
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {

            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame frame = new JFrame("Testing");
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        Test test = new Test();
        test.setBackground(Color.WHITE);
        frame.add(test);
        frame.setSize(600, 350);
        frame.setVisible(true);
    }
}

But...

... one problem with this approach can already be seen in the screenshot: The image becomes larger. And you'll have to take this into account when painting the image. So if your original sprites all had a nice, predefined, easy-to-handle size like 16x32, they will afterwards have a size of 18x34, which is rather odd for a tile. This may not a problem, depending on how you are handling your tile sizes. But if it is a problem, one could think about possible solutions. One solution might be to ...

  • take the 16x32 input image
  • create a 16x32 output image
  • paint the 16x32 intput image into the region (1,1)-(15,31) of the output image

But considering the fact that in sprites of this size, every single pixel may be important, this may have undesirable effects as well...


An aside: Altough I assume that the code that you posted was only intended as a MCVE, I'd like to point out (for others who might read this question and the code) :

  • You shoud NOT load images in the paintComponent method
  • For efficient painting, any PNG that is loaded (particularly when it contains transparency) should be converted into an image with a known type. This can be done with the convertToARGB method in my code snippet.
like image 31
Marco13 Avatar answered Sep 28 '22 06:09

Marco13