001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Cursor;
008import java.awt.Dimension;
009import java.awt.Graphics;
010import java.awt.Graphics2D;
011import java.awt.GraphicsEnvironment;
012import java.awt.Image;
013import java.awt.Point;
014import java.awt.Rectangle;
015import java.awt.RenderingHints;
016import java.awt.Toolkit;
017import java.awt.Transparency;
018import java.awt.image.BufferedImage;
019import java.awt.image.ColorModel;
020import java.awt.image.FilteredImageSource;
021import java.awt.image.ImageFilter;
022import java.awt.image.ImageProducer;
023import java.awt.image.RGBImageFilter;
024import java.awt.image.WritableRaster;
025import java.io.ByteArrayInputStream;
026import java.io.File;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.StringReader;
030import java.net.URI;
031import java.net.URL;
032import java.nio.charset.StandardCharsets;
033import java.util.Arrays;
034import java.util.Base64;
035import java.util.Collection;
036import java.util.HashMap;
037import java.util.HashSet;
038import java.util.Hashtable;
039import java.util.Iterator;
040import java.util.LinkedList;
041import java.util.List;
042import java.util.Map;
043import java.util.Set;
044import java.util.TreeSet;
045import java.util.concurrent.CompletableFuture;
046import java.util.concurrent.ExecutorService;
047import java.util.concurrent.Executors;
048import java.util.function.Consumer;
049import java.util.regex.Matcher;
050import java.util.regex.Pattern;
051import java.util.zip.ZipEntry;
052import java.util.zip.ZipFile;
053
054import javax.imageio.IIOException;
055import javax.imageio.ImageIO;
056import javax.imageio.ImageReadParam;
057import javax.imageio.ImageReader;
058import javax.imageio.metadata.IIOMetadata;
059import javax.imageio.stream.ImageInputStream;
060import javax.swing.ImageIcon;
061import javax.xml.parsers.ParserConfigurationException;
062
063import org.openstreetmap.josm.Main;
064import org.openstreetmap.josm.data.osm.DataSet;
065import org.openstreetmap.josm.data.osm.OsmPrimitive;
066import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
067import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
068import org.openstreetmap.josm.gui.mappaint.Range;
069import org.openstreetmap.josm.gui.mappaint.StyleElementList;
070import org.openstreetmap.josm.gui.mappaint.styleelement.MapImage;
071import org.openstreetmap.josm.gui.mappaint.styleelement.NodeElement;
072import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement;
073import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
074import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
075import org.openstreetmap.josm.io.CachedFile;
076import org.openstreetmap.josm.spi.preferences.Config;
077import org.w3c.dom.Element;
078import org.w3c.dom.Node;
079import org.w3c.dom.NodeList;
080import org.xml.sax.Attributes;
081import org.xml.sax.InputSource;
082import org.xml.sax.SAXException;
083import org.xml.sax.XMLReader;
084import org.xml.sax.helpers.DefaultHandler;
085
086import com.kitfox.svg.SVGDiagram;
087import com.kitfox.svg.SVGException;
088import com.kitfox.svg.SVGUniverse;
089
090/**
091 * Helper class to support the application with images.
092 *
093 * How to use:
094 *
095 * <code>ImageIcon icon = new ImageProvider(name).setMaxSize(ImageSizes.MAP).get();</code>
096 * (there are more options, see below)
097 *
098 * short form:
099 * <code>ImageIcon icon = ImageProvider.get(name);</code>
100 *
101 * @author imi
102 */
103public class ImageProvider {
104
105    // CHECKSTYLE.OFF: SingleSpaceSeparator
106    private static final String HTTP_PROTOCOL  = "http://";
107    private static final String HTTPS_PROTOCOL = "https://";
108    private static final String WIKI_PROTOCOL  = "wiki://";
109    // CHECKSTYLE.ON: SingleSpaceSeparator
110
111    /**
112     * Supported image types
113     */
114    public enum ImageType {
115        /** Scalable vector graphics */
116        SVG,
117        /** Everything else, e.g. png, gif (must be supported by Java) */
118        OTHER
119    }
120
121    /**
122     * Supported image sizes
123     * @since 7687
124     */
125    public enum ImageSizes {
126        /** SMALL_ICON value of an Action */
127        SMALLICON(Config.getPref().getInt("iconsize.smallicon", 16)),
128        /** LARGE_ICON_KEY value of an Action */
129        LARGEICON(Config.getPref().getInt("iconsize.largeicon", 24)),
130        /** map icon */
131        MAP(Config.getPref().getInt("iconsize.map", 16)),
132        /** map icon maximum size */
133        MAPMAX(Config.getPref().getInt("iconsize.mapmax", 48)),
134        /** cursor icon size */
135        CURSOR(Config.getPref().getInt("iconsize.cursor", 32)),
136        /** cursor overlay icon size */
137        CURSOROVERLAY(CURSOR),
138        /** menu icon size */
139        MENU(SMALLICON),
140        /** menu icon size in popup menus
141         * @since 8323
142         */
143        POPUPMENU(LARGEICON),
144        /** Layer list icon size
145         * @since 8323
146         */
147        LAYER(Config.getPref().getInt("iconsize.layer", 16)),
148        /** Toolbar button icon size
149         * @since 9253
150         */
151        TOOLBAR(LARGEICON),
152        /** Side button maximum height
153         * @since 9253
154         */
155        SIDEBUTTON(Config.getPref().getInt("iconsize.sidebutton", 20)),
156        /** Settings tab icon size
157         * @since 9253
158         */
159        SETTINGS_TAB(Config.getPref().getInt("iconsize.settingstab", 48)),
160        /**
161         * The default image size
162         * @since 9705
163         */
164        DEFAULT(Config.getPref().getInt("iconsize.default", 24)),
165        /**
166         * Splash dialog logo size
167         * @since 10358
168         */
169        SPLASH_LOGO(128, 129),
170        /**
171         * About dialog logo size
172         * @since 10358
173         */
174        ABOUT_LOGO(256, 258),
175        /**
176         * Status line logo size
177         * @since 13369
178         */
179        STATUSLINE(18, 18);
180
181        private final int virtualWidth;
182        private final int virtualHeight;
183
184        ImageSizes(int imageSize) {
185            this.virtualWidth = imageSize;
186            this.virtualHeight = imageSize;
187        }
188
189        ImageSizes(int width, int height) {
190            this.virtualWidth = width;
191            this.virtualHeight = height;
192        }
193
194        ImageSizes(ImageSizes that) {
195            this.virtualWidth = that.virtualWidth;
196            this.virtualHeight = that.virtualHeight;
197        }
198
199        /**
200         * Returns the image width in virtual pixels
201         * @return the image width in virtual pixels
202         * @since 9705
203         */
204        public int getVirtualWidth() {
205            return virtualWidth;
206        }
207
208        /**
209         * Returns the image height in virtual pixels
210         * @return the image height in virtual pixels
211         * @since 9705
212         */
213        public int getVirtualHeight() {
214            return virtualHeight;
215        }
216
217        /**
218         * Returns the image width in pixels to use for display
219         * @return the image width in pixels to use for display
220         * @since 10484
221         */
222        public int getAdjustedWidth() {
223            return GuiSizesHelper.getSizeDpiAdjusted(virtualWidth);
224        }
225
226        /**
227         * Returns the image height in pixels to use for display
228         * @return the image height in pixels to use for display
229         * @since 10484
230         */
231        public int getAdjustedHeight() {
232            return GuiSizesHelper.getSizeDpiAdjusted(virtualHeight);
233        }
234
235        /**
236         * Returns the image size as dimension
237         * @return the image size as dimension
238         * @since 9705
239         */
240        public Dimension getImageDimension() {
241            return new Dimension(virtualWidth, virtualHeight);
242        }
243    }
244
245    /**
246     * Property set on {@code BufferedImage} returned by {@link #makeImageTransparent}.
247     * @since 7132
248     */
249    public static final String PROP_TRANSPARENCY_FORCED = "josm.transparency.forced";
250
251    /**
252     * Property set on {@code BufferedImage} returned by {@link #read} if metadata is required.
253     * @since 7132
254     */
255    public static final String PROP_TRANSPARENCY_COLOR = "josm.transparency.color";
256
257    /** set of class loaders to take images from */
258    protected static final Set<ClassLoader> classLoaders = new HashSet<>(Arrays.asList(
259            ClassLoader.getSystemClassLoader(), ImageProvider.class.getClassLoader()));
260
261    /** directories in which images are searched */
262    protected Collection<String> dirs;
263    /** caching identifier */
264    protected String id;
265    /** sub directory the image can be found in */
266    protected String subdir;
267    /** image file name */
268    protected String name;
269    /** archive file to take image from */
270    protected File archive;
271    /** directory inside the archive */
272    protected String inArchiveDir;
273    /** virtual width of the resulting image, -1 when original image data should be used */
274    protected int virtualWidth = -1;
275    /** virtual height of the resulting image, -1 when original image data should be used */
276    protected int virtualHeight = -1;
277    /** virtual maximum width of the resulting image, -1 for no restriction */
278    protected int virtualMaxWidth = -1;
279    /** virtual maximum height of the resulting image, -1 for no restriction */
280    protected int virtualMaxHeight = -1;
281    /** In case of errors do not throw exception but return <code>null</code> for missing image */
282    protected boolean optional;
283    /** <code>true</code> if warnings should be suppressed */
284    protected boolean suppressWarnings;
285    /** ordered list of overlay images */
286    protected List<ImageOverlay> overlayInfo;
287    /** <code>true</code> if icon must be grayed out */
288    protected boolean isDisabled;
289    /** <code>true</code> if multi-resolution image is requested */
290    protected boolean multiResolution = true;
291
292    private static SVGUniverse svgUniverse;
293
294    /**
295     * The icon cache
296     */
297    private static final Map<String, ImageResource> cache = new HashMap<>();
298
299    /**
300     * Caches the image data for rotated versions of the same image.
301     */
302    private static final Map<Image, Map<Long, Image>> ROTATE_CACHE = new HashMap<>();
303
304    private static final ExecutorService IMAGE_FETCHER =
305            Executors.newSingleThreadExecutor(Utils.newThreadFactory("image-fetcher-%d", Thread.NORM_PRIORITY));
306
307    /**
308     * Constructs a new {@code ImageProvider} from a filename in a given directory.
309     * @param subdir subdirectory the image lies in
310     * @param name the name of the image. If it does not end with '.png' or '.svg',
311     * both extensions are tried.
312     */
313    public ImageProvider(String subdir, String name) {
314        this.subdir = subdir;
315        this.name = name;
316    }
317
318    /**
319     * Constructs a new {@code ImageProvider} from a filename.
320     * @param name the name of the image. If it does not end with '.png' or '.svg',
321     * both extensions are tried.
322     */
323    public ImageProvider(String name) {
324        this.name = name;
325    }
326
327    /**
328     * Constructs a new {@code ImageProvider} from an existing one.
329     * @param image the existing image provider to be copied
330     * @since 8095
331     */
332    public ImageProvider(ImageProvider image) {
333        this.dirs = image.dirs;
334        this.id = image.id;
335        this.subdir = image.subdir;
336        this.name = image.name;
337        this.archive = image.archive;
338        this.inArchiveDir = image.inArchiveDir;
339        this.virtualWidth = image.virtualWidth;
340        this.virtualHeight = image.virtualHeight;
341        this.virtualMaxWidth = image.virtualMaxWidth;
342        this.virtualMaxHeight = image.virtualMaxHeight;
343        this.optional = image.optional;
344        this.suppressWarnings = image.suppressWarnings;
345        this.overlayInfo = image.overlayInfo;
346        this.isDisabled = image.isDisabled;
347        this.multiResolution = image.multiResolution;
348    }
349
350    /**
351     * Directories to look for the image.
352     * @param dirs The directories to look for.
353     * @return the current object, for convenience
354     */
355    public ImageProvider setDirs(Collection<String> dirs) {
356        this.dirs = dirs;
357        return this;
358    }
359
360    /**
361     * Set an id used for caching.
362     * If name starts with <code>http://</code> Id is not used for the cache.
363     * (A URL is unique anyway.)
364     * @param id the id for the cached image
365     * @return the current object, for convenience
366     */
367    public ImageProvider setId(String id) {
368        this.id = id;
369        return this;
370    }
371
372    /**
373     * Specify a zip file where the image is located.
374     *
375     * (optional)
376     * @param archive zip file where the image is located
377     * @return the current object, for convenience
378     */
379    public ImageProvider setArchive(File archive) {
380        this.archive = archive;
381        return this;
382    }
383
384    /**
385     * Specify a base path inside the zip file.
386     *
387     * The subdir and name will be relative to this path.
388     *
389     * (optional)
390     * @param inArchiveDir path inside the archive
391     * @return the current object, for convenience
392     */
393    public ImageProvider setInArchiveDir(String inArchiveDir) {
394        this.inArchiveDir = inArchiveDir;
395        return this;
396    }
397
398    /**
399     * Add an overlay over the image. Multiple overlays are possible.
400     *
401     * @param overlay overlay image and placement specification
402     * @return the current object, for convenience
403     * @since 8095
404     */
405    public ImageProvider addOverlay(ImageOverlay overlay) {
406        if (overlayInfo == null) {
407            overlayInfo = new LinkedList<>();
408        }
409        overlayInfo.add(overlay);
410        return this;
411    }
412
413    /**
414     * Set the dimensions of the image.
415     *
416     * If not specified, the original size of the image is used.
417     * The width part of the dimension can be -1. Then it will only set the height but
418     * keep the aspect ratio. (And the other way around.)
419     * @param size final dimensions of the image
420     * @return the current object, for convenience
421     */
422    public ImageProvider setSize(Dimension size) {
423        this.virtualWidth = size.width;
424        this.virtualHeight = size.height;
425        return this;
426    }
427
428    /**
429     * Set the dimensions of the image.
430     *
431     * If not specified, the original size of the image is used.
432     * @param size final dimensions of the image
433     * @return the current object, for convenience
434     * @since 7687
435     */
436    public ImageProvider setSize(ImageSizes size) {
437        return setSize(size.getImageDimension());
438    }
439
440    /**
441     * Set the dimensions of the image.
442     *
443     * @param width final width of the image
444     * @param height final height of the image
445     * @return the current object, for convenience
446     * @since 10358
447     */
448    public ImageProvider setSize(int width, int height) {
449        this.virtualWidth = width;
450        this.virtualHeight = height;
451        return this;
452    }
453
454    /**
455     * Set image width
456     * @param width final width of the image
457     * @return the current object, for convenience
458     * @see #setSize
459     */
460    public ImageProvider setWidth(int width) {
461        this.virtualWidth = width;
462        return this;
463    }
464
465    /**
466     * Set image height
467     * @param height final height of the image
468     * @return the current object, for convenience
469     * @see #setSize
470     */
471    public ImageProvider setHeight(int height) {
472        this.virtualHeight = height;
473        return this;
474    }
475
476    /**
477     * Limit the maximum size of the image.
478     *
479     * It will shrink the image if necessary, but keep the aspect ratio.
480     * The given width or height can be -1 which means this direction is not bounded.
481     *
482     * 'size' and 'maxSize' are not compatible, you should set only one of them.
483     * @param maxSize maximum image size
484     * @return the current object, for convenience
485     */
486    public ImageProvider setMaxSize(Dimension maxSize) {
487        this.virtualMaxWidth = maxSize.width;
488        this.virtualMaxHeight = maxSize.height;
489        return this;
490    }
491
492    /**
493     * Limit the maximum size of the image.
494     *
495     * It will shrink the image if necessary, but keep the aspect ratio.
496     * The given width or height can be -1 which means this direction is not bounded.
497     *
498     * This function sets value using the most restrictive of the new or existing set of
499     * values.
500     *
501     * @param maxSize maximum image size
502     * @return the current object, for convenience
503     * @see #setMaxSize(Dimension)
504     */
505    public ImageProvider resetMaxSize(Dimension maxSize) {
506        if (this.virtualMaxWidth == -1 || maxSize.width < this.virtualMaxWidth) {
507            this.virtualMaxWidth = maxSize.width;
508        }
509        if (this.virtualMaxHeight == -1 || maxSize.height < this.virtualMaxHeight) {
510            this.virtualMaxHeight = maxSize.height;
511        }
512        return this;
513    }
514
515    /**
516     * Limit the maximum size of the image.
517     *
518     * It will shrink the image if necessary, but keep the aspect ratio.
519     * The given width or height can be -1 which means this direction is not bounded.
520     *
521     * 'size' and 'maxSize' are not compatible, you should set only one of them.
522     * @param size maximum image size
523     * @return the current object, for convenience
524     * @since 7687
525     */
526    public ImageProvider setMaxSize(ImageSizes size) {
527        return setMaxSize(size.getImageDimension());
528    }
529
530    /**
531     * Convenience method, see {@link #setMaxSize(Dimension)}.
532     * @param maxSize maximum image size
533     * @return the current object, for convenience
534     */
535    public ImageProvider setMaxSize(int maxSize) {
536        return this.setMaxSize(new Dimension(maxSize, maxSize));
537    }
538
539    /**
540     * Limit the maximum width of the image.
541     * @param maxWidth maximum image width
542     * @return the current object, for convenience
543     * @see #setMaxSize
544     */
545    public ImageProvider setMaxWidth(int maxWidth) {
546        this.virtualMaxWidth = maxWidth;
547        return this;
548    }
549
550    /**
551     * Limit the maximum height of the image.
552     * @param maxHeight maximum image height
553     * @return the current object, for convenience
554     * @see #setMaxSize
555     */
556    public ImageProvider setMaxHeight(int maxHeight) {
557        this.virtualMaxHeight = maxHeight;
558        return this;
559    }
560
561    /**
562     * Decide, if an exception should be thrown, when the image cannot be located.
563     *
564     * Set to true, when the image URL comes from user data and the image may be missing.
565     *
566     * @param optional true, if JOSM should <b>not</b> throw a RuntimeException
567     * in case the image cannot be located.
568     * @return the current object, for convenience
569     */
570    public ImageProvider setOptional(boolean optional) {
571        this.optional = optional;
572        return this;
573    }
574
575    /**
576     * Suppresses warning on the command line in case the image cannot be found.
577     *
578     * In combination with setOptional(true);
579     * @param suppressWarnings if <code>true</code> warnings are suppressed
580     * @return the current object, for convenience
581     */
582    public ImageProvider setSuppressWarnings(boolean suppressWarnings) {
583        this.suppressWarnings = suppressWarnings;
584        return this;
585    }
586
587    /**
588     * Add an additional class loader to search image for.
589     * @param additionalClassLoader class loader to add to the internal set
590     * @return {@code true} if the set changed as a result of the call
591     * @since 12870
592     */
593    public static boolean addAdditionalClassLoader(ClassLoader additionalClassLoader) {
594        return classLoaders.add(additionalClassLoader);
595    }
596
597    /**
598     * Add a collection of additional class loaders to search image for.
599     * @param additionalClassLoaders class loaders to add to the internal set
600     * @return {@code true} if the set changed as a result of the call
601     * @since 12870
602     */
603    public static boolean addAdditionalClassLoaders(Collection<ClassLoader> additionalClassLoaders) {
604        return classLoaders.addAll(additionalClassLoaders);
605    }
606
607    /**
608     * Set, if image must be filtered to grayscale so it will look like disabled icon.
609     *
610     * @param disabled true, if image must be grayed out for disabled state
611     * @return the current object, for convenience
612     * @since 10428
613     */
614    public ImageProvider setDisabled(boolean disabled) {
615        this.isDisabled = disabled;
616        return this;
617    }
618
619    /**
620     * Decide, if multi-resolution image is requested (default <code>true</code>).
621     * <p>
622     * A <code>java.awt.image.MultiResolutionImage</code> is a Java 9 {@link Image}
623     * implementation, which adds support for HiDPI displays. The effect will be
624     * that in HiDPI mode, when GUI elements are scaled by a factor 1.5, 2.0, etc.,
625     * the images are not just up-scaled, but a higher resolution version of the image is rendered instead.
626     * <p>
627     * Use {@link HiDPISupport#getBaseImage(java.awt.Image)} to extract the original image from a multi-resolution image.
628     * <p>
629     * See {@link HiDPISupport#processMRImage} for how to process the image without removing the multi-resolution magic.
630     * @param multiResolution true, if multi-resolution image is requested
631     * @return the current object, for convenience
632     */
633    public ImageProvider setMultiResolution(boolean multiResolution) {
634        this.multiResolution = multiResolution;
635        return this;
636    }
637
638    /**
639     * Determines if this icon is located on a remote location (http, https, wiki).
640     * @return {@code true} if this icon is located on a remote location (http, https, wiki)
641     * @since 13250
642     */
643    public boolean isRemote() {
644        return name.startsWith(HTTP_PROTOCOL) || name.startsWith(HTTPS_PROTOCOL) || name.startsWith(WIKI_PROTOCOL);
645    }
646
647    /**
648     * Execute the image request and scale result.
649     * @return the requested image or null if the request failed
650     */
651    public ImageIcon get() {
652        ImageResource ir = getResource();
653
654        if (ir == null) {
655            return null;
656        }
657        if (virtualMaxWidth != -1 || virtualMaxHeight != -1)
658            return ir.getImageIconBounded(new Dimension(virtualMaxWidth, virtualMaxHeight), multiResolution);
659        else
660            return ir.getImageIcon(new Dimension(virtualWidth, virtualHeight), multiResolution);
661    }
662
663    /**
664     * Load the image in a background thread.
665     *
666     * This method returns immediately and runs the image request asynchronously.
667     * @param action the action that will deal with the image
668     *
669     * @return the future of the requested image
670     * @since 13252
671     */
672    public CompletableFuture<Void> getAsync(Consumer<? super ImageIcon> action) {
673        return isRemote()
674                ? CompletableFuture.supplyAsync(this::get, IMAGE_FETCHER).thenAcceptAsync(action)
675                : CompletableFuture.completedFuture(get()).thenAccept(action);
676    }
677
678    /**
679     * Execute the image request.
680     *
681     * @return the requested image or null if the request failed
682     * @since 7693
683     */
684    public ImageResource getResource() {
685        ImageResource ir = getIfAvailableImpl();
686        if (ir == null) {
687            if (!optional) {
688                String ext = name.indexOf('.') != -1 ? "" : ".???";
689                throw new JosmRuntimeException(
690                        tr("Fatal: failed to locate image ''{0}''. This is a serious configuration problem. JOSM will stop working.",
691                                name + ext));
692            } else {
693                if (!suppressWarnings) {
694                    Logging.error(tr("Failed to locate image ''{0}''", name));
695                }
696                return null;
697            }
698        }
699        if (overlayInfo != null) {
700            ir = new ImageResource(ir, overlayInfo);
701        }
702        if (isDisabled) {
703            ir.setDisabled(true);
704        }
705        return ir;
706    }
707
708    /**
709     * Load the image in a background thread.
710     *
711     * This method returns immediately and runs the image request asynchronously.
712     * @param action the action that will deal with the image
713     *
714     * @return the future of the requested image
715     * @since 13252
716     */
717    public CompletableFuture<Void> getResourceAsync(Consumer<? super ImageResource> action) {
718        return isRemote()
719                ? CompletableFuture.supplyAsync(this::getResource, IMAGE_FETCHER).thenAcceptAsync(action)
720                : CompletableFuture.completedFuture(getResource()).thenAccept(action);
721    }
722
723    /**
724     * Load an image with a given file name.
725     *
726     * @param subdir subdirectory the image lies in
727     * @param name The icon name (base name with or without '.png' or '.svg' extension)
728     * @return The requested Image.
729     * @throws RuntimeException if the image cannot be located
730     */
731    public static ImageIcon get(String subdir, String name) {
732        return new ImageProvider(subdir, name).get();
733    }
734
735    /**
736     * Load an image with a given file name.
737     *
738     * @param name The icon name (base name with or without '.png' or '.svg' extension)
739     * @return the requested image or null if the request failed
740     * @see #get(String, String)
741     */
742    public static ImageIcon get(String name) {
743        return new ImageProvider(name).get();
744    }
745
746    /**
747     * Load an image from directory with a given file name and size.
748     *
749     * @param subdir subdirectory the image lies in
750     * @param name The icon name (base name with or without '.png' or '.svg' extension)
751     * @param size Target icon size
752     * @return The requested Image.
753     * @throws RuntimeException if the image cannot be located
754     * @since 10428
755     */
756    public static ImageIcon get(String subdir, String name, ImageSizes size) {
757        return new ImageProvider(subdir, name).setSize(size).get();
758    }
759
760    /**
761     * Load an empty image with a given size.
762     *
763     * @param size Target icon size
764     * @return The requested Image.
765     * @since 10358
766     */
767    public static ImageIcon getEmpty(ImageSizes size) {
768        Dimension iconRealSize = GuiSizesHelper.getDimensionDpiAdjusted(size.getImageDimension());
769        return new ImageIcon(new BufferedImage(iconRealSize.width, iconRealSize.height,
770            BufferedImage.TYPE_INT_ARGB));
771    }
772
773    /**
774     * Load an image with a given file name, but do not throw an exception
775     * when the image cannot be found.
776     *
777     * @param subdir subdirectory the image lies in
778     * @param name The icon name (base name with or without '.png' or '.svg' extension)
779     * @return the requested image or null if the request failed
780     * @see #get(String, String)
781     */
782    public static ImageIcon getIfAvailable(String subdir, String name) {
783        return new ImageProvider(subdir, name).setOptional(true).get();
784    }
785
786    /**
787     * Load an image with a given file name and size.
788     *
789     * @param name The icon name (base name with or without '.png' or '.svg' extension)
790     * @param size Target icon size
791     * @return the requested image or null if the request failed
792     * @see #get(String, String)
793     * @since 10428
794     */
795    public static ImageIcon get(String name, ImageSizes size) {
796        return new ImageProvider(name).setSize(size).get();
797    }
798
799    /**
800     * Load an image with a given file name, but do not throw an exception
801     * when the image cannot be found.
802     *
803     * @param name The icon name (base name with or without '.png' or '.svg' extension)
804     * @return the requested image or null if the request failed
805     * @see #getIfAvailable(String, String)
806     */
807    public static ImageIcon getIfAvailable(String name) {
808        return new ImageProvider(name).setOptional(true).get();
809    }
810
811    /**
812     * {@code data:[<mediatype>][;base64],<data>}
813     * @see <a href="http://tools.ietf.org/html/rfc2397">RFC2397</a>
814     */
815    private static final Pattern dataUrlPattern = Pattern.compile(
816            "^data:([a-zA-Z]+/[a-zA-Z+]+)?(;base64)?,(.+)$");
817
818    /**
819     * Clears the internal image cache.
820     * @since 11021
821     */
822    public static void clearCache() {
823        synchronized (cache) {
824            cache.clear();
825        }
826    }
827
828    /**
829     * Internal implementation of the image request.
830     *
831     * @return the requested image or null if the request failed
832     */
833    private ImageResource getIfAvailableImpl() {
834        synchronized (cache) {
835            // This method is called from different thread and modifying HashMap concurrently can result
836            // for example in loops in map entries (ie freeze when such entry is retrieved)
837            if (name == null)
838                return null;
839
840            String prefix = isDisabled ? "dis:" : "";
841            if (name.startsWith("data:")) {
842                String url = name;
843                ImageResource ir = cache.get(prefix+url);
844                if (ir != null) return ir;
845                ir = getIfAvailableDataUrl(url);
846                if (ir != null) {
847                    cache.put(prefix+url, ir);
848                }
849                return ir;
850            }
851
852            ImageType type = Utils.hasExtension(name, "svg") ? ImageType.SVG : ImageType.OTHER;
853
854            if (name.startsWith(HTTP_PROTOCOL) || name.startsWith(HTTPS_PROTOCOL)) {
855                String url = name;
856                ImageResource ir = cache.get(prefix+url);
857                if (ir != null) return ir;
858                ir = getIfAvailableHttp(url, type);
859                if (ir != null) {
860                    cache.put(prefix+url, ir);
861                }
862                return ir;
863            } else if (name.startsWith(WIKI_PROTOCOL)) {
864                ImageResource ir = cache.get(prefix+name);
865                if (ir != null) return ir;
866                ir = getIfAvailableWiki(name, type);
867                if (ir != null) {
868                    cache.put(prefix+name, ir);
869                }
870                return ir;
871            }
872
873            if (subdir == null) {
874                subdir = "";
875            } else if (!subdir.isEmpty() && !subdir.endsWith("/")) {
876                subdir += '/';
877            }
878            String[] extensions;
879            if (name.indexOf('.') != -1) {
880                extensions = new String[] {""};
881            } else {
882                extensions = new String[] {".png", ".svg"};
883            }
884            final int typeArchive = 0;
885            final int typeLocal = 1;
886            for (int place : new Integer[] {typeArchive, typeLocal}) {
887                for (String ext : extensions) {
888
889                    if (".svg".equals(ext)) {
890                        type = ImageType.SVG;
891                    } else if (".png".equals(ext)) {
892                        type = ImageType.OTHER;
893                    }
894
895                    String fullName = subdir + name + ext;
896                    String cacheName = prefix + fullName;
897                    /* cache separately */
898                    if (dirs != null && !dirs.isEmpty()) {
899                        cacheName = "id:" + id + ':' + fullName;
900                        if (archive != null) {
901                            cacheName += ':' + archive.getName();
902                        }
903                    }
904
905                    switch (place) {
906                    case typeArchive:
907                        if (archive != null) {
908                            cacheName = "zip:"+archive.hashCode()+':'+cacheName;
909                            ImageResource ir = cache.get(cacheName);
910                            if (ir != null) return ir;
911
912                            ir = getIfAvailableZip(fullName, archive, inArchiveDir, type);
913                            if (ir != null) {
914                                cache.put(cacheName, ir);
915                                return ir;
916                            }
917                        }
918                        break;
919                    case typeLocal:
920                        ImageResource ir = cache.get(cacheName);
921                        if (ir != null) return ir;
922
923                        // getImageUrl() does a ton of "stat()" calls and gets expensive
924                        // and redundant when you have a whole ton of objects. So,
925                        // index the cache by the name of the icon we're looking for
926                        // and don't bother to create a URL unless we're actually creating the image.
927                        URL path = getImageUrl(fullName);
928                        if (path == null) {
929                            continue;
930                        }
931                        ir = getIfAvailableLocalURL(path, type);
932                        if (ir != null) {
933                            cache.put(cacheName, ir);
934                            return ir;
935                        }
936                        break;
937                    }
938                }
939            }
940            return null;
941        }
942    }
943
944    /**
945     * Internal implementation of the image request for URL's.
946     *
947     * @param url URL of the image
948     * @param type data type of the image
949     * @return the requested image or null if the request failed
950     */
951    private static ImageResource getIfAvailableHttp(String url, ImageType type) {
952        try (CachedFile cf = new CachedFile(url).setDestDir(
953                new File(Config.getDirs().getCacheDirectory(true), "images").getPath());
954             InputStream is = cf.getInputStream()) {
955            switch (type) {
956            case SVG:
957                SVGDiagram svg = null;
958                synchronized (getSvgUniverse()) {
959                    URI uri = getSvgUniverse().loadSVG(is, Utils.fileToURL(cf.getFile()).toString());
960                    svg = getSvgUniverse().getDiagram(uri);
961                }
962                return svg == null ? null : new ImageResource(svg);
963            case OTHER:
964                BufferedImage img = null;
965                try {
966                    img = read(Utils.fileToURL(cf.getFile()), false, false);
967                } catch (IOException e) {
968                    Logging.log(Logging.LEVEL_WARN, "IOException while reading HTTP image:", e);
969                }
970                return img == null ? null : new ImageResource(img);
971            default:
972                throw new AssertionError("Unsupported type: " + type);
973            }
974        } catch (IOException e) {
975            Logging.debug(e);
976            return null;
977        }
978    }
979
980    /**
981     * Internal implementation of the image request for inline images (<b>data:</b> urls).
982     *
983     * @param url the data URL for image extraction
984     * @return the requested image or null if the request failed
985     */
986    private static ImageResource getIfAvailableDataUrl(String url) {
987        Matcher m = dataUrlPattern.matcher(url);
988        if (m.matches()) {
989            String base64 = m.group(2);
990            String data = m.group(3);
991            byte[] bytes;
992            try {
993                if (";base64".equals(base64)) {
994                    bytes = Base64.getDecoder().decode(data);
995                } else {
996                    bytes = Utils.decodeUrl(data).getBytes(StandardCharsets.UTF_8);
997                }
998            } catch (IllegalArgumentException ex) {
999                Logging.log(Logging.LEVEL_WARN, "Unable to decode URL data part: "+ex.getMessage() + " (" + data + ')', ex);
1000                return null;
1001            }
1002            String mediatype = m.group(1);
1003            if ("image/svg+xml".equals(mediatype)) {
1004                String s = new String(bytes, StandardCharsets.UTF_8);
1005                SVGDiagram svg;
1006                synchronized (getSvgUniverse()) {
1007                    URI uri = getSvgUniverse().loadSVG(new StringReader(s), Utils.encodeUrl(s));
1008                    svg = getSvgUniverse().getDiagram(uri);
1009                }
1010                if (svg == null) {
1011                    Logging.warn("Unable to process svg: "+s);
1012                    return null;
1013                }
1014                return new ImageResource(svg);
1015            } else {
1016                try {
1017                    // See #10479: for PNG files, always enforce transparency to be sure tNRS chunk is used even not in paletted mode
1018                    // This can be removed if someday Oracle fixes https://bugs.openjdk.java.net/browse/JDK-6788458
1019                    // CHECKSTYLE.OFF: LineLength
1020                    // hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/dc4322602480/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java#l656
1021                    // CHECKSTYLE.ON: LineLength
1022                    Image img = read(new ByteArrayInputStream(bytes), false, true);
1023                    return img == null ? null : new ImageResource(img);
1024                } catch (IOException e) {
1025                    Logging.log(Logging.LEVEL_WARN, "IOException while reading image:", e);
1026                }
1027            }
1028        }
1029        return null;
1030    }
1031
1032    /**
1033     * Internal implementation of the image request for wiki images.
1034     *
1035     * @param name image file name
1036     * @param type data type of the image
1037     * @return the requested image or null if the request failed
1038     */
1039    private static ImageResource getIfAvailableWiki(String name, ImageType type) {
1040        final List<String> defaultBaseUrls = Arrays.asList(
1041                "https://wiki.openstreetmap.org/w/images/",
1042                "https://upload.wikimedia.org/wikipedia/commons/",
1043                "https://wiki.openstreetmap.org/wiki/File:"
1044                );
1045        final Collection<String> baseUrls = Config.getPref().getList("image-provider.wiki.urls", defaultBaseUrls);
1046
1047        final String fn = name.substring(name.lastIndexOf('/') + 1);
1048
1049        ImageResource result = null;
1050        for (String b : baseUrls) {
1051            String url;
1052            if (b.endsWith(":")) {
1053                url = getImgUrlFromWikiInfoPage(b, fn);
1054                if (url == null) {
1055                    continue;
1056                }
1057            } else {
1058                final String fnMD5 = Utils.md5Hex(fn);
1059                url = b + fnMD5.substring(0, 1) + '/' + fnMD5.substring(0, 2) + '/' + fn;
1060            }
1061            result = getIfAvailableHttp(url, type);
1062            if (result != null) {
1063                break;
1064            }
1065        }
1066        return result;
1067    }
1068
1069    /**
1070     * Internal implementation of the image request for images in Zip archives.
1071     *
1072     * @param fullName image file name
1073     * @param archive the archive to get image from
1074     * @param inArchiveDir directory of the image inside the archive or <code>null</code>
1075     * @param type data type of the image
1076     * @return the requested image or null if the request failed
1077     */
1078    private static ImageResource getIfAvailableZip(String fullName, File archive, String inArchiveDir, ImageType type) {
1079        try (ZipFile zipFile = new ZipFile(archive, StandardCharsets.UTF_8)) {
1080            if (inArchiveDir == null || ".".equals(inArchiveDir)) {
1081                inArchiveDir = "";
1082            } else if (!inArchiveDir.isEmpty()) {
1083                inArchiveDir += '/';
1084            }
1085            String entryName = inArchiveDir + fullName;
1086            ZipEntry entry = zipFile.getEntry(entryName);
1087            if (entry != null) {
1088                int size = (int) entry.getSize();
1089                int offs = 0;
1090                byte[] buf = new byte[size];
1091                try (InputStream is = zipFile.getInputStream(entry)) {
1092                    switch (type) {
1093                    case SVG:
1094                        SVGDiagram svg = null;
1095                        synchronized (getSvgUniverse()) {
1096                            URI uri = getSvgUniverse().loadSVG(is, entryName);
1097                            svg = getSvgUniverse().getDiagram(uri);
1098                        }
1099                        return svg == null ? null : new ImageResource(svg);
1100                    case OTHER:
1101                        while (size > 0) {
1102                            int l = is.read(buf, offs, size);
1103                            offs += l;
1104                            size -= l;
1105                        }
1106                        BufferedImage img = null;
1107                        try {
1108                            img = read(new ByteArrayInputStream(buf), false, false);
1109                        } catch (IOException e) {
1110                            Logging.warn(e);
1111                        }
1112                        return img == null ? null : new ImageResource(img);
1113                    default:
1114                        throw new AssertionError("Unknown ImageType: "+type);
1115                    }
1116                }
1117            }
1118        } catch (IOException e) {
1119            Logging.log(Logging.LEVEL_WARN, tr("Failed to handle zip file ''{0}''. Exception was: {1}", archive.getName(), e.toString()), e);
1120        }
1121        return null;
1122    }
1123
1124    /**
1125     * Internal implementation of the image request for local images.
1126     *
1127     * @param path image file path
1128     * @param type data type of the image
1129     * @return the requested image or null if the request failed
1130     */
1131    private static ImageResource getIfAvailableLocalURL(URL path, ImageType type) {
1132        switch (type) {
1133        case SVG:
1134            SVGDiagram svg;
1135            synchronized (getSvgUniverse()) {
1136                URI uri = getSvgUniverse().loadSVG(path);
1137                svg = getSvgUniverse().getDiagram(uri);
1138            }
1139            return svg == null ? null : new ImageResource(svg);
1140        case OTHER:
1141            BufferedImage img = null;
1142            try {
1143                // See #10479: for PNG files, always enforce transparency to be sure tNRS chunk is used even not in paletted mode
1144                // This can be removed if someday Oracle fixes https://bugs.openjdk.java.net/browse/JDK-6788458
1145                // hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/dc4322602480/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java#l656
1146                img = read(path, false, true);
1147                if (Logging.isDebugEnabled() && isTransparencyForced(img)) {
1148                    Logging.debug("Transparency has been forced for image {0}", path);
1149                }
1150            } catch (IOException e) {
1151                Logging.warn(e);
1152            }
1153            return img == null ? null : new ImageResource(img);
1154        default:
1155            throw new AssertionError();
1156        }
1157    }
1158
1159    private URL getImageUrl(String path, String name) {
1160        if (path != null && path.startsWith("resource://")) {
1161            String p = path.substring("resource://".length());
1162            for (ClassLoader source : classLoaders) {
1163                URL res;
1164                if ((res = source.getResource(p + name)) != null)
1165                    return res;
1166            }
1167        } else {
1168            File f = new File(path, name);
1169            if ((path != null || f.isAbsolute()) && f.exists())
1170                return Utils.fileToURL(f);
1171        }
1172        return null;
1173    }
1174
1175    private URL getImageUrl(String imageName) {
1176        URL u;
1177
1178        // Try passed directories first
1179        if (dirs != null) {
1180            for (String name : dirs) {
1181                try {
1182                    u = getImageUrl(name, imageName);
1183                    if (u != null)
1184                        return u;
1185                } catch (SecurityException e) {
1186                    Logging.log(Logging.LEVEL_WARN, tr(
1187                            "Failed to access directory ''{0}'' for security reasons. Exception was: {1}",
1188                            name, e.toString()), e);
1189                }
1190
1191            }
1192        }
1193        // Try user-data directory
1194        if (Config.getDirs() != null) {
1195            String dir = new File(Config.getDirs().getUserDataDirectory(false), "images").getAbsolutePath();
1196            try {
1197                u = getImageUrl(dir, imageName);
1198                if (u != null)
1199                    return u;
1200            } catch (SecurityException e) {
1201                Logging.log(Logging.LEVEL_WARN, tr(
1202                        "Failed to access directory ''{0}'' for security reasons. Exception was: {1}", dir, e
1203                        .toString()), e);
1204            }
1205        }
1206
1207        // Absolute path?
1208        u = getImageUrl(null, imageName);
1209        if (u != null)
1210            return u;
1211
1212        // Try plugins and josm classloader
1213        u = getImageUrl("resource://images/", imageName);
1214        if (u != null)
1215            return u;
1216
1217        // Try all other resource directories
1218        if (Main.pref != null) {
1219            for (String location : Main.pref.getAllPossiblePreferenceDirs()) {
1220                u = getImageUrl(location + "images", imageName);
1221                if (u != null)
1222                    return u;
1223                u = getImageUrl(location, imageName);
1224                if (u != null)
1225                    return u;
1226            }
1227        }
1228
1229        return null;
1230    }
1231
1232    /**
1233     * Reads the wiki page on a certain file in html format in order to find the real image URL.
1234     *
1235     * @param base base URL for Wiki image
1236     * @param fn filename of the Wiki image
1237     * @return image URL for a Wiki image or null in case of error
1238     */
1239    private static String getImgUrlFromWikiInfoPage(final String base, final String fn) {
1240        try {
1241            final XMLReader parser = Utils.newSafeSAXParser().getXMLReader();
1242            parser.setContentHandler(new DefaultHandler() {
1243                @Override
1244                public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
1245                    if ("img".equalsIgnoreCase(localName)) {
1246                        String val = atts.getValue("src");
1247                        if (val.endsWith(fn))
1248                            throw new SAXReturnException(val);  // parsing done, quit early
1249                    }
1250                }
1251            });
1252
1253            parser.setEntityResolver((publicId, systemId) -> new InputSource(new ByteArrayInputStream(new byte[0])));
1254
1255            try (CachedFile cf = new CachedFile(base + fn).setDestDir(
1256                        new File(Config.getDirs().getUserDataDirectory(true), "images").getPath());
1257                 InputStream is = cf.getInputStream()) {
1258                parser.parse(new InputSource(is));
1259            }
1260        } catch (SAXReturnException e) {
1261            Logging.trace(e);
1262            return e.getResult();
1263        } catch (IOException | SAXException | ParserConfigurationException e) {
1264            Logging.warn("Parsing " + base + fn + " failed:\n" + e);
1265            return null;
1266        }
1267        Logging.warn("Parsing " + base + fn + " failed: Unexpected content.");
1268        return null;
1269    }
1270
1271    /**
1272     * Load a cursor with a given file name, optionally decorated with an overlay image.
1273     *
1274     * @param name the cursor image filename in "cursor" directory
1275     * @param overlay optional overlay image
1276     * @return cursor with a given file name, optionally decorated with an overlay image
1277     */
1278    public static Cursor getCursor(String name, String overlay) {
1279        ImageIcon img = get("cursor", name);
1280        if (overlay != null) {
1281            img = new ImageProvider("cursor", name).setMaxSize(ImageSizes.CURSOR)
1282                .addOverlay(new ImageOverlay(new ImageProvider("cursor/modifier/" + overlay)
1283                    .setMaxSize(ImageSizes.CURSOROVERLAY))).get();
1284        }
1285        if (GraphicsEnvironment.isHeadless()) {
1286            Logging.debug("Cursors are not available in headless mode. Returning null for '{0}'", name);
1287            return null;
1288        }
1289        return Toolkit.getDefaultToolkit().createCustomCursor(img.getImage(),
1290                "crosshair".equals(name) ? new Point(10, 10) : new Point(3, 2), "Cursor");
1291    }
1292
1293    /** 90 degrees in radians units */
1294    private static final double DEGREE_90 = 90.0 * Math.PI / 180.0;
1295
1296    /**
1297     * Creates a rotated version of the input image.
1298     *
1299     * @param img the image to be rotated.
1300     * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we
1301     * will mod it with 360 before using it. More over for caching performance, it will be rounded to
1302     * an entire value between 0 and 360.
1303     *
1304     * @return the image after rotating.
1305     * @since 6172
1306     */
1307    public static Image createRotatedImage(Image img, double rotatedAngle) {
1308        return createRotatedImage(img, rotatedAngle, ImageResource.DEFAULT_DIMENSION);
1309    }
1310
1311    /**
1312     * Creates a rotated version of the input image.
1313     *
1314     * @param img the image to be rotated.
1315     * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we
1316     * will mod it with 360 before using it. More over for caching performance, it will be rounded to
1317     * an entire value between 0 and 360.
1318     * @param dimension ignored
1319     * @return the image after rotating and scaling.
1320     * @since 6172
1321     */
1322    public static Image createRotatedImage(Image img, double rotatedAngle, Dimension dimension) {
1323        CheckParameterUtil.ensureParameterNotNull(img, "img");
1324
1325        // convert rotatedAngle to an integer value from 0 to 360
1326        Long angleLong = Math.round(rotatedAngle % 360);
1327        Long originalAngle = rotatedAngle != 0 && angleLong == 0 ? Long.valueOf(360L) : angleLong;
1328
1329        synchronized (ROTATE_CACHE) {
1330            Map<Long, Image> cacheByAngle = ROTATE_CACHE.computeIfAbsent(img, k -> new HashMap<>());
1331            Image rotatedImg = cacheByAngle.get(originalAngle);
1332
1333            if (rotatedImg == null) {
1334                // convert originalAngle to a value from 0 to 90
1335                double angle = originalAngle % 90;
1336                if (originalAngle != 0 && angle == 0) {
1337                    angle = 90.0;
1338                }
1339                double radian = Utils.toRadians(angle);
1340
1341                rotatedImg = HiDPISupport.processMRImage(img, img0 -> {
1342                    new ImageIcon(img0); // load completely
1343                    int iw = img0.getWidth(null);
1344                    int ih = img0.getHeight(null);
1345                    int w;
1346                    int h;
1347
1348                    if ((originalAngle >= 0 && originalAngle <= 90) || (originalAngle > 180 && originalAngle <= 270)) {
1349                        w = (int) (iw * Math.sin(DEGREE_90 - radian) + ih * Math.sin(radian));
1350                        h = (int) (iw * Math.sin(radian) + ih * Math.sin(DEGREE_90 - radian));
1351                    } else {
1352                        w = (int) (ih * Math.sin(DEGREE_90 - radian) + iw * Math.sin(radian));
1353                        h = (int) (ih * Math.sin(radian) + iw * Math.sin(DEGREE_90 - radian));
1354                    }
1355                    Image image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1356                    Graphics g = image.getGraphics();
1357                    Graphics2D g2d = (Graphics2D) g.create();
1358
1359                    // calculate the center of the icon.
1360                    int cx = iw / 2;
1361                    int cy = ih / 2;
1362
1363                    // move the graphics center point to the center of the icon.
1364                    g2d.translate(w / 2, h / 2);
1365
1366                    // rotate the graphics about the center point of the icon
1367                    g2d.rotate(Utils.toRadians(originalAngle));
1368
1369                    g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
1370                    g2d.drawImage(img0, -cx, -cy, null);
1371
1372                    g2d.dispose();
1373                    new ImageIcon(image); // load completely
1374                    return image;
1375                });
1376                cacheByAngle.put(originalAngle, rotatedImg);
1377            }
1378            return rotatedImg;
1379        }
1380    }
1381
1382    /**
1383     * Creates a scaled down version of the input image to fit maximum dimensions. (Keeps aspect ratio)
1384     *
1385     * @param img the image to be scaled down.
1386     * @param maxSize the maximum size in pixels (both for width and height)
1387     *
1388     * @return the image after scaling.
1389     * @since 6172
1390     */
1391    public static Image createBoundedImage(Image img, int maxSize) {
1392        return new ImageResource(img).getImageIconBounded(new Dimension(maxSize, maxSize)).getImage();
1393    }
1394
1395    /**
1396     * Returns a scaled instance of the provided {@code BufferedImage}.
1397     * This method will use a multi-step scaling technique that provides higher quality than the usual
1398     * one-step technique (only useful in downscaling cases, where {@code targetWidth} or {@code targetHeight} is
1399     * smaller than the original dimensions, and generally only when the {@code BILINEAR} hint is specified).
1400     *
1401     * From https://community.oracle.com/docs/DOC-983611: "The Perils of Image.getScaledInstance()"
1402     *
1403     * @param img the original image to be scaled
1404     * @param targetWidth the desired width of the scaled instance, in pixels
1405     * @param targetHeight the desired height of the scaled instance, in pixels
1406     * @param hint one of the rendering hints that corresponds to
1407     * {@code RenderingHints.KEY_INTERPOLATION} (e.g.
1408     * {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},
1409     * {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},
1410     * {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})
1411     * @return a scaled version of the original {@code BufferedImage}
1412     * @since 13038
1413     */
1414    public static BufferedImage createScaledImage(BufferedImage img, int targetWidth, int targetHeight, Object hint) {
1415        int type = (img.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
1416        // start with original size, then scale down in multiple passes with drawImage() until the target size is reached
1417        BufferedImage ret = img;
1418        int w = img.getWidth(null);
1419        int h = img.getHeight(null);
1420        do {
1421            if (w > targetWidth) {
1422                w /= 2;
1423            }
1424            if (w < targetWidth) {
1425                w = targetWidth;
1426            }
1427            if (h > targetHeight) {
1428                h /= 2;
1429            }
1430            if (h < targetHeight) {
1431                h = targetHeight;
1432            }
1433            BufferedImage tmp = new BufferedImage(w, h, type);
1434            Graphics2D g2 = tmp.createGraphics();
1435            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
1436            g2.drawImage(ret, 0, 0, w, h, null);
1437            g2.dispose();
1438            ret = tmp;
1439        } while (w != targetWidth || h != targetHeight);
1440        return ret;
1441    }
1442
1443    /**
1444     * Replies the icon for an OSM primitive type
1445     * @param type the type
1446     * @return the icon
1447     */
1448    public static ImageIcon get(OsmPrimitiveType type) {
1449        CheckParameterUtil.ensureParameterNotNull(type, "type");
1450        return get("data", type.getAPIName());
1451    }
1452
1453    /**
1454     * @param primitive Object for which an icon shall be fetched. The icon is chosen based on tags.
1455     * @param iconSize Target size of icon. Icon is padded if required.
1456     * @return Icon for {@code primitive} that fits in cell.
1457     * @since 8903
1458     */
1459    public static ImageIcon getPadded(OsmPrimitive primitive, Dimension iconSize) {
1460        // Check if the current styles have special icon for tagged nodes.
1461        if (primitive instanceof org.openstreetmap.josm.data.osm.Node) {
1462            Pair<StyleElementList, Range> nodeStyles;
1463            DataSet ds = primitive.getDataSet();
1464            if (ds != null) {
1465                ds.getReadLock().lock();
1466            }
1467            try {
1468                nodeStyles = MapPaintStyles.getStyles().generateStyles(primitive, 100, false);
1469            } finally {
1470                if (ds != null) {
1471                    ds.getReadLock().unlock();
1472                }
1473            }
1474            for (StyleElement style : nodeStyles.a) {
1475                if (style instanceof NodeElement) {
1476                    NodeElement nodeStyle = (NodeElement) style;
1477                    MapImage icon = nodeStyle.mapImage;
1478                    if (icon != null) {
1479                        int backgroundRealWidth = GuiSizesHelper.getSizeDpiAdjusted(iconSize.width);
1480                        int backgroundRealHeight = GuiSizesHelper.getSizeDpiAdjusted(iconSize.height);
1481                        int iconRealWidth = icon.getWidth();
1482                        int iconRealHeight = icon.getHeight();
1483                        BufferedImage image = new BufferedImage(backgroundRealWidth, backgroundRealHeight,
1484                                BufferedImage.TYPE_INT_ARGB);
1485                        double scaleFactor = Math.min(backgroundRealWidth / (double) iconRealWidth, backgroundRealHeight
1486                                / (double) iconRealHeight);
1487                        Image iconImage = icon.getImage(false);
1488                        Image scaledIcon;
1489                        final int scaledWidth;
1490                        final int scaledHeight;
1491                        if (scaleFactor < 1) {
1492                            // Scale icon such that it fits on background.
1493                            scaledWidth = (int) (iconRealWidth * scaleFactor);
1494                            scaledHeight = (int) (iconRealHeight * scaleFactor);
1495                            scaledIcon = iconImage.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH);
1496                        } else {
1497                            // Use original size, don't upscale.
1498                            scaledWidth = iconRealWidth;
1499                            scaledHeight = iconRealHeight;
1500                            scaledIcon = iconImage;
1501                        }
1502                        image.getGraphics().drawImage(scaledIcon, (backgroundRealWidth - scaledWidth) / 2,
1503                                (backgroundRealHeight - scaledHeight) / 2, null);
1504
1505                        return new ImageIcon(image);
1506                    }
1507                }
1508            }
1509        }
1510
1511        // Check if the presets have icons for nodes/relations.
1512        if (!OsmPrimitiveType.WAY.equals(primitive.getType())) {
1513            final Collection<TaggingPreset> presets = new TreeSet<>((o1, o2) -> {
1514                final int o1TypesSize = o1.types == null || o1.types.isEmpty() ? Integer.MAX_VALUE : o1.types.size();
1515                final int o2TypesSize = o2.types == null || o2.types.isEmpty() ? Integer.MAX_VALUE : o2.types.size();
1516                return Integer.compare(o1TypesSize, o2TypesSize);
1517            });
1518            presets.addAll(TaggingPresets.getMatchingPresets(primitive));
1519            for (final TaggingPreset preset : presets) {
1520                if (preset.getIcon() != null) {
1521                    return preset.getIcon();
1522                }
1523            }
1524        }
1525
1526        // Use generic default icon.
1527        return ImageProvider.get(primitive.getDisplayType());
1528    }
1529
1530    /**
1531     * Constructs an image from the given SVG data.
1532     * @param svg the SVG data
1533     * @param dim the desired image dimension
1534     * @return an image from the given SVG data at the desired dimension.
1535     */
1536    public static BufferedImage createImageFromSvg(SVGDiagram svg, Dimension dim) {
1537        if (Logging.isTraceEnabled()) {
1538            Logging.trace("createImageFromSvg: {0} {1}", svg.getXMLBase(), dim);
1539        }
1540        float sourceWidth = svg.getWidth();
1541        float sourceHeight = svg.getHeight();
1542        int realWidth = Math.round(GuiSizesHelper.getSizeDpiAdjusted(sourceWidth));
1543        int realHeight = Math.round(GuiSizesHelper.getSizeDpiAdjusted(sourceHeight));
1544        Double scaleX, scaleY;
1545        if (dim.width != -1) {
1546            realWidth = dim.width;
1547            scaleX = (double) realWidth / sourceWidth;
1548            if (dim.height == -1) {
1549                scaleY = scaleX;
1550                realHeight = (int) Math.round(sourceHeight * scaleY);
1551            } else {
1552                realHeight = dim.height;
1553                scaleY = (double) realHeight / sourceHeight;
1554            }
1555        } else if (dim.height != -1) {
1556            realHeight = dim.height;
1557            scaleX = scaleY = (double) realHeight / sourceHeight;
1558            realWidth = (int) Math.round(sourceWidth * scaleX);
1559        } else {
1560            scaleX = scaleY = (double) realHeight / sourceHeight;
1561        }
1562
1563        if (realWidth == 0 || realHeight == 0) {
1564            return null;
1565        }
1566        BufferedImage img = new BufferedImage(realWidth, realHeight, BufferedImage.TYPE_INT_ARGB);
1567        Graphics2D g = img.createGraphics();
1568        g.setClip(0, 0, realWidth, realHeight);
1569        g.scale(scaleX, scaleY);
1570        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1571        try {
1572            synchronized (getSvgUniverse()) {
1573                svg.render(g);
1574            }
1575        } catch (SVGException ex) {
1576            Logging.log(Logging.LEVEL_ERROR, "Unable to load svg:", ex);
1577            return null;
1578        }
1579        return img;
1580    }
1581
1582    private static synchronized SVGUniverse getSvgUniverse() {
1583        if (svgUniverse == null) {
1584            svgUniverse = new SVGUniverse();
1585        }
1586        return svgUniverse;
1587    }
1588
1589    /**
1590     * Returns a <code>BufferedImage</code> as the result of decoding
1591     * a supplied <code>File</code> with an <code>ImageReader</code>
1592     * chosen automatically from among those currently registered.
1593     * The <code>File</code> is wrapped in an
1594     * <code>ImageInputStream</code>.  If no registered
1595     * <code>ImageReader</code> claims to be able to read the
1596     * resulting stream, <code>null</code> is returned.
1597     *
1598     * <p> The current cache settings from <code>getUseCache</code>and
1599     * <code>getCacheDirectory</code> will be used to control caching in the
1600     * <code>ImageInputStream</code> that is created.
1601     *
1602     * <p> Note that there is no <code>read</code> method that takes a
1603     * filename as a <code>String</code>; use this method instead after
1604     * creating a <code>File</code> from the filename.
1605     *
1606     * <p> This method does not attempt to locate
1607     * <code>ImageReader</code>s that can read directly from a
1608     * <code>File</code>; that may be accomplished using
1609     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
1610     *
1611     * @param input a <code>File</code> to read from.
1612     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color, if any.
1613     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1614     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1615     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1616     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1617     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1618     *
1619     * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
1620     *
1621     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
1622     * @throws IOException if an error occurs during reading.
1623     * @see BufferedImage#getProperty
1624     * @since 7132
1625     */
1626    public static BufferedImage read(File input, boolean readMetadata, boolean enforceTransparency) throws IOException {
1627        CheckParameterUtil.ensureParameterNotNull(input, "input");
1628        if (!input.canRead()) {
1629            throw new IIOException("Can't read input file!");
1630        }
1631
1632        ImageInputStream stream = ImageIO.createImageInputStream(input);
1633        if (stream == null) {
1634            throw new IIOException("Can't create an ImageInputStream!");
1635        }
1636        BufferedImage bi = read(stream, readMetadata, enforceTransparency);
1637        if (bi == null) {
1638            stream.close();
1639        }
1640        return bi;
1641    }
1642
1643    /**
1644     * Returns a <code>BufferedImage</code> as the result of decoding
1645     * a supplied <code>InputStream</code> with an <code>ImageReader</code>
1646     * chosen automatically from among those currently registered.
1647     * The <code>InputStream</code> is wrapped in an
1648     * <code>ImageInputStream</code>.  If no registered
1649     * <code>ImageReader</code> claims to be able to read the
1650     * resulting stream, <code>null</code> is returned.
1651     *
1652     * <p> The current cache settings from <code>getUseCache</code>and
1653     * <code>getCacheDirectory</code> will be used to control caching in the
1654     * <code>ImageInputStream</code> that is created.
1655     *
1656     * <p> This method does not attempt to locate
1657     * <code>ImageReader</code>s that can read directly from an
1658     * <code>InputStream</code>; that may be accomplished using
1659     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
1660     *
1661     * <p> This method <em>does not</em> close the provided
1662     * <code>InputStream</code> after the read operation has completed;
1663     * it is the responsibility of the caller to close the stream, if desired.
1664     *
1665     * @param input an <code>InputStream</code> to read from.
1666     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images, if any.
1667     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1668     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1669     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1670     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1671     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1672     *
1673     * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
1674     *
1675     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
1676     * @throws IOException if an error occurs during reading.
1677     * @since 7132
1678     */
1679    public static BufferedImage read(InputStream input, boolean readMetadata, boolean enforceTransparency) throws IOException {
1680        CheckParameterUtil.ensureParameterNotNull(input, "input");
1681
1682        ImageInputStream stream = ImageIO.createImageInputStream(input);
1683        BufferedImage bi = read(stream, readMetadata, enforceTransparency);
1684        if (bi == null) {
1685            stream.close();
1686        }
1687        return bi;
1688    }
1689
1690    /**
1691     * Returns a <code>BufferedImage</code> as the result of decoding
1692     * a supplied <code>URL</code> with an <code>ImageReader</code>
1693     * chosen automatically from among those currently registered.  An
1694     * <code>InputStream</code> is obtained from the <code>URL</code>,
1695     * which is wrapped in an <code>ImageInputStream</code>.  If no
1696     * registered <code>ImageReader</code> claims to be able to read
1697     * the resulting stream, <code>null</code> is returned.
1698     *
1699     * <p> The current cache settings from <code>getUseCache</code>and
1700     * <code>getCacheDirectory</code> will be used to control caching in the
1701     * <code>ImageInputStream</code> that is created.
1702     *
1703     * <p> This method does not attempt to locate
1704     * <code>ImageReader</code>s that can read directly from a
1705     * <code>URL</code>; that may be accomplished using
1706     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
1707     *
1708     * @param input a <code>URL</code> to read from.
1709     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images, if any.
1710     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1711     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1712     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1713     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1714     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1715     *
1716     * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
1717     *
1718     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
1719     * @throws IOException if an error occurs during reading.
1720     * @since 7132
1721     */
1722    public static BufferedImage read(URL input, boolean readMetadata, boolean enforceTransparency) throws IOException {
1723        CheckParameterUtil.ensureParameterNotNull(input, "input");
1724
1725        try (InputStream istream = Utils.openStream(input)) {
1726            ImageInputStream stream = ImageIO.createImageInputStream(istream);
1727            BufferedImage bi = read(stream, readMetadata, enforceTransparency);
1728            if (bi == null) {
1729                stream.close();
1730            }
1731            return bi;
1732        } catch (IOException e) {
1733            throw new IIOException("Can't get input stream from URL!", e);
1734        }
1735    }
1736
1737    /**
1738     * Returns a <code>BufferedImage</code> as the result of decoding
1739     * a supplied <code>ImageInputStream</code> with an
1740     * <code>ImageReader</code> chosen automatically from among those
1741     * currently registered.  If no registered
1742     * <code>ImageReader</code> claims to be able to read the stream,
1743     * <code>null</code> is returned.
1744     *
1745     * <p> Unlike most other methods in this class, this method <em>does</em>
1746     * close the provided <code>ImageInputStream</code> after the read
1747     * operation has completed, unless <code>null</code> is returned,
1748     * in which case this method <em>does not</em> close the stream.
1749     *
1750     * @param stream an <code>ImageInputStream</code> to read from.
1751     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images, if any.
1752     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1753     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1754     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1755     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1756     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1757     *
1758     * @return a <code>BufferedImage</code> containing the decoded
1759     * contents of the input, or <code>null</code>.
1760     *
1761     * @throws IllegalArgumentException if <code>stream</code> is <code>null</code>.
1762     * @throws IOException if an error occurs during reading.
1763     * @since 7132
1764     */
1765    public static BufferedImage read(ImageInputStream stream, boolean readMetadata, boolean enforceTransparency) throws IOException {
1766        CheckParameterUtil.ensureParameterNotNull(stream, "stream");
1767
1768        Iterator<ImageReader> iter = ImageIO.getImageReaders(stream);
1769        if (!iter.hasNext()) {
1770            return null;
1771        }
1772
1773        ImageReader reader = iter.next();
1774        ImageReadParam param = reader.getDefaultReadParam();
1775        reader.setInput(stream, true, !readMetadata && !enforceTransparency);
1776        BufferedImage bi = null;
1777        try {
1778            bi = reader.read(0, param);
1779            if (bi.getTransparency() != Transparency.TRANSLUCENT && (readMetadata || enforceTransparency)) {
1780                Color color = getTransparentColor(bi.getColorModel(), reader);
1781                if (color != null) {
1782                    Hashtable<String, Object> properties = new Hashtable<>(1);
1783                    properties.put(PROP_TRANSPARENCY_COLOR, color);
1784                    bi = new BufferedImage(bi.getColorModel(), bi.getRaster(), bi.isAlphaPremultiplied(), properties);
1785                    if (enforceTransparency) {
1786                        Logging.trace("Enforcing image transparency of {0} for {1}", stream, color);
1787                        bi = makeImageTransparent(bi, color);
1788                    }
1789                }
1790            }
1791        } catch (LinkageError e) {
1792            // On Windows, ComponentColorModel.getRGBComponent can fail with "UnsatisfiedLinkError: no awt in java.library.path", see #13973
1793            // Then it can leads to "NoClassDefFoundError: Could not initialize class sun.awt.image.ShortInterleavedRaster", see #15079
1794            Logging.error(e);
1795        } finally {
1796            reader.dispose();
1797            stream.close();
1798        }
1799        return bi;
1800    }
1801
1802    // CHECKSTYLE.OFF: LineLength
1803
1804    /**
1805     * Returns the {@code TransparentColor} defined in image reader metadata.
1806     * @param model The image color model
1807     * @param reader The image reader
1808     * @return the {@code TransparentColor} defined in image reader metadata, or {@code null}
1809     * @throws IOException if an error occurs during reading
1810     * @see <a href="http://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/standard_metadata.html">javax_imageio_1.0 metadata</a>
1811     * @since 7499
1812     */
1813    public static Color getTransparentColor(ColorModel model, ImageReader reader) throws IOException {
1814        // CHECKSTYLE.ON: LineLength
1815        try {
1816            IIOMetadata metadata = reader.getImageMetadata(0);
1817            if (metadata != null) {
1818                String[] formats = metadata.getMetadataFormatNames();
1819                if (formats != null) {
1820                    for (String f : formats) {
1821                        if ("javax_imageio_1.0".equals(f)) {
1822                            Node root = metadata.getAsTree(f);
1823                            if (root instanceof Element) {
1824                                NodeList list = ((Element) root).getElementsByTagName("TransparentColor");
1825                                if (list.getLength() > 0) {
1826                                    Node item = list.item(0);
1827                                    if (item instanceof Element) {
1828                                        // Handle different color spaces (tested with RGB and grayscale)
1829                                        String value = ((Element) item).getAttribute("value");
1830                                        if (!value.isEmpty()) {
1831                                            String[] s = value.split(" ");
1832                                            if (s.length == 3) {
1833                                                return parseRGB(s);
1834                                            } else if (s.length == 1) {
1835                                                int pixel = Integer.parseInt(s[0]);
1836                                                int r = model.getRed(pixel);
1837                                                int g = model.getGreen(pixel);
1838                                                int b = model.getBlue(pixel);
1839                                                return new Color(r, g, b);
1840                                            } else {
1841                                                Logging.warn("Unable to translate TransparentColor '"+value+"' with color model "+model);
1842                                            }
1843                                        }
1844                                    }
1845                                }
1846                            }
1847                            break;
1848                        }
1849                    }
1850                }
1851            }
1852        } catch (IIOException | NumberFormatException e) {
1853            // JAI doesn't like some JPEG files with error "Inconsistent metadata read from stream" (see #10267)
1854            Logging.warn(e);
1855        }
1856        return null;
1857    }
1858
1859    private static Color parseRGB(String... s) {
1860        int[] rgb = new int[3];
1861        try {
1862            for (int i = 0; i < 3; i++) {
1863                rgb[i] = Integer.parseInt(s[i]);
1864            }
1865            return new Color(rgb[0], rgb[1], rgb[2]);
1866        } catch (IllegalArgumentException e) {
1867            Logging.error(e);
1868            return null;
1869        }
1870    }
1871
1872    /**
1873     * Returns a transparent version of the given image, based on the given transparent color.
1874     * @param bi The image to convert
1875     * @param color The transparent color
1876     * @return The same image as {@code bi} where all pixels of the given color are transparent.
1877     * This resulting image has also the special property {@link #PROP_TRANSPARENCY_FORCED} set to {@code color}
1878     * @see BufferedImage#getProperty
1879     * @see #isTransparencyForced
1880     * @since 7132
1881     */
1882    public static BufferedImage makeImageTransparent(BufferedImage bi, Color color) {
1883        // the color we are looking for. Alpha bits are set to opaque
1884        final int markerRGB = color.getRGB() | 0xFF000000;
1885        ImageFilter filter = new RGBImageFilter() {
1886            @Override
1887            public int filterRGB(int x, int y, int rgb) {
1888                if ((rgb | 0xFF000000) == markerRGB) {
1889                   // Mark the alpha bits as zero - transparent
1890                   return 0x00FFFFFF & rgb;
1891                } else {
1892                   return rgb;
1893                }
1894            }
1895        };
1896        ImageProducer ip = new FilteredImageSource(bi.getSource(), filter);
1897        Image img = Toolkit.getDefaultToolkit().createImage(ip);
1898        ColorModel colorModel = ColorModel.getRGBdefault();
1899        WritableRaster raster = colorModel.createCompatibleWritableRaster(img.getWidth(null), img.getHeight(null));
1900        String[] names = bi.getPropertyNames();
1901        Hashtable<String, Object> properties = new Hashtable<>(1 + (names != null ? names.length : 0));
1902        if (names != null) {
1903            for (String name : names) {
1904                properties.put(name, bi.getProperty(name));
1905            }
1906        }
1907        properties.put(PROP_TRANSPARENCY_FORCED, Boolean.TRUE);
1908        BufferedImage result = new BufferedImage(colorModel, raster, false, properties);
1909        Graphics2D g2 = result.createGraphics();
1910        g2.drawImage(img, 0, 0, null);
1911        g2.dispose();
1912        return result;
1913    }
1914
1915    /**
1916     * Determines if the transparency of the given {@code BufferedImage} has been enforced by a previous call to {@link #makeImageTransparent}.
1917     * @param bi The {@code BufferedImage} to test
1918     * @return {@code true} if the transparency of {@code bi} has been enforced by a previous call to {@code makeImageTransparent}.
1919     * @see #makeImageTransparent
1920     * @since 7132
1921     */
1922    public static boolean isTransparencyForced(BufferedImage bi) {
1923        return bi != null && !bi.getProperty(PROP_TRANSPARENCY_FORCED).equals(Image.UndefinedProperty);
1924    }
1925
1926    /**
1927     * Determines if the given {@code BufferedImage} has a transparent color determined by a previous call to {@link #read}.
1928     * @param bi The {@code BufferedImage} to test
1929     * @return {@code true} if {@code bi} has a transparent color determined by a previous call to {@code read}.
1930     * @see #read
1931     * @since 7132
1932     */
1933    public static boolean hasTransparentColor(BufferedImage bi) {
1934        return bi != null && !bi.getProperty(PROP_TRANSPARENCY_COLOR).equals(Image.UndefinedProperty);
1935    }
1936
1937    /**
1938     * Shutdown background image fetcher.
1939     * @param now if {@code true}, attempts to stop all actively executing tasks, halts the processing of waiting tasks.
1940     * if {@code false}, initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted
1941     * @since 8412
1942     */
1943    public static void shutdown(boolean now) {
1944        if (now) {
1945            IMAGE_FETCHER.shutdownNow();
1946        } else {
1947            IMAGE_FETCHER.shutdown();
1948        }
1949    }
1950
1951    /**
1952     * Converts an {@link Image} to a {@link BufferedImage} instance.
1953     * @param image image to convert
1954     * @return a {@code BufferedImage} instance for the given {@code Image}.
1955     * @since 13038
1956     */
1957    public static BufferedImage toBufferedImage(Image image) {
1958        if (image instanceof BufferedImage) {
1959            return (BufferedImage) image;
1960        } else {
1961            BufferedImage buffImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
1962            Graphics2D g2 = buffImage.createGraphics();
1963            g2.drawImage(image, 0, 0, null);
1964            g2.dispose();
1965            return buffImage;
1966        }
1967    }
1968
1969    /**
1970     * Converts an {@link Rectangle} area of {@link Image} to a {@link BufferedImage} instance.
1971     * @param image image to convert
1972     * @param cropArea rectangle to crop image with
1973     * @return a {@code BufferedImage} instance for the cropped area of {@code Image}.
1974     * @since 13127
1975     */
1976    public static BufferedImage toBufferedImage(Image image, Rectangle cropArea) {
1977        BufferedImage buffImage = null;
1978        Rectangle r = new Rectangle(image.getWidth(null), image.getHeight(null));
1979        if (r.intersection(cropArea).equals(cropArea)) {
1980            buffImage = new BufferedImage(cropArea.width, cropArea.height, BufferedImage.TYPE_INT_ARGB);
1981            Graphics2D g2 = buffImage.createGraphics();
1982            g2.drawImage(image, 0, 0, cropArea.width, cropArea.height,
1983                cropArea.x, cropArea.y, cropArea.x + cropArea.width, cropArea.y + cropArea.height, null);
1984            g2.dispose();
1985        }
1986        return buffImage;
1987    }
1988}