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

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

see #15476, fix #15511 - fix image scaling regression and makes geoimage feature more configurable through prefs (adjustable max zoom, zoom-step, click zooming with mouse buttons (e.g. if a mouse wheel is not present). Patch by cmuelle8

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