source: josm/trunk/src/org/openstreetmap/josm/tools/ImageProvider.java @ 13869

Last change on this file since 13869 was 13869, checked in by Don-vip, 7 months ago

spotbugs - MS_MUTABLE_COLLECTION_PKGPROTECT

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