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