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