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