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