26

I have a JPanel with a painted background image and a layout manager holding other smaller images, all of this inside a JFrame. The background image is pretty big and I want to be able to have it maintain its aspect ratio whether its on a big or small monitor.

Eventually, I want to be able to have my LayoutManager and the smaller images in its cells "glued" to the background picture.

I looked around for resources and it seems that many examples use a BufferedImage but I am not; will this pose a problem? I'll post my code below for painting the image, If I lack any information please let me know.

public class MonitorPanel extends JPanel {
    Image img;
    public MonitorPanel() throws MalformedURLException {
        //add components

        try {
            img = ImageIO.read(new File("src/customer_vlans.jpg"));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
protected void paintComponent(Graphics g)
{
    //paint background image
    super.paintComponent(g);
    //g.drawImage(img, 0, 0, getWidth(), getHeight(), this);
    g.drawImage(img, 0, 0, this);

}

}

EDIT: I should mention that I know the aspect ratio formula: original height / original width x new width = new height However, I do not know how to use that correctly to my advantage.

David Kroukamp
  • 36,155
  • 13
  • 81
  • 138
exit_1
  • 1,240
  • 4
  • 13
  • 32

4 Answers4

49

Well, the quickest and easiest solution is to use Image.getScaledInstance

g.drawImage(img.getScaledInstance(newWidth, -1, Image. SCALE_SMOOTH), x, y, this);

If your wondering about the negative number, the java docs say:

If either width or height is a negative number then a value is substituted to maintain the aspect ratio of the original image dimensions. If both width and height are negative, then the original image dimensions are used.

UPDATE

Just as a side note (my Google was playing up).

getScaledInstance is neither the fastest or highest quality approach, but it is the easiest.

Take a read through The Perils of Image.getScaledInstance for some more ideas

UPDATE

Scaling an image to fit an area is slightly more complicated then simply scaling the aspect ratio. You have to make a choice over if you want the image to "fit" within the area (possibly leaving blank areas around it) or over "fill" the area (so that it's smallest dimension fits the largest dimension of the area).

FitFill

Fit & Fill

Basically, I work with scale factors

This returns the scaling factor for a particular size. I use this to make decisions about which factor I want to use based which algorithm I need

public static double getScaleFactor(int iMasterSize, int iTargetSize) {

    double dScale = 1;
    if (iMasterSize > iTargetSize) {

        dScale = (double) iTargetSize / (double) iMasterSize;

    } else {

        dScale = (double) iTargetSize / (double) iMasterSize;

    }

    return dScale;

}

It's used by these two methods. They simply take two Dimensions. The original and the target.

public static double getScaleFactorToFit(Dimension original, Dimension toFit) {

    double dScale = 1d;

    if (original != null && toFit != null) {

        double dScaleWidth = getScaleFactor(original.width, toFit.width);
        double dScaleHeight = getScaleFactor(original.height, toFit.height);

        dScale = Math.min(dScaleHeight, dScaleWidth);

    }

    return dScale;

}

public static double getScaleFactorToFill(Dimension masterSize, Dimension targetSize) {

    double dScaleWidth = getScaleFactor(masterSize.width, targetSize.width);
    double dScaleHeight = getScaleFactor(masterSize.height, targetSize.height);

    double dScale = Math.max(dScaleHeight, dScaleWidth);

    return dScale;

}

It's relatively simple to pass an image into (either directly or via a support method). So for example, you could call this from within your paint method

double factor getScaledFactorToFit(new Dimension(image.getWidth(), image.getHeight()), getSize());

int scaledWidth = image.getWidth() * scale;
int scaledHeight *= image.getWidth() * scale;

This will automatically take care of the aspect ratio for you ;)

UPDATED with expanded example

public double getScaleFactor(int iMasterSize, int iTargetSize) {

    double dScale = 1;
    if (iMasterSize > iTargetSize) {

        dScale = (double) iTargetSize / (double) iMasterSize;

    } else {

        dScale = (double) iTargetSize / (double) iMasterSize;

    }

    return dScale;

}

public double getScaleFactorToFit(Dimension original, Dimension toFit) {

    double dScale = 1d;

    if (original != null && toFit != null) {

        double dScaleWidth = getScaleFactor(original.width, toFit.width);
        double dScaleHeight = getScaleFactor(original.height, toFit.height);

        dScale = Math.min(dScaleHeight, dScaleWidth);

    }

    return dScale;

}

@Override
protected void paintComponent(Graphics g) {

    super.paintComponent(g);

    double scaleFactor = Math.min(1d, getScaleFactorToFit(new Dimension(image.getWidth(), image.getHeight()), getSize()));

    int scaleWidth = (int) Math.round(image.getWidth() * scaleFactor);
    int scaleHeight = (int) Math.round(image.getHeight() * scaleFactor);

    Image scaled = image.getScaledInstance(scaleWidth, scaleHeight, Image.SCALE_SMOOTH);

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

    int x = (width - scaled.getWidth(this)) / 2;
    int y = (height - scaled.getHeight(this)) / 2;

    g.drawImage(scaled, x, y, this);

}
Martin Höller
  • 2,714
  • 26
  • 44
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • +1 nice. My answer just extends yours and scales the image to the size of the `JPanel` using `getScaledInstance` – David Kroukamp Aug 14 '12 at 20:18
  • Thankyou for your post. How exactly to I get the newWidth? Would I use getWidth() Sorry if that's a trivial question I'm very new to GUI Java – exit_1 Aug 14 '12 at 20:18
  • @Pat `newWidth` is the scaled size, so, yeah, I guess you could use `getWidth` – MadProgrammer Aug 14 '12 at 20:21
  • 1
    Awesome! I used both of your answers to get the desired effect. I wish I could check both of your answers, thanks so much for your time and knowledge. – exit_1 Aug 14 '12 at 20:25
  • So if I use your most recently updated method and choose to "fit", will the image resize in aspect when it gets smaller and cease to resize once the window is expanded beyond the original image size? I sort of just realized that thats the affect I really want haha – exit_1 Aug 14 '12 at 22:22
  • Actually, the more I look at this, the more I like it. Do you have any examples using this method that you could share with me? – exit_1 Aug 14 '12 at 22:45
  • @Pat well, pretty much what I put in will do exactly as you want, the only thing you need to take into account is `getScaleFactor` can return a value > 1. You simply need to bracket that value. Check out the update for a complete example – MadProgrammer Aug 14 '12 at 23:25
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/15365/discussion-between-pat-and-madprogrammer) – exit_1 Aug 14 '12 at 23:47
  • There is a useless condition in both "getScaleFactor" method, just `return (double) iTargetSize / iMasterSize` (no need for the double cast also) – Fagner Brack Oct 09 '13 at 11:39
  • @FagnerBrack Of that, I'm aware – MadProgrammer Oct 09 '13 at 19:37
  • What is the reason then? It is wise to show less code with less verbosity and more meaning than more code with more verbosity and less meaning. – Fagner Brack Oct 10 '13 at 06:15
  • @FagnerBrack While I'm not going to argue the fact, the code was taken from work library code I've not looked at for a number of years, rather then messing with something that I know was working, I left it as was... – MadProgrammer Oct 10 '13 at 06:16
  • It is like discussing the redundancy of `if( true ) {}`, there is 2 reasons for such code: 1. The programmer is stupid. 2. There is some hidden magic behind it that was probably not commented or unit tested. Assuming the second, as long as the code provides what the OP asked, there is no reason for redundancy. I must say I overestimated that code and lost a few minutes testing to be sure, although my logic insisted otherwise. – Fagner Brack Oct 10 '13 at 06:24
  • @FagnerBrack I'm going for option 1. right now :P - If I recall (the code is +5 years old), I think I use to have different functionality based on target and master sizes which I decided to take out or was testing for... – MadProgrammer Oct 10 '13 at 06:27
  • I came up with something pretty simple and interesting for what is worth https://gist.github.com/FagnerMartinsBrack/6913946 – Fagner Brack Oct 10 '13 at 06:30
  • @FagnerBrack It's certainly interesting, the API I have works on two principles, either it will calculate the scale factor required to make the original dimension to fit within the target dimension OR calculate the scale factor required to make the original dimension to fill (over fill) the target dimension...I think your idea could be worked to meet these requirements (with different instances) though ;) – MadProgrammer Oct 10 '13 at 06:34
  • I could kiss you right now, MadProgrammer. Is there a simple way to run this in its own thread to speed the process up? The resizing is perfect, but a little slow. – TEK Nov 06 '13 at 21:41
  • I usually use a `SwingWorker` – MadProgrammer Nov 06 '13 at 21:58
4

Try something like this:

import java.awt.*;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class SG2B2 {

    JFrame frame;

    public static void main(String[] args) {
        SG2B2 gui = new SG2B2();
        gui.createUI();
    }

    public void createUI() {
        frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        MyDrawPanel drawPanel = new MyDrawPanel();
        frame.getContentPane().add(BorderLayout.CENTER, drawPanel);
        frame.setSize(300, 400);
        frame.setVisible(true);
    }

    class MyDrawPanel extends JPanel {

        Image image;
        private final String pic = "Logo.jpg";

        public MyDrawPanel() {
            image = new ImageIcon(pic).getImage();
            image = scaleImage(image);
        }

        @Override
        public void paintComponent(Graphics g) {
            Graphics2D g2 = (Graphics2D) g;
            g2.drawImage(image, 0, 0, this);
        }

        private Image scaleImage(Image rawImage) {
            Image scaledImage = null;
            System.out.println("Scaling");
            try {
                int rawImageWidth = rawImage.getWidth(this);
                int rawImageHeight = rawImage.getHeight(this);
                int paneWidth = (int) getWidth();
                int paneHeight = (int) getHeight();
                System.out.println("Image W = " + rawImageWidth
                        + ", H = " + rawImageHeight
                        + "; Pane W = " + paneWidth
                        + ", H = " + paneHeight);
                // preserve the original ratio  
                float widthRatio = (float) rawImageWidth / (float) paneWidth;
                float heightRatio = (float) rawImageHeight / (float) paneHeight;
                int widthFactor = -1;
                int heightFactor = -1;
                if ((widthRatio > heightRatio) && (widthRatio > 1.0)) {
                    widthFactor = paneWidth;
                } else if ((heightRatio > widthRatio) && (heightRatio > 1.0)) {
                    heightFactor = paneHeight;
                }
                System.out.println("widthRatio = "
                        + String.format("%.3f", widthRatio)
                        + ", heightRatio = "
                        + String.format("%.3f", heightRatio));
                System.out.println("widthFactor = " + widthFactor
                        + ", heightFactor = " + heightFactor);
                if ((widthFactor < 0) && (heightFactor < 0)) {
                    scaledImage = rawImage;
                } else {
                    scaledImage = rawImage.getScaledInstance(widthFactor, heightFactor,
                            Image.SCALE_SMOOTH);
                    // load the new image, 'getScaledInstance' loads asynchronously  
                    MediaTracker tracker = new MediaTracker(this);
                    tracker.addImage(scaledImage, 0);
                    tracker.waitForID(0);
                }
            } catch (InterruptedException ie) {
                System.err.println("load interrupt: " + ie.getMessage());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return (scaledImage);
        }
    }
}

which will ultimately scale the image to the JPanel's size by using getScaledInstance(int width, int height, ImageObserver io)

David Kroukamp
  • 36,155
  • 13
  • 81
  • 138
  • 1
    If I was going to use `MediaTracker` I'd probably off load it to a second `Thread` (obviously this is just an example ;)). The other interesting fact to remember is that every Java `Component` is it's own `ImageObserver`, so you can get away without the `MediaTracker` as the `Component` is already doing this (of sorts). Unless you have a massively huge image you know is going to take time to load, then I'd probably do something of the same in a separate `Thread` ;) IMHO – MadProgrammer Aug 14 '12 at 20:20
3

For anyone interested ammending the PaintComponent method by MadProgrammer as follows allows much quicker display update

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

            double scaleFactor = Math.min(1d, getScaleFactorToFit(new Dimension(image.getWidth(), image.getHeight()), getSize()));

            int scaleWidth = (int) Math.round(image.getWidth() * scaleFactor);
            int scaleHeight = (int) Math.round(image.getHeight() * scaleFactor);

            //Image scaled = image.getScaledInstance(scaleWidth, scaleHeight, Image.SCALE_SMOOTH);

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

            int x = (width - scaleWidth) / 2;
            int y = (height - scaleHeight) / 2;

            g2d.drawImage(image, x, y, scaleWidth, scaleHeight, this);

        }
Joe
  • 1,455
  • 2
  • 19
  • 36
1

I came up with this solution:

public class ImageLabel extends JPanel {

    private Image image = null;

    public void setImage(Image img) {
        image = img;
        repaint();
    }

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

        if (image != null) {

            int imgWidth, imgHeight;
            double contRatio = (double) getWidth() / (double) getHeight();
            double imgRatio =  (double) image.getWidth(this) / (double) image.getHeight(this);

            //width limited
            if(contRatio < imgRatio){
                imgWidth = getWidth();
                imgHeight = (int) (getWidth() / imgRatio);

            //height limited
            }else{
                imgWidth = (int) (getHeight() * imgRatio);
                imgHeight = getHeight();
            }

            //to center
            int x = (int) (((double) getWidth() / 2) - ((double) imgWidth / 2));
            int y = (int) (((double) getHeight()/ 2) - ((double) imgHeight / 2));

            g.drawImage(image, x, y, imgWidth, imgHeight, this);
        }
    }
}
Doctor Parameter
  • 1,202
  • 2
  • 15
  • 21