Changeset 17871 in josm


Ignore:
Timestamp:
2021-05-06T21:47:00+02:00 (3 years ago)
Author:
simon04
Message:

see #20813 - Modernize ImageDisplay using ImageIO and subsampling

Location:
trunk/src/org/openstreetmap/josm
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java

    r17834 r17871  
    1010import java.awt.Graphics2D;
    1111import java.awt.Image;
    12 import java.awt.MediaTracker;
    1312import java.awt.Point;
    1413import java.awt.Rectangle;
     
    2221import java.awt.geom.Rectangle2D;
    2322import java.awt.image.BufferedImage;
    24 import java.awt.image.ImageObserver;
    25 import java.io.File;
    2623import java.io.IOException;
    2724import java.util.Objects;
    2825
    29 import javax.imageio.ImageIO;
    3026import javax.swing.JComponent;
    3127import javax.swing.SwingUtilities;
     
    3329import org.openstreetmap.josm.data.preferences.BooleanProperty;
    3430import org.openstreetmap.josm.data.preferences.DoubleProperty;
     31import org.openstreetmap.josm.data.preferences.IntegerProperty;
     32import org.openstreetmap.josm.gui.MainApplication;
    3533import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
    3634import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener;
     
    4038import org.openstreetmap.josm.tools.Destroyable;
    4139import org.openstreetmap.josm.tools.ExifReader;
    42 import org.openstreetmap.josm.tools.HiDPISupport;
    4340import org.openstreetmap.josm.tools.ImageProcessor;
    44 import org.openstreetmap.josm.tools.ImageProvider;
    4541import org.openstreetmap.josm.tools.Logging;
    4642
     
    7975    private VisRect selectedRect;
    8076
    81     /** The tracker to load the images */
    82     private final MediaTracker tracker = new MediaTracker(this);
    83 
    8477    private final ImgDisplayMouseListener imgMouseListener = new ImgDisplayMouseListener();
    8578
     
    10497        new DoubleProperty("geoimage.maximum-zoom-scale", 2.0);
    10598
    106     /** Use bilinear filtering **/
    107     private static final BooleanProperty BILIN_DOWNSAMP =
    108         new BooleanProperty("geoimage.bilinear-downsampling-progressive", true);
    109     private static final BooleanProperty BILIN_UPSAMP =
    110         new BooleanProperty("geoimage.bilinear-upsampling", false);
    111     private static double bilinUpper;
    112     private static double bilinLower;
     99    /** Maximum width (in pixels) for loading images **/
     100    private static final IntegerProperty MAX_WIDTH =
     101        new IntegerProperty("geoimage.maximum-width", 6000);
    113102
    114103    /** Show a background for the error text (may be hard on eyes) */
     
    121110            dragButton = AGPIFO_STYLE.get() ? 1 : 3;
    122111            zoomButton = dragButton == 1 ? 3 : 1;
    123         }
    124         if (e == null ||
    125             e.getKey().equals(MAX_ZOOM.getKey()) ||
    126             e.getKey().equals(BILIN_DOWNSAMP.getKey()) ||
    127             e.getKey().equals(BILIN_UPSAMP.getKey())) {
    128             bilinUpper = (BILIN_UPSAMP.get() ? 2*MAX_ZOOM.get() : (BILIN_DOWNSAMP.get() ? 0.5 : 0));
    129             bilinLower = (BILIN_DOWNSAMP.get() ? 0 : 1);
    130112        }
    131113    }
     
    237219
    238220    /** The thread that reads the images. */
    239     protected class LoadImageRunnable implements Runnable, ImageObserver {
     221    protected class LoadImageRunnable implements Runnable {
    240222
    241223        private final ImageEntry entry;
    242         private final File file;
    243224
    244225        LoadImageRunnable(ImageEntry entry) {
    245226            this.entry = entry;
    246             this.file = entry.getFile();
    247         }
    248 
    249         @Override
    250         public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
    251             if (((infoflags & ImageObserver.WIDTH) == ImageObserver.WIDTH) &&
    252                 ((infoflags & ImageObserver.HEIGHT) == ImageObserver.HEIGHT)) {
    253                 synchronized (entry) {
    254                     entry.setWidth(width);
    255                     entry.setHeight(height);
    256                     entry.notifyAll();
    257                     return false;
    258                 }
    259             }
    260             return true;
    261         }
    262 
    263         private boolean updateImageEntry(Image img) {
    264             if (img == null) {
    265                 synchronized (ImageDisplay.this) {
    266                     errorLoading = true;
    267                     ImageDisplay.this.repaint();
    268                     return false;
    269                 }
    270             }
    271             if (!(entry.getWidth() > 0 && entry.getHeight() > 0)) {
    272                 synchronized (entry) {
    273                     int width = img.getWidth(this);
    274                     int height = img.getHeight(this);
    275 
    276                     if (!(entry.getWidth() > 0 && entry.getHeight() > 0) && width > 0 && height > 0) {
    277                         // dimensions not in metadata but already present in image, so observer won't be called
    278                         entry.setWidth(width);
    279                         entry.setHeight(height);
    280                         entry.notifyAll();
    281                     }
    282 
    283                     long now = System.currentTimeMillis();
    284                     while (!(entry.getWidth() > 0 && entry.getHeight() > 0)) {
    285                         try {
    286                             entry.wait(1000);
    287                             if (this.entry != ImageDisplay.this.entry)
    288                                 return false;
    289                             if (System.currentTimeMillis() - now > 10000)
    290                                 synchronized (ImageDisplay.this) {
    291                                     errorLoading = true;
    292                                     ImageDisplay.this.repaint();
    293                                     return false;
    294                                 }
    295                         } catch (InterruptedException e) {
    296                             Logging.trace(e);
    297                             Logging.warn("InterruptedException in {0} while getting properties of image {1}",
    298                                     getClass().getSimpleName(), file.getPath());
    299                             Thread.currentThread().interrupt();
    300                         }
    301                     }
    302                 }
    303             }
    304             return true;
    305         }
    306 
    307         private boolean mayFitMemory(long amountWanted) {
    308             return amountWanted < (
    309                    Runtime.getRuntime().maxMemory() -
    310                    Runtime.getRuntime().totalMemory() +
    311                    Runtime.getRuntime().freeMemory());
    312227        }
    313228
    314229        @Override
    315230        public void run() {
    316             BufferedImage img;
    317231            try {
    318                 img = ImageIO.read(file);
    319 
    320                 if (!updateImageEntry(img))
    321                     return;
    322 
    323                 int width = entry.getWidth();
    324                 int height = entry.getHeight();
    325 
    326                 if (mayFitMemory(((long) width)*height*4*2)) {
    327                     Logging.info(tr("Loading {0}", file.getPath()));
    328                     tracker.addImage(img, 1);
    329 
    330                     // Wait for the end of loading
    331                     while (!tracker.checkID(1, true)) {
    332                         if (this.entry != ImageDisplay.this.entry) {
    333                             // The file has changed
    334                             tracker.removeImage(img);
    335                             return;
    336                         }
    337                         try {
    338                             Thread.sleep(5);
    339                         } catch (InterruptedException e) {
    340                             Logging.trace(e);
    341                             Logging.warn("InterruptedException in {0} while loading image {1}",
    342                                     getClass().getSimpleName(), file.getPath());
    343                             Thread.currentThread().interrupt();
    344                         }
     232                Dimension target = new Dimension(MAX_WIDTH.get(), MAX_WIDTH.get());
     233                BufferedImage img = entry.read(target);
     234                if (img == null) {
     235                    synchronized (ImageDisplay.this) {
     236                        errorLoading = true;
     237                        ImageDisplay.this.repaint();
     238                        return;
    345239                    }
    346                     if (tracker.isErrorID(1)) {
    347                         Logging.warn("Abort loading of {0} since tracker errored with 1", file);
    348                         // the tracker catches OutOfMemory conditions
    349                         tracker.removeImage(img);
    350                         img = null;
    351                     } else {
    352                         tracker.removeImage(img);
    353                     }
    354                 } else {
    355                     Logging.warn("Abort loading of {0} since it might not fit into memory", file);
    356                     img = null;
    357                 }
     240                }
     241
     242                int width = img.getWidth();
     243                int height = img.getHeight();
     244                entry.setWidth(width);
     245                entry.setHeight(height);
    358246
    359247                synchronized (ImageDisplay.this) {
     
    363251                    }
    364252
    365                     if (img != null) {
    366                         boolean switchedDim = false;
    367                         if (ExifReader.orientationNeedsCorrection(entry.getExifOrientation())) {
    368                             if (ExifReader.orientationSwitchesDimensions(entry.getExifOrientation())) {
    369                                 width = img.getHeight(null);
    370                                 height = img.getWidth(null);
    371                                 switchedDim = true;
    372                             }
    373                             final BufferedImage rot = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    374                             final AffineTransform xform = ExifReader.getRestoreOrientationTransform(
    375                                     entry.getExifOrientation(),
    376                                     img.getWidth(null),
    377                                     img.getHeight(null));
    378                             final Graphics2D g = rot.createGraphics();
    379                             g.drawImage(img, xform, null);
    380                             g.dispose();
    381                             img = rot;
     253                    boolean switchedDim = false;
     254                    if (ExifReader.orientationNeedsCorrection(entry.getExifOrientation())) {
     255                        if (ExifReader.orientationSwitchesDimensions(entry.getExifOrientation())) {
     256                            width = img.getHeight(null);
     257                            height = img.getWidth(null);
     258                            switchedDim = true;
    382259                        }
    383 
    384                         ImageDisplay.this.image = img;
    385                         updateProcessedImage();
    386                         // This will clear the loading info box
    387                         ImageDisplay.this.oldEntry = ImageDisplay.this.entry;
    388                         visibleRect = new VisRect(0, 0, width, height);
    389 
    390                         Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}",
    391                                 file.getPath(), width, height, width*height*4/1024/1024, switchedDim);
     260                        final BufferedImage rot = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
     261                        final AffineTransform xform = ExifReader.getRestoreOrientationTransform(
     262                                entry.getExifOrientation(),
     263                                img.getWidth(null),
     264                                img.getHeight(null));
     265                        final Graphics2D g = rot.createGraphics();
     266                        g.drawImage(img, xform, null);
     267                        g.dispose();
     268                        img = rot;
    392269                    }
    393270
     271                    ImageDisplay.this.image = img;
     272                    updateProcessedImage();
     273                    // This will clear the loading info box
     274                    ImageDisplay.this.oldEntry = ImageDisplay.this.entry;
     275                    visibleRect = new VisRect(0, 0, width, height);
     276
     277                    Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}",
     278                            entry.getFile().getPath(), width, height, width * height * 4 / 1024 / 1024, switchedDim);
     279
    394280                    selectedRect = null;
    395                     errorLoading = (img == null);
     281                    errorLoading = false;
    396282                }
    397283                ImageDisplay.this.repaint();
     
    746632        LoadImageRunnable runnable = setImage0(entry);
    747633        if (runnable != null) {
    748             new Thread(runnable, LoadImageRunnable.class.getName()).start();
     634            MainApplication.worker.execute(runnable);
    749635        }
    750636    }
     
    821707            Rectangle r = new Rectangle(visibleRect);
    822708            Rectangle target = calculateDrawImageRectangle(visibleRect, size);
    823             double scale = target.width / (double) r.width; // pixel ratio is 1:1
    824 
    825             if (selectedRect == null && !visibleRect.isDragUpdate &&
    826                 bilinLower < scale && scale < bilinUpper) {
    827                 try {
    828                     BufferedImage bi = ImageProvider.toBufferedImage(image, r);
    829                     if (bi != null) {
    830                         r.x = r.y = 0;
    831                         double hiDPIScale = HiDPISupport.getHiDPIScale();
    832                         int width = (int) (target.width * hiDPIScale);
    833                         int height = (int) (target.height * hiDPIScale);
    834                         // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance()
    835                         // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm
    836                         bi = ImageProvider.createScaledImage(bi, width, height, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    837                         r.width = width;
    838                         r.height = height;
    839                         image = bi;
    840                     }
    841                 } catch (OutOfMemoryError oom) {
    842                     Logging.trace(oom);
    843                     // fall-back to the non-bilinear scaler
    844                     r.x = visibleRect.x;
    845                     r.y = visibleRect.y;
    846                 }
    847             } else {
    848                 // if target and r cause drawImage to scale image region to a tmp buffer exceeding
    849                 // its bounds, it will silently fail; crop with r first in such cases
    850                 // (might be impl. dependent, exhibited by openjdk 1.8.0_151)
    851                 if (scale*(r.x+r.width) > Short.MAX_VALUE || scale*(r.y+r.height) > Short.MAX_VALUE) {
    852                     image = ImageProvider.toBufferedImage(image, r);
    853                     r.x = r.y = 0;
    854                 }
    855             }
    856709
    857710            g.drawImage(image,
  • trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java

    r17580 r17871  
    22package org.openstreetmap.josm.gui.layer.geoimage;
    33
     4import java.awt.Dimension;
    45import java.awt.Image;
     6import java.awt.image.BufferedImage;
    57import java.io.File;
     8import java.io.IOException;
     9import java.io.UncheckedIOException;
    610import java.util.Collections;
    711import java.util.Objects;
     
    913import org.openstreetmap.josm.data.ImageData;
    1014import org.openstreetmap.josm.data.gpx.GpxImageEntry;
     15import org.openstreetmap.josm.tools.ImageProvider;
     16import org.openstreetmap.josm.tools.Logging;
     17
     18import javax.imageio.IIOParam;
     19import javax.imageio.ImageReadParam;
     20import javax.imageio.ImageReader;
     21
     22import static org.openstreetmap.josm.tools.I18n.tr;
    1123
    1224/**
     
    118130        return Objects.equals(thumbnail, other.thumbnail) && Objects.equals(dataSet, other.dataSet);
    119131    }
     132
     133    /**
     134     * Reads the image represented by this entry in the given target dimension.
     135     * @param target the desired dimension used for {@linkplain IIOParam#setSourceSubsampling subsampling} or {@code null}
     136     * @return the read image
     137     * @throws IOException if any I/O error occurs
     138     */
     139    public BufferedImage read(Dimension target) throws IOException {
     140        Logging.info(tr("Loading {0}", getFile().getPath()));
     141        return ImageProvider.read(getFile(), false, false,
     142                r -> target == null ? r.getDefaultReadParam() : withSubsampling(r, target));
     143    }
     144
     145    private ImageReadParam withSubsampling(ImageReader reader, Dimension target) {
     146        try {
     147            ImageReadParam param = reader.getDefaultReadParam();
     148            Dimension source = new Dimension(reader.getWidth(0), reader.getHeight(0));
     149            if (source.getWidth() > target.getWidth() || source.getHeight() > target.getHeight()) {
     150                int subsampling = (int) Math.floor(Math.max(
     151                        source.getWidth() / target.getWidth(),
     152                        source.getHeight() / target.getHeight()));
     153                param.setSourceSubsampling(subsampling, subsampling, 0, 0);
     154            }
     155            return param;
     156        } catch (IOException e) {
     157            throw new UncheckedIOException(e);
     158        }
     159    }
    120160}
  • trunk/src/org/openstreetmap/josm/tools/ImageProvider.java

    r17369 r17871  
    4848import java.util.concurrent.Executors;
    4949import java.util.function.Consumer;
     50import java.util.function.Function;
    5051import java.util.function.UnaryOperator;
    5152import java.util.regex.Matcher;
     
    15481549     */
    15491550    public static BufferedImage read(File input, boolean readMetadata, boolean enforceTransparency) throws IOException {
     1551        return read(input, readMetadata, enforceTransparency, ImageReader::getDefaultReadParam);
     1552    }
     1553
     1554    /**
     1555     * Returns a <code>BufferedImage</code> as the result of decoding
     1556     * a supplied <code>File</code> with an <code>ImageReader</code>
     1557     * chosen automatically from among those currently registered.
     1558     * The <code>File</code> is wrapped in an
     1559     * <code>ImageInputStream</code>.  If no registered
     1560     * <code>ImageReader</code> claims to be able to read the
     1561     * resulting stream, <code>null</code> is returned.
     1562     *
     1563     * <p> The current cache settings from <code>getUseCache</code>and
     1564     * <code>getCacheDirectory</code> will be used to control caching in the
     1565     * <code>ImageInputStream</code> that is created.
     1566     *
     1567     * <p> Note that there is no <code>read</code> method that takes a
     1568     * filename as a <code>String</code>; use this method instead after
     1569     * creating a <code>File</code> from the filename.
     1570     *
     1571     * <p> This method does not attempt to locate
     1572     * <code>ImageReader</code>s that can read directly from a
     1573     * <code>File</code>; that may be accomplished using
     1574     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
     1575     *
     1576     * @param input a <code>File</code> to read from.
     1577     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color, if any.
     1578     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
     1579     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
     1580     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
     1581     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
     1582     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
     1583     * @param readParamFunction a function to compute the read parameters from the image reader
     1584     *
     1585     * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
     1586     *
     1587     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
     1588     * @throws IOException if an error occurs during reading.
     1589     * @see BufferedImage#getProperty
     1590     * @since xxx
     1591     */
     1592    public static BufferedImage read(File input, boolean readMetadata, boolean enforceTransparency,
     1593                                     Function<ImageReader, ImageReadParam> readParamFunction) throws IOException {
    15501594        CheckParameterUtil.ensureParameterNotNull(input, "input");
    15511595        if (!input.canRead()) {
     
    15571601            throw new IIOException("Can't create an ImageInputStream!");
    15581602        }
    1559         BufferedImage bi = read(stream, readMetadata, enforceTransparency);
     1603        BufferedImage bi = read(stream, readMetadata, enforceTransparency, readParamFunction);
    15601604        if (bi == null) {
    15611605            stream.close();
     
    16871731     */
    16881732    public static BufferedImage read(ImageInputStream stream, boolean readMetadata, boolean enforceTransparency) throws IOException {
     1733        return read(stream, readMetadata, enforceTransparency, ImageReader::getDefaultReadParam);
     1734    }
     1735
     1736    private static BufferedImage read(ImageInputStream stream, boolean readMetadata, boolean enforceTransparency,
     1737                                      Function<ImageReader, ImageReadParam> readParamFunction) throws IOException {
    16891738        CheckParameterUtil.ensureParameterNotNull(stream, "stream");
    16901739
     
    16951744
    16961745        ImageReader reader = iter.next();
    1697         ImageReadParam param = reader.getDefaultReadParam();
    16981746        reader.setInput(stream, true, !readMetadata && !enforceTransparency);
     1747        ImageReadParam param = readParamFunction.apply(reader);
    16991748        BufferedImage bi = null;
    17001749        try { // NOPMD
Note: See TracChangeset for help on using the changeset viewer.