Page 1 of 1

Java Image Manipulation Part 3: Math and SwingWorker Rate Topic: -----

#1 Dogstopper  Icon User is offline

  • The Ninjaducky
  • member icon



Reputation: 2870
  • View blog
  • Posts: 11,025
  • Joined: 15-July 08

Posted 19 March 2010 - 11:48 AM

*
POPULAR

Java Image Manipulation Part 3
~ Dogstopper

In my first tutorial, I domonstrated how you can load images from both files and from the web, and in my second tutorial I demonstrated how to scale images so that they grew as you increase the height, but what I neglected to tell you was that often the image could grow to large width-wise and would dissapear off the edge of the screen. In this tutorial, I will go over the basic math needed to completely center an image, so that no part of the image will go offscreen. Also, in this tutorial I will show you how to load images in a separate thread safely even though Swing is not thread-safe.

Centering An Image - A Simple Ratio
When truly scaling and centering an image, you must remember that it must compensate so that the greatest dimension is always inside the boundaries. For this calculation, you need the scaledWidth and scaledHeight. To get those, its really a simple proportion. Let's start with scaledWidth.

Given: Big Image Width - img.getWidth()
Big Image Height - img.getHeight()
The Maximum Possible Height - getHeight()
Goal: To find the maximum possible width of the scaled image

img.getWidth() / img.getHeight() = x / getHeight()

Now, just cross multiply.
(img.getWidth() / img.getHeight()) * getHeight() = x

So, that's how we get the scaled image to fit into a maximum height. Now how about to fit an image into the maximum width (In our previous example, when the image is too wide, but fits vertically). For this, we need to scale the height.

Given: Big Image Width - img.getWidth()
Big Image Height - img.getHeight()
The Maximum Possible Width - getWidth()
Goal: To find the maximum possible height of the scaled image

img.getHeight() / img.getWidth() = x / getWidth()

Now, just cross multiply.
(img.getHeight() / img.getWidth()) * getWidth() = x

If all this math doesn't make much sense, it will after updationg the PicturePanel. If you've been following previous examples, you should know how exactly to set up this PicturePanel. I'm just going to rewrite the paintComponent() method to demonstrate this new technique. If it helps, draw out on a sheet of paper a centered image just so that you can visualize it. Remeber, this code will scale down a large image and scale up a small image.
    public void paintComponent(Graphics g) {
        super.paintComponent(g);             
            
        // Scale it by width
        int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));

        // If the image is not off the screen horizontally...
        if (scaledWidth < getWidth()) {
            // Center the left and right destination x coordinates.
            int leftOffset = getWidth()/2 - scaledWidth/2;
            int rightOffset = getWidth()/2 + scaledWidth/2;
                
            g.drawImage(img, 
                    leftOffset, 0, rightOffset, getHeight(), 
                    0, 0, img.getWidth(), img.getHeight(), 
                    null);
        }

        // Otherwise, the image width is too much, even scaled
        // So we need to center it the other direction
        else {
            int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
                
            int topOffset = getHeight()/2 - scaledHeight/2;
            int bottomOffset = getHeight()/2 + scaledHeight/2;
                
            g.drawImage(img,
                    0, topOffset, getWidth(), bottomOffset, 
                    0, 0, img.getWidth(), img.getHeight(), 
                    null);
        }
    }



If at this point, you are still confused as to what this does, fire up your compiler and stretch the JFrame to the extreme in both directions...You'll find that the image never leaves the frame.

Loading Images In Separate Threads
If you have ever made a GUI application with threads, it was wrong because Swing is not thread safe. However, there must be some way to have a multithreaded GUI application! Well, yes, sort of. Let me introduce you to SwingWorker, an object that allows you to load an object in the background while other aspects of the program continue, unlike our first tutorial where the application would lock up until it was loaded. Here is what the Swing Worker looks like:
    // The BufferedImage specifies that the doInBackground() method returns a BufferedImage
    // And the Void specifies that the done() method is void.  
    private SwingWorker<BufferedImage, Void> worker = new SwingWorker<BufferedImage, Void>() {
        @Override
        public BufferedImage doInBackground() {
            
           // All we do in the background is load like we did in tutorial 1
            BufferedImage image = null;
            try {
                image = ImageIO.read(url);
            } catch (IOException e) {
                System.err.println("A loading error occurred");
            }
            return image;
        }

        // This method is called when the doInBackground() method is done
        @Override
        public void done() {
            try {
                // We are going to set a global img variable to 
                // the loaded image.
                img = get();

            // Catch any errors
            } catch (InterruptedException ignore) {}
            catch (ExecutionException e) {
                System.err.println("A loading error occurred: " + why);
            }
        }
    };



Now, your question may be why use the get() method to set the img variable? Well, that is to absolutely ensure that the image is done loading and the thread is done executing. Otherwise you can get extreme synchronization errors. So, I made a helper class to hold this SwingWorker. Just think of get() as the way to access what was loaded in doInBackground().
public class WebScanner {
    
    // These are the variables that hold the URL and the 
    // image when done loading
    private BufferedImage img;
    private URL url;
    
    // This is a bit more complicated, but I explained it above.
    private SwingWorker<BufferedImage, Void> worker = new SwingWorker<BufferedImage, Void>() {
        @Override
        public BufferedImage doInBackground() {
            BufferedImage image = null;
            try {
                image = ImageIO.read(url);
            } catch (IOException e) {
                System.err.println("A loading error occurred");
            }
            return image;
        }

        @Override
        public void done() {
            try {
                img = get();
            } catch (InterruptedException ignore) {}
            catch (ExecutionException e) {
                String why = null;
                Throwable cause = e.getCause();
                if (cause != null) {
                    why = cause.getMessage();
                } else {
                    why = e.getMessage();
                }
                System.err.println("A loading error occurred: " + why);
            }
        }
    };
    
    // Sets the URL. Again...using an astronomy picture...
    public void setURL() {
        try {
            url = new URL("http://antwrp.gsfc.nasa.gov/apod/image/1003/mb_2010-03-10_SeaGullThor.jpg");
        } catch (MalformedURLException e) {
            System.err.println("Your tutorial writer wrote down the wrong internet address");
        }
    }
    
    // This returns the image when done loading. If there is
    // a null image, then we retry.
    public BufferedImage getImage(String fn) {
        if (img == null) {
            System.err.println("Attempting again");
            // Starts the worker
            worker.execute();
        }
        return img;
    }
    
    // Access to the worker
    public void startWorker() {
        // Starts the worker
        worker.execute();
    }
    
    public boolean isWorkerDone() { 
        return worker.isDone();
    }
    
}



This class helps the SwingWorker do what it needs to do, whether it be starting it or checking whether it's done. Also, we have a method that grabs the global img variable. Now, I created a GUI that ha a splitPane to allow the image to be loading and to have a place to let the user know wha's going on with the image. Like above, I'm going to explain little here, but comment throughout the code.
public class WebFrame extends JFrame implements ActionListener {
    
    private PicturePanel imagePanel;
    private WebScanner scan;
    private Timer timer;
    private JTextArea textField;
    private JSplitPane splitPane;
    
    public WebFrame() {
        super("Picture Viewer and Downloader");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(900,600);
        
        // Set our initial properties for our text output field
        textField = new JTextArea("Please wait...Loading huge image");
        textField.setLineWrap(true);
        JScrollPane pane = new JScrollPane(textField);
        
        // Instantiate our picture panel
        imagePanel = new PicturePanel();
        
        // Add the textField and imagePanel to the splitPane
        splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
                imagePanel, pane);
        splitPane.setOneTouchExpandable(true);
        splitPane.setDividerLocation(540);
                
        // Add the splitPane to the Frame and make frame visible
        add(splitPane);
        setVisible(true);
        
        // Make a new WebScanner and set the loading url.
        scan = new WebScanner();
        scan.setURL();

        // Make a timer that execute every half second.
        timer = new Timer(500, this);
        timer.setInitialDelay(1000);
        timer.start();
        
        // This actually begins our worker thread
        scan.startWorker();
        
        // Let the user know what's going on.
        textField.setText("Still working");
    }
    
    @Override
    public void actionPerformed(ActionEvent e) {
        // If the worker is not done, then add a dot and return
        if (!scan.isWorkerDone()) {
            textField.append(".");
            return;
        }
        
        // If the image is null, then keep trying...
        BufferedImage img = scan.getImage("");
        if (img == null) {
            textField.append(".");
            return;
        }
            
        // Finally, if the image is loaded, set it and stop the timer.
        imagePanel.setImage(scan.getImage(""));
        textField.setText("Image loaded");
        timer.stop();
    }
    
    public static void main(String[] args)  {
        new WebFrame();     
    }
    
}



See how we use the WebScanner class to check up on the SwuingWorker? In the actionPerformed() method (called every 1/2 second by the timer), we check to see if the image is done loading and if so, then we stop the timer and set the image in the image panel, otherwise we wait for it to finish. Now, the PicturePanel class is pretty similar to what you had before, but it has centered images and a "busy screen" that spins a line around and around until the iamge is done loading. (The line spinning is accomplished using the point (cos theta, sin theta) from the center). I again comment in there.
 public class PicturePanel extends JPanel implements ActionListener {


    private static final long serialVersionUID = -8687907176608557245L;
    
    // this is our image.
    private BufferedImage img = null;
    
    // Used for the "busy screen"
    private double theta = 0;
    
    // Start the timer that repaints every 60 milliseconds.
    // If the image is loaded, it displays the image, otherwise
    // it continues the busy screen.
    public PicturePanel() {     
        Timer time = new Timer(60, this);
        time.start();
    }
    
    @Override
    public void actionPerformed(ActionEvent arg0) {
        repaint();              
    }
    
    // This sets the loaded image, which stops the "busy screen"
    public void setImage(BufferedImage image) {
        img = image;
    }
    
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);    
        
        // If the image is loaded.
        if (img != null) {          
            
            // Then  we center it on the screen as I explained above.
            int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));
            if (scaledWidth < getWidth()) {
                // Center the left and right destination x coordinates.
                int leftOffset = getWidth()/2 - scaledWidth/2;
                int rightOffset = getWidth()/2 + scaledWidth/2;
                
                g.drawImage(img, 
                        leftOffset, 0, rightOffset, getHeight(), 
                        0, 0, img.getWidth(), img.getHeight(), 
                        null);
            }
            else {
                int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
                
                int topOffset = getHeight()/2 - scaledHeight/2;
                int bottomOffset = getHeight()/2 + scaledHeight/2;
                
                g.drawImage(img,
                        0, topOffset, getWidth(), bottomOffset, 
                        0, 0, img.getWidth(), img.getHeight(), 
                        null);
            }
        }
        
        // However, if the image has not been loaded yet, then
        // we make a busy screen (a line that swings in a circle).
        else {
            // set the origin to the center of the screen.
            int originX = getWidth()/2;
            int originY = getHeight()/2;
            
            // Trig to make the line move along a circle.
            theta += Math.PI/12;
            g.setColor(Color.RED);
            g.drawLine(originX, originY, (int)(Math.cos(theta)*getWidth()), 
                    (int)(Math.sin(theta)*getHeight()));
        }

    }
    
}



If you don't understand the trig, then that's simply what's making the line spin as you will see when you run it.
Alright, well that's all for this tutorial. I hope you understand how to use SwingWorkers to make SAFE multithreaded GUI applications.

Oh, Here's all three classes with imports in case you missed something:
WebScanner.java
public class PicturePanel extends JPanel implements ActionListener {


    private static final long serialVersionUID = -8687907176608557245L;
    
    // this is our image.
    private BufferedImage img = null;
    
    // Used for the "busy screen"
    private double theta = 0;
    
    // Start the timer that repaints every 60 milliseconds.
    // If the image is loaded, it displays the image, otherwise
    // it continues the busy screen.
    public PicturePanel() {     
        Timer time = new Timer(60, this);
        time.start();
    }
    
    @Override
    public void actionPerformed(ActionEvent arg0) {
        repaint();              
    }
    
    // This sets the loaded image, which stops the "busy screen"
    public void setImage(BufferedImage image) {
        img = image;
    }
    
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);    
        
        // If the image is loaded.
        if (img != null) {          
            
            // Then  we center it on the screen as I explained above.
            int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));
            if (scaledWidth < getWidth()) {
                // Center the left and right destination x coordinates.
                int leftOffset = getWidth()/2 - scaledWidth/2;
                int rightOffset = getWidth()/2 + scaledWidth/2;
                
                g.drawImage(img, 
                        leftOffset, 0, rightOffset, getHeight(), 
                        0, 0, img.getWidth(), img.getHeight(), 
                        null);
            }
            else {
                int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
                
                int topOffset = getHeight()/2 - scaledHeight/2;
                int bottomOffset = getHeight()/2 + scaledHeight/2;
                
                g.drawImage(img,
                        0, topOffset, getWidth(), bottomOffset, 
                        0, 0, img.getWidth(), img.getHeight(), 
                        null);
            }
        }
        
        // However, if the image has not been loaded yet, then
        // we make a busy screen (a line that swings in a circle.
        else {
            int originX = getWidth()/2;
            int originY = getHeight()/2;
            
            // Trig to make the line move along a circle.
            theta += Math.PI/12;
            g.setColor(Color.RED);
            g.drawLine(originX, originY, (int)(Math.cos(theta)*getWidth()), 
                    (int)(Math.sin(theta)*getHeight()));
        }

    }
    
}



PicturePanel.java
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;

import javax.swing.JPanel;
import javax.swing.Timer;

public class PicturePanel extends JPanel implements ActionListener {


    private static final long serialVersionUID = -8687907176608557245L;
    
    // this is our image.
    private BufferedImage img = null;
    
    // Used for the "busy screen"
    private double theta = 0;
    
    // Start the timer that repaints every 60 milliseconds.
    // If the image is loaded, it displays the image, otherwise
    // it continues the busy screen.
    public PicturePanel() {     
        Timer time = new Timer(60, this);
        time.start();
    }
    
    @Override
    public void actionPerformed(ActionEvent arg0) {
        repaint();              
    }
    
    // This sets the loaded image, which stops the "busy screen"
    public void setImage(BufferedImage image) {
        img = image;
    }
    
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);    
        
        // If the image is loaded.
        if (img != null) {          
            
            // Then  we center it on the screen as I explained above.
            int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));
            if (scaledWidth < getWidth()) {
                // Center the left and right destination x coordinates.
                int leftOffset = getWidth()/2 - scaledWidth/2;
                int rightOffset = getWidth()/2 + scaledWidth/2;
                
                g.drawImage(img, 
                        leftOffset, 0, rightOffset, getHeight(), 
                        0, 0, img.getWidth(), img.getHeight(), 
                        null);
            }
            else {
                int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
                
                int topOffset = getHeight()/2 - scaledHeight/2;
                int bottomOffset = getHeight()/2 + scaledHeight/2;
                
                g.drawImage(img,
                        0, topOffset, getWidth(), bottomOffset, 
                        0, 0, img.getWidth(), img.getHeight(), 
                        null);
            }
        }
        
        // However, if the image has not been loaded yet, then
        // we make a busy screen (a line that swings in a circle.
        else {
            int originX = getWidth()/2;
            int originY = getHeight()/2;
            
            // Trig to make the line move along a circle.
            theta += Math.PI/12;
            g.setColor(Color.RED);
            g.drawLine(originX, originY, (int)(Math.cos(theta)*getWidth()), 
                    (int)(Math.sin(theta)*getHeight()));
        }

    }
    
}



WebFrame.java
public class PicturePanel extends JPanel implements ActionListener {


    private static final long serialVersionUID = -8687907176608557245L;
    
    // this is our image.
    private BufferedImage img = null;
    
    // Used for the "busy screen"
    private double theta = 0;
    
    // Start the timer that repaints every 60 milliseconds.
    // If the image is loaded, it displays the image, otherwise
    // it continues the busy screen.
    public PicturePanel() {     
        Timer time = new Timer(60, this);
        time.start();
    }
    
    @Override
    public void actionPerformed(ActionEvent arg0) {
        repaint();              
    }
    
    // This sets the loaded image, which stops the "busy screen"
    public void setImage(BufferedImage image) {
        img = image;
    }
    
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);    
        
        // If the image is loaded.
        if (img != null) {          
            
            // Then  we center it on the screen as I explained above.
            int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));
            if (scaledWidth < getWidth()) {
                // Center the left and right destination x coordinates.
                int leftOffset = getWidth()/2 - scaledWidth/2;
                int rightOffset = getWidth()/2 + scaledWidth/2;
                
                g.drawImage(img, 
                        leftOffset, 0, rightOffset, getHeight(), 
                        0, 0, img.getWidth(), img.getHeight(), 
                        null);
            }
            else {
                int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
                
                int topOffset = getHeight()/2 - scaledHeight/2;
                int bottomOffset = getHeight()/2 + scaledHeight/2;
                
                g.drawImage(img,
                        0, topOffset, getWidth(), bottomOffset, 
                        0, 0, img.getWidth(), img.getHeight(), 
                        null);
            }
        }
        
        // However, if the image has not been loaded yet, then
        // we make a busy screen (a line that swings in a circle.
        else {
            int originX = getWidth()/2;
            int originY = getHeight()/2;
            
            // Trig to make the line move along a circle.
            theta += Math.PI/12;
            g.setColor(Color.RED);
            g.drawLine(originX, originY, (int)(Math.cos(theta)*getWidth()), 
                    (int)(Math.sin(theta)*getHeight()));
        }

    }
    
}



Happy Coding! :)

Is This A Good Question/Topic? 5
  • +

Replies To: Java Image Manipulation Part 3: Math and SwingWorker

#2 stalks  Icon User is offline

  • New D.I.C Head

Reputation: 8
  • View blog
  • Posts: 20
  • Joined: 07-October 10

Posted 07 October 2010 - 05:12 PM

A great tutorial. I think that Swing is often unfairly slated.

Unfortunately, the WebFrame initialisation isn't technically thread-safe.

You are allowed to create UI objects on the main application thread, but once any of the objects have been realised, you have to switch to manipulating through the Swing UI thread.

The call on line 45:

// Let the user know what's going on.
textField.setText("Still working");


isn't thread safe, because it's still on the main application thread and is being executed after the call on line 30:

setVisible(true);


The dos and don'ts are here (but are quite old so mention "show" instead of "setVisible").
If there's no other code between the call to setVisible and a single subsequent call, you'll generally be ok. There is quite a bit of code between those calls in this tutorial and given that you can easily just run it all on the Swing UI Thread, with SwingUtilities.invokeLater, I believe that it's better to be safe.
As a matter of course, I always start my Swing apps like this. It doesn't really make the code that much less readable and means you shouldn't ever get any strange behaviour.

  // --- snip
  public static void main(final String[] args)  {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        new WebFrame();
      }
    });
  }
  // --- snip



(code not compiled so there may be some errors, but I'm sure the general gist has come across)

Edited: Actually, I'm wrong as the JTextField.setText is one of the few thread safe method! I still believe the rest of what I said though :)

This post has been edited by stalks: 07 October 2010 - 05:15 PM

Was This Post Helpful? 0
  • +
  • -

#3 Dogstopper  Icon User is offline

  • The Ninjaducky
  • member icon



Reputation: 2870
  • View blog
  • Posts: 11,025
  • Joined: 15-July 08

Posted 07 October 2010 - 05:22 PM

http://download.orac...va.lang.String)

setText() is thread safe...

And it's been a while since I wrote this thread. Now I start my programs in the fashion that you have described, but I am not yet completely adept with threads yet :/

That's something I still must learn when I get time. Thanks for the input! If it inversely affects my code, I'll request a change.
Was This Post Helpful? 0
  • +
  • -

#4 stalks  Icon User is offline

  • New D.I.C Head

Reputation: 8
  • View blog
  • Posts: 20
  • Joined: 07-October 10

Posted 07 October 2010 - 05:31 PM

View PostDogstopper, on 07 October 2010 - 04:22 PM, said:

setText() is thread safe...


Yeah, I spotted that just after I posted (see my edit :blush:). Apologies.

If you're interested in developing your threading, Java Concurrency in Practice is a great book. It can be a bit detailed and hard to get in to with some of the chapters, but the information is both interesting and scary at times!
Was This Post Helpful? 1
  • +
  • -

#5 Dogstopper  Icon User is offline

  • The Ninjaducky
  • member icon



Reputation: 2870
  • View blog
  • Posts: 11,025
  • Joined: 15-July 08

Posted 07 October 2010 - 05:33 PM

Thanks! I'll definitely look into it!
Was This Post Helpful? 0
  • +
  • -

#6 Remludar  Icon User is offline

  • New D.I.C Head

Reputation: 1
  • View blog
  • Posts: 6
  • Joined: 29-April 12

Posted 29 April 2012 - 04:46 PM

I'm not sure if anyone else noticed this... but the final three "complete code with imports" postings are basically all exactly the same thing.

Nonetheless, after piecing together the actual code from the tutorial and figuring out the imports, this was a really helpful tutorial.

I just wanted to point that first bit out so that maybe it could be fixed for accuracy.

Cheers.
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1