0

I have a JPictureBox extends from a java.awt.Component see code here http://pastebin.com/SAJc6Sht. But it only works well when there is no image stretching. Especially for the big picture, it slows program dramatically. How to increase drawing speed of JPictureBox for large image?

@Override
public void paint(Graphics g) {    
    super.paint(g);

    int x = 0;
    int y = 0;
    int w = 0;
    int h = 0;
    if (image != null) {
        switch (sizeMode) {
            case AUTO_SIZE:
            case NORMAL:
                w = image.getWidth();
                h = image.getHeight();
                break;
            case CENTER_IMAGE:
                w = image.getWidth();
                h = image.getHeight();
                x = (getWidth() - w) / 2;
                y = (getHeight() - h) / 2;
                break;
            case STRETCH_IMAGE:
                w = getWidth();
                h = getHeight();
                break;
            case ZOOM:
                w = (int) Math.round(image.getWidth() * zoomFactor);
                h = (int) Math.round(image.getHeight() * zoomFactor);
                break;
            case FIT_BOTH:
                if (image.getWidth() > image.getHeight()) {
                    w = getWidth();
                    h = (int) (w / getAR());

                    if (h > getHeight()) {
                        h = getHeight();
                        w = (int) (h * getAR());
                    }
                } else {
                    h = getHeight();
                    w = (int) (h * getAR());

                    if (w > getWidth()) {
                        w = getWidth();
                        h = (int) (w / getAR());
                    }
                }
                break;
            case FIT_WIDTH:
                w = getWidth();
                h = (int) (w / getAR());
                break;
            case FIT_HEIGHT:
                h = getHeight();
                w = (int) (h * getAR());
                break;
        }

        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        g2d.drawImage(image, x, y, w, h, this);
    } else if (errorIcon != null) {
        w = errorIcon.getIconWidth();
        h = errorIcon.getIconHeight();
        x = (getWidth() - w) / 2;
        y = (getHeight() - h) / 2;
        errorIcon.paintIcon(this, g, x, y);
    }
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
  • 2
    Changing `g2d.drawImage(image, x, y, w, h, null);` to `g2d.drawImage(image, x, y, w, h, this);` won't hurt. How about not resizing the image within the Event Dispatching Thread? – MadProgrammer Dec 01 '14 at 00:14
  • Nothing change. I only resize it with JSplitPane. – Martin Leung Dec 01 '14 at 00:20
  • For better help sooner, post an [MCVE](http://stackoverflow.com/help/mcve) (Minimal Complete Verifiable Example) or [SSCCE](http://www.sscce.org/) (Short, Self Contained, Correct Example). And I mean post it here, not post a link to it. – Andrew Thompson Dec 01 '14 at 00:25
  • *"I'm update my question"* Let me know when there is an MCVE. – Andrew Thompson Dec 01 '14 at 00:49

1 Answers1

2

Basically, you want to off load the scaling of the image to a background thread, scaling is time consuming and you don't want to do within the context of the Event Dispatching Thread.

This then raises a few more issues. You don't want to scale the image until you really have to to and you really only want the latest result.

Instead of trying to scale the image on EVERY change, you can setup a small, single repeat timer which you reset each time you want to make a change. This will consolidate the multiple resize requests down to as few requests as possible. This example uses a javax.swing.Timer set to a short 125 millisecond delay. So it will wait at least 125 milliseconds between requests for a change before actually triggering the update.

Next, it uses a ExecutorService set up with a single thread. This provides us with the means to "attempt" to cancel any pre-existing operations, as we don't want there result and start our latest request.

Next, the actual scaling operation employs a two step scale, first, it tries to do a fast, low quality scale which can be put on the screen quickly and then performs a slower, high quality scale which is updated at some time in the future...

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new JScrollPane(new TestPane()));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel implements Scrollable {

        private BufferedImage master;
        private Image scaled;
        private double zoom = 1d;
        private ExecutorService service;
        private List<Future> scaleTasks;
        private final Timer zoomTimer;

        public TestPane() {
            scaleTasks = new ArrayList<>(5);
            service = Executors.newSingleThreadExecutor();
            try {
                master = ImageIO.read(new File("Some image some where"));
                scaled = master;
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            zoomTimer = new Timer(125, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    System.out.println("Update Zoom to " + getZoom());
                    updateToZoomFactor(getZoom());
                }
            });
            zoomTimer.setRepeats(false);

            InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap am = getActionMap();

            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "plus");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "minus");

            am.put("plus", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    double zoom = getZoom() + 0.1;
                    setZoom(zoom);
                }
            });
            am.put("minus", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    double zoom = getZoom() - 0.1;
                    setZoom(zoom);
                }
            });

        }

        @Override
        public Dimension getPreferredSize() {
            return scaled == null
                            ? new Dimension(master.getWidth(), master.getHeight())
                            : new Dimension(scaled.getWidth(this), scaled.getHeight(this));
        }

        public BufferedImage getMaster() {
            return master;
        }

        public void setZoom(double value) {
            if (value < 0.1) {
                value = 0.1;
            } else if (value > 2) {
                value = 2d;
            }
            if (value != zoom) {
                zoom = value;
                zoomTimer.restart();
            }
        }

        public double getZoom() {
            return zoom;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (scaled != null) {
                Graphics2D g2d = (Graphics2D) g.create();
                int x = (getWidth() - scaled.getWidth(this)) / 2;
                int y = (getHeight() - scaled.getHeight(this)) / 2;
                g2d.drawImage(scaled, x, y, this);
                g2d.dispose();
            }
        }

        protected void setScaledResult(final Image image) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    scaled = image;
                    invalidate();
                    revalidate();
                    repaint();
                }
            });
        }

        protected void updateToZoomFactor(double zoom) {
            Future[] tasks = scaleTasks.toArray(new Future[scaleTasks.size()]);
            for (Future task : tasks) {
                if (!task.isCancelled()) {
                    task.cancel(true);
                } else {
                    scaleTasks.remove(task);
                }
            }
            service.submit(new RescaleTask(zoom));
        }

        @Override
        public Dimension getPreferredScrollableViewportSize() {
            return new Dimension(400, 400);
        }

        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 128;
        }

        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 128;
        }

        @Override
        public boolean getScrollableTracksViewportWidth() {
            return false;
        }

        @Override
        public boolean getScrollableTracksViewportHeight() {
            return false;
        }

        protected class RescaleTask implements Callable<Image> {

            private double zoom;

            protected RescaleTask(double zoom) {
                this.zoom = zoom;
            }

            @Override
            public Image call() throws Exception {
                if (zoom == 1) {
                    scaled = getMaster();
                } else {
                    int width = (int) (getMaster().getWidth() * zoom);
                    int height = (int) (getMaster().getHeight() * zoom);
                    Image scaled = getMaster().getScaledInstance((int) width, (int) height, Image.SCALE_FAST);
                    if (!Thread.currentThread().isInterrupted()) {
                        setScaledResult(scaled);

                        if (zoom < 1) {
                            scaled = getScaledDownInstance(getMaster(), (int) width, (int) height);
                        } else {
                            scaled = getScaledUpInstance(getMaster(), (int) width, (int) height);
                        }

                        if (!Thread.currentThread().isInterrupted()) {
                            setScaledResult(scaled);
                        } else {
                            System.out.println("Was interrupted during quality scale");
                        }

                    } else {
                        System.out.println("Was interrupted during fast scale");
                    }
                }
                return scaled;
            }

            protected BufferedImage getScaledDownInstance(BufferedImage img,
                            int targetWidth,
                            int targetHeight) {

                int type = (img.getTransparency() == Transparency.OPAQUE)
                                ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;

                BufferedImage ret = (BufferedImage) img;

                if (targetHeight > 0 || targetWidth > 0) {

                    int w = img.getWidth();
                    int h = img.getHeight();

                    do {

                        System.out.println(w + "x" + h + " -> " + targetWidth + "x" + targetHeight);

                        if (w > targetWidth) {
                            w /= 2;
                            if (w < targetWidth) {
                                w = targetWidth;
                            }
                        }

                        if (h > targetHeight) {
                            h /= 2;
                            if (h < targetHeight) {
                                h = targetHeight;
                            }
                        }

                        BufferedImage tmp = new BufferedImage(Math.max(w, 1), Math.max(h, 1), type);
                        Graphics2D g2 = tmp.createGraphics();
                        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                        g2.drawImage(ret, 0, 0, w, h, null);
                        g2.dispose();

                        ret = tmp;

                    } while (w != targetWidth || h != targetHeight);

                } else {

                    ret = new BufferedImage(1, 1, type);

                }

                return ret;

            }

            protected BufferedImage getScaledUpInstance(BufferedImage img,
                            int targetWidth,
                            int targetHeight) {

                int type = BufferedImage.TYPE_INT_ARGB;

                BufferedImage ret = (BufferedImage) img;
                int w = img.getWidth();
                int h = img.getHeight();

                do {

                    if (w < targetWidth) {
                        w *= 2;
                        if (w > targetWidth) {
                            w = targetWidth;
                        }
                    }

                    if (h < targetHeight) {
                        h *= 2;
                        if (h > targetHeight) {
                            h = targetHeight;
                        }
                    }

//          createCompatibleImage(w, h, type)
                    BufferedImage tmp = new BufferedImage(w, h, type);
                    Graphics2D g2 = tmp.createGraphics();
                    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                    g2.drawImage(ret, 0, 0, w, h, null);
                    g2.dispose();

                    ret = tmp;
                    tmp = null;

                } while (w != targetWidth || h != targetHeight);

                return ret;

            }

        }

    }

}

nb: This is little over kill, but demonstrates some key ideas

One of the other things that might help is to convert the image to a compatible color model for the GraphicsDevice, for example...

            master = ImageIO.read(new File("Some image some where"));
            GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
            GraphicsConfiguration gc = gd.getDefaultConfiguration();
            BufferedImage compatible = gc.createCompatibleImage(master.getWidth(), master.getHeight(), Transparency.TRANSLUCENT);
            Graphics2D g2d = compatiable.createGraphics();
            g2d.drawImage(master, 0, 0, this);
            g2d.dispose();
            master = compatible;
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • I saw your code (I don't understand it, not at all :D) then I try to make some changes: painted it in a separate thread and set the buffer after drawing success. It makes my program seems to be running faster, but what I saw on the screen was not very nice but acceptable. Thank you. http://pastebin.com/XtCpyv2v – Martin Leung Dec 01 '14 at 02:29
  • That's why I do two scale operations, one fast (and horrible) and one slow and good.. – MadProgrammer Dec 01 '14 at 02:48
  • Don't rescale the image every time `paint` is called, paint could be called for a lot of reasons, not all of them because the component has been resized. Update the image when the zoom factor is changed, but again, `setZoomFactor` could be called in quick succession, better to use something like the `Timer` example to reduce the number of times you try and scale the image. Don't use a single step scale, this is really, really bad. See http://stackoverflow.com/questions/14115950/quality-of-image-after-resize-very-low-java/14116752#14116752 for more details – MadProgrammer Dec 01 '14 at 02:53