Ticket #15574: josm-image-loading_check-ram-constraints-to-prevent-oom-exceptions_do-not-use-expensive-scaling-while-dragging.patch

File josm-image-loading_check-ram-constraints-to-prevent-oom-exceptions_do-not-use-expensive-scaling-while-dragging.patch, 16.2 KB (added by cmuelle8, 6 years ago)

light version; does ram constraint checking as proposed with the larger patch; adds some javadoc; does not use bilinear scaling when image is dragged; [main diff to larger patch: strips 2nd loading attempt if loading fails due to ram constraints] (update with fix to ticket:15625)

  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java

     
    2222import java.awt.geom.AffineTransform;
    2323import java.awt.geom.Rectangle2D;
    2424import java.awt.image.BufferedImage;
     25import java.awt.image.ImageObserver;
    2526import java.io.File;
    2627
    2728import javax.swing.JComponent;
     
    112113    public static class VisRect extends Rectangle {
    113114        private final Rectangle init;
    114115
     116        /** set when this {@code VisRect} is updated by a mouse drag operation and
     117         * unset on mouse release **/
     118        public boolean isDragUpdate;
     119
    115120        /**
    116121         * Constructs a new {@code VisRect}.
    117122         * @param     x the specified X coordinate
     
    124129            init = new Rectangle(this);
    125130        }
    126131
     132        /**
     133         * Constructs a new {@code VisRect}.
     134         * @param     x the specified X coordinate
     135         * @param     y the specified Y coordinate
     136         * @param     width  the width of the rectangle
     137         * @param     height the height of the rectangle
     138         * @param     peer share full bounds with this peer {@code VisRect}
     139         */
    127140        public VisRect(int x, int y, int width, int height, VisRect peer) {
    128141            super(x, y, width, height);
    129142            init = peer.init;
     
    145158            this(0, 0, 0, 0);
    146159        }
    147160
     161        @SuppressWarnings("javadoc")
    148162        public boolean isFullView() {
    149163            return init.equals(this);
    150164        }
    151165
     166        @SuppressWarnings("javadoc")
    152167        public boolean isFullView1D() {
    153168            return (init.x == x && init.width == width)
    154169                || (init.y == y && init.height == height);
    155170        }
    156171
     172        @SuppressWarnings("javadoc")
    157173        public void reset() {
    158174            setBounds(init);
    159175        }
    160176
     177        @SuppressWarnings("javadoc")
    161178        public void checkRectPos() {
    162179            if (x < 0) {
    163180                x = 0;
     
    173190            }
    174191        }
    175192
     193        @SuppressWarnings("javadoc")
    176194        public void checkRectSize() {
    177195            if (width > init.width) {
    178196                width = init.width;
     
    182200            }
    183201        }
    184202
     203        @SuppressWarnings("javadoc")
    185204        public void checkPointInside(Point p) {
    186205            if (p.x < x) {
    187206                p.x = x;
     
    199218    }
    200219
    201220    /** The thread that reads the images. */
    202     private class LoadImageRunnable implements Runnable {
     221    private class LoadImageRunnable implements Runnable, ImageObserver {
    203222
    204223        private final File file;
    205224        private final int orientation;
     225        private int width;
     226        private int height;
    206227
    207228        LoadImageRunnable(File file, Integer orientation) {
    208229            this.file = file;
     
    210231        }
    211232
    212233        @Override
     234        public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
     235            if (((infoflags & ImageObserver.WIDTH) == ImageObserver.WIDTH) &&
     236                ((infoflags & ImageObserver.HEIGHT) == ImageObserver.HEIGHT)) {
     237                this.width = width;
     238                this.height = height;
     239                synchronized (this) {
     240                    this.notify();
     241                    return false;
     242                }
     243            }
     244            return true;
     245        }
     246
     247        @Override
    213248        public void run() {
    214249            Image img = Toolkit.getDefaultToolkit().createImage(file.getPath());
    215             tracker.addImage(img, 1);
    216250
    217             // Wait for the end of loading
    218             while (!tracker.checkID(1, true)) {
    219                 if (this.file != ImageDisplay.this.file) {
    220                     // The file has changed
    221                     tracker.removeImage(img);
    222                     return;
     251            synchronized (this) {
     252                width = -1;
     253                img.getWidth(this);
     254                img.getHeight(this);
     255
     256                while (width < 0) {
     257                    try {
     258                        this.wait();
     259                        if (width < 0) {
     260                            errorLoading = true;
     261                            return;
     262                        }
     263                    } catch (InterruptedException e) {
     264                        e.printStackTrace();
     265                    }
    223266                }
    224                 try {
    225                     Thread.sleep(5);
    226                 } catch (InterruptedException e) {
    227                     Logging.warn("InterruptedException in "+getClass().getSimpleName()+" while loading image "+file.getPath());
    228                     Thread.currentThread().interrupt();
     267            }
     268
     269            long allocatedMem = Runtime.getRuntime().totalMemory() -
     270                    Runtime.getRuntime().freeMemory();
     271            long mem = Runtime.getRuntime().maxMemory()-allocatedMem;
     272
     273            if (mem > ((long)width*height*4)*2) {
     274                Logging.info("Loading "+file.getPath()+" using default toolkit");
     275                tracker.addImage(img, 1);
     276
     277                // Wait for the end of loading
     278                while (!tracker.checkID(1, true)) {
     279                    if (this.file != ImageDisplay.this.file) {
     280                        // The file has changed
     281                        tracker.removeImage(img);
     282                        return;
     283                    }
     284                    try {
     285                        Thread.sleep(5);
     286                    } catch (InterruptedException e) {
     287                        Logging.warn("InterruptedException in "+getClass().getSimpleName()+
     288                                " while loading image "+file.getPath());
     289                        Thread.currentThread().interrupt();
     290                    }
    229291                }
     292                if (tracker.isErrorID(1)) {
     293                    img = null;
     294                    System.gc();
     295                }
     296            } else {
     297                img = null;
    230298            }
    231299
    232             boolean error = tracker.isErrorID(1);
    233             if (img.getWidth(null) < 0 || img.getHeight(null) < 0) {
    234                 error = true;
     300            if (img == null || width <= 0 || height <= 0) {
     301                tracker.removeImage(img);
     302                img = null;
    235303            }
    236304
    237305            synchronized (ImageDisplay.this) {
     
    241309                    return;
    242310                }
    243311
    244                 if (!error) {
    245                     ImageDisplay.this.image = img;
    246                     visibleRect = new VisRect(0, 0, img.getWidth(null), img.getHeight(null));
    247 
    248                     final int w = (int) visibleRect.getWidth();
    249                     final int h = (int) visibleRect.getHeight();
    250 
     312                if (img != null) {
     313                    boolean switchedDim = false;
    251314                    if (ExifReader.orientationNeedsCorrection(orientation)) {
    252                         final int hh, ww;
    253315                        if (ExifReader.orientationSwitchesDimensions(orientation)) {
    254                             ww = h;
    255                             hh = w;
    256                         } else {
    257                             ww = w;
    258                             hh = h;
     316                            width = img.getHeight(null);
     317                            height = img.getWidth(null);
     318                            switchedDim = true;
    259319                        }
    260                         final BufferedImage rot = new BufferedImage(ww, hh, BufferedImage.TYPE_INT_RGB);
    261                         final AffineTransform xform = ExifReader.getRestoreOrientationTransform(orientation, w, h);
     320                        final BufferedImage rot = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
     321                        final AffineTransform xform = ExifReader.getRestoreOrientationTransform(orientation,
     322                                img.getWidth(null), img.getHeight(null));
    262323                        final Graphics2D g = rot.createGraphics();
    263                         g.drawImage(image, xform, null);
     324                        g.drawImage(img, xform, null);
    264325                        g.dispose();
    265 
    266                         visibleRect.setSize(ww, hh);
    267                         image.flush();
    268                         ImageDisplay.this.image = rot;
     326                        img.flush();
     327                        img = rot;
    269328                    }
     329
     330                    ImageDisplay.this.image = img;
     331                    visibleRect = new VisRect(0, 0, width, height);
     332
     333                    Logging.info("Loaded "+file.getPath()+
     334                            " with dimensions "+width+"x"+height+
     335                            " mem(prev-avail="+mem/1024/1024+"m,taken="+
     336                            width*height*4/1024/1024+"m)"+
     337                            " exifOrientationSwitchedDimension="+switchedDim);
    270338                }
    271339
    272340                selectedRect = null;
    273                 errorLoading = error;
     341                errorLoading = (img == null);
    274342            }
    275343            tracker.removeImage(img);
    276344            ImageDisplay.this.repaint();
     
    473541
    474542            if (mouseIsDragging(e)) {
    475543                Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
     544                visibleRect.isDragUpdate = true;
    476545                visibleRect.x += mousePointInImg.x - p.x;
    477546                visibleRect.y += mousePointInImg.y - p.y;
    478547                visibleRect.checkRectPos();
     
    503572
    504573        @Override
    505574        public void mouseReleased(MouseEvent e) {
    506             if (!mouseIsZoomSelecting(e) || selectedRect == null)
    507                 return;
    508 
    509575            File file;
    510576            Image image;
    511577
     
    514580                image = ImageDisplay.this.image;
    515581            }
    516582
    517             if (image == null) {
     583            if (image == null)
    518584                return;
     585
     586            if (mouseIsDragging(e)) {
     587                visibleRect.isDragUpdate = false;
    519588            }
    520589
    521             int oldWidth = selectedRect.width;
    522             int oldHeight = selectedRect.height;
     590            if (mouseIsZoomSelecting(e) && selectedRect != null) {
     591                int oldWidth = selectedRect.width;
     592                int oldHeight = selectedRect.height;
    523593
    524             // Check that the zoom doesn't exceed MAX_ZOOM:1
    525             if (selectedRect.width < getSize().width / MAX_ZOOM.get()) {
    526                 selectedRect.width = (int) (getSize().width / MAX_ZOOM.get());
    527             }
    528             if (selectedRect.height < getSize().height / MAX_ZOOM.get()) {
    529                 selectedRect.height = (int) (getSize().height / MAX_ZOOM.get());
    530             }
     594                // Check that the zoom doesn't exceed MAX_ZOOM:1
     595                if (selectedRect.width < getSize().width / MAX_ZOOM.get()) {
     596                    selectedRect.width = (int) (getSize().width / MAX_ZOOM.get());
     597                }
     598                if (selectedRect.height < getSize().height / MAX_ZOOM.get()) {
     599                    selectedRect.height = (int) (getSize().height / MAX_ZOOM.get());
     600                }
    531601
    532             // Set the same ratio for the visible rectangle and the display area
    533             int hFact = selectedRect.height * getSize().width;
    534             int wFact = selectedRect.width * getSize().height;
    535             if (hFact > wFact) {
    536                 selectedRect.width = hFact / getSize().height;
    537             } else {
    538                 selectedRect.height = wFact / getSize().width;
    539             }
     602                // Set the same ratio for the visible rectangle and the display area
     603                int hFact = selectedRect.height * getSize().width;
     604                int wFact = selectedRect.width * getSize().height;
     605                if (hFact > wFact) {
     606                    selectedRect.width = hFact / getSize().height;
     607                } else {
     608                    selectedRect.height = wFact / getSize().width;
     609                }
    540610
    541             // Keep the center of the selection
    542             if (selectedRect.width != oldWidth) {
    543                 selectedRect.x -= (selectedRect.width - oldWidth) / 2;
    544             }
    545             if (selectedRect.height != oldHeight) {
    546                 selectedRect.y -= (selectedRect.height - oldHeight) / 2;
    547             }
     611                // Keep the center of the selection
     612                if (selectedRect.width != oldWidth) {
     613                    selectedRect.x -= (selectedRect.width - oldWidth) / 2;
     614                }
     615                if (selectedRect.height != oldHeight) {
     616                    selectedRect.y -= (selectedRect.height - oldHeight) / 2;
     617                }
    548618
    549             selectedRect.checkRectSize();
    550             selectedRect.checkRectPos();
     619                selectedRect.checkRectSize();
     620                selectedRect.checkRectPos();
     621            }
    551622
    552623            synchronized (ImageDisplay.this) {
    553624                if (file == ImageDisplay.this.file) {
    554                     ImageDisplay.this.visibleRect.setBounds(selectedRect);
     625                    if (selectedRect == null) {
     626                        ImageDisplay.this.visibleRect = visibleRect;
     627                    } else {
     628                        ImageDisplay.this.visibleRect.setBounds(selectedRect);
     629                        selectedRect = null;
     630                    }
    555631                }
    556632            }
    557             selectedRect = null;
    558633            ImageDisplay.this.repaint();
    559634        }
    560635
     
    586661        preferenceChanged(null);
    587662    }
    588663
     664    /**
     665     * Sets a new source image to be displayed by this {@code ImageDisplay}.
     666     * @param file new source image
     667     * @param orientation orientation of new source (landscape, portrait, upside-down, etc.)
     668     */
    589669    public void setImage(File file, Integer orientation) {
    590670        synchronized (this) {
    591671            this.file = file;
     
    650730            Rectangle target = calculateDrawImageRectangle(visibleRect, size);
    651731            double scale = target.width / (double) r.width; // pixel ratio is 1:1
    652732
    653             if (selectedRect == null && bilinLower < scale && scale < bilinUpper) {
    654                 BufferedImage bi = ImageProvider.toBufferedImage(image, r);
    655                 r.x = r.y = 0;
     733            if (selectedRect == null && !visibleRect.isDragUpdate &&
     734                bilinLower < scale && scale < bilinUpper) {
     735                try {
     736                    BufferedImage bi = ImageProvider.toBufferedImage(image, r);
     737                    if (bi != null) {
     738                        r.x = r.y = 0;
    656739
    657                 // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance()
    658                 // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm
    659                 image = ImageProvider.createScaledImage(bi, target.width, target.height,
    660                             RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    661                 r.width = target.width;
    662                 r.height = target.height;
     740                        // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance()
     741                        // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm
     742                        bi = ImageProvider.createScaledImage(bi, target.width, target.height,
     743                                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
     744                        r.width = target.width;
     745                        r.height = target.height;
     746                        image = bi;
     747                    }
     748                } catch (java.lang.OutOfMemoryError oom) {
     749                    // fall-back to the non-bilinear scaler
     750                    r.x = visibleRect.x;
     751                    r.y = visibleRect.y;
     752                    System.gc();
     753                }
    663754            } else {
    664755                // if target and r cause drawImage to scale image region to a tmp buffer exceeding
    665756                // its bounds, it will silently fail; crop with r first in such cases
     
    797888        return new VisRect(x + compRect.x, y + compRect.y, w, h, imgRect);
    798889    }
    799890
     891    /**
     892     * Make the current image either scale to fit inside this component,
     893     * or show a portion of image (1:1), if the image size is larger than
     894     * the component size.
     895     */
    800896    public void zoomBestFitOrOne() {
    801897        File file;
    802898        Image image;