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

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

see #15476 - fix checkstyle/pmd warnings, add some javadoc

  • Property svn:eol-style set to native
File size: 78.9 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 image is rendered instead.
620     * <p>
621     * Use {@link HiDPISupport#getBaseImage(java.awt.Image)} to extract the original image from a multi-resolution image.
622     * <p>
623     * See {@link HiDPISupport#processMRImage} for how to process the image without removing the multi-resolution magic.
624     * @param multiResolution true, if multi-resolution image is requested
625     * @return the current object, for convenience
626     */
627    public ImageProvider setMultiResolution(boolean multiResolution) {
628        this.multiResolution = multiResolution;
629        return this;
630    }
631
632    /**
633     * Execute the image request and scale result.
634     * @return the requested image or null if the request failed
635     */
636    public ImageIcon get() {
637        ImageResource ir = getResource();
638
639        if (ir == null) {
640            return null;
641        }
642        if (virtualMaxWidth != -1 || virtualMaxHeight != -1)
643            return ir.getImageIconBounded(new Dimension(virtualMaxWidth, virtualMaxHeight), multiResolution);
644        else
645            return ir.getImageIcon(new Dimension(virtualWidth, virtualHeight), multiResolution);
646    }
647
648    /**
649     * Load the image in a background thread.
650     *
651     * This method returns immediately and runs the image request asynchronously.
652     *
653     * @return the future of the requested image
654     * @since 10714
655     */
656    public CompletableFuture<ImageIcon> getAsync() {
657        return name.startsWith(HTTP_PROTOCOL) || name.startsWith(WIKI_PROTOCOL)
658                ? CompletableFuture.supplyAsync(this::get, IMAGE_FETCHER)
659                : CompletableFuture.completedFuture(get());
660    }
661
662    /**
663     * Execute the image request.
664     *
665     * @return the requested image or null if the request failed
666     * @since 7693
667     */
668    public ImageResource getResource() {
669        ImageResource ir = getIfAvailableImpl();
670        if (ir == null) {
671            if (!optional) {
672                String ext = name.indexOf('.') != -1 ? "" : ".???";
673                throw new JosmRuntimeException(
674                        tr("Fatal: failed to locate image ''{0}''. This is a serious configuration problem. JOSM will stop working.",
675                                name + ext));
676            } else {
677                if (!suppressWarnings) {
678                    Logging.error(tr("Failed to locate image ''{0}''", name));
679                }
680                return null;
681            }
682        }
683        if (overlayInfo != null) {
684            ir = new ImageResource(ir, overlayInfo);
685        }
686        if (isDisabled) {
687            ir.setDisabled(true);
688        }
689        return ir;
690    }
691
692    /**
693     * Load the image in a background thread.
694     *
695     * This method returns immediately and runs the image request asynchronously.
696     *
697     * @return the future of the requested image
698     * @since 10714
699     */
700    public CompletableFuture<ImageResource> getResourceAsync() {
701        return name.startsWith(HTTP_PROTOCOL) || name.startsWith(WIKI_PROTOCOL)
702                ? CompletableFuture.supplyAsync(this::getResource, IMAGE_FETCHER)
703                : CompletableFuture.completedFuture(getResource());
704    }
705
706    /**
707     * Load an image with a given file name.
708     *
709     * @param subdir subdirectory the image lies in
710     * @param name The icon name (base name with or without '.png' or '.svg' extension)
711     * @return The requested Image.
712     * @throws RuntimeException if the image cannot be located
713     */
714    public static ImageIcon get(String subdir, String name) {
715        return new ImageProvider(subdir, name).get();
716    }
717
718    /**
719     * Load an image with a given file name.
720     *
721     * @param name The icon name (base name with or without '.png' or '.svg' extension)
722     * @return the requested image or null if the request failed
723     * @see #get(String, String)
724     */
725    public static ImageIcon get(String name) {
726        return new ImageProvider(name).get();
727    }
728
729    /**
730     * Load an image from directory with a given file name and size.
731     *
732     * @param subdir subdirectory the image lies in
733     * @param name The icon name (base name with or without '.png' or '.svg' extension)
734     * @param size Target icon size
735     * @return The requested Image.
736     * @throws RuntimeException if the image cannot be located
737     * @since 10428
738     */
739    public static ImageIcon get(String subdir, String name, ImageSizes size) {
740        return new ImageProvider(subdir, name).setSize(size).get();
741    }
742
743    /**
744     * Load an empty image with a given size.
745     *
746     * @param size Target icon size
747     * @return The requested Image.
748     * @since 10358
749     */
750    public static ImageIcon getEmpty(ImageSizes size) {
751        Dimension iconRealSize = GuiSizesHelper.getDimensionDpiAdjusted(size.getImageDimension());
752        return new ImageIcon(new BufferedImage(iconRealSize.width, iconRealSize.height,
753            BufferedImage.TYPE_INT_ARGB));
754    }
755
756    /**
757     * Load an image with a given file name, but do not throw an exception
758     * when the image cannot be found.
759     *
760     * @param subdir subdirectory the image lies in
761     * @param name The icon name (base name with or without '.png' or '.svg' extension)
762     * @return the requested image or null if the request failed
763     * @see #get(String, String)
764     */
765    public static ImageIcon getIfAvailable(String subdir, String name) {
766        return new ImageProvider(subdir, name).setOptional(true).get();
767    }
768
769    /**
770     * Load an image with a given file name and size.
771     *
772     * @param name The icon name (base name with or without '.png' or '.svg' extension)
773     * @param size Target icon size
774     * @return the requested image or null if the request failed
775     * @see #get(String, String)
776     * @since 10428
777     */
778    public static ImageIcon get(String name, ImageSizes size) {
779        return new ImageProvider(name).setSize(size).get();
780    }
781
782    /**
783     * Load an image with a given file name, but do not throw an exception
784     * when the image cannot be found.
785     *
786     * @param name The icon name (base name with or without '.png' or '.svg' extension)
787     * @return the requested image or null if the request failed
788     * @see #getIfAvailable(String, String)
789     */
790    public static ImageIcon getIfAvailable(String name) {
791        return new ImageProvider(name).setOptional(true).get();
792    }
793
794    /**
795     * {@code data:[<mediatype>][;base64],<data>}
796     * @see <a href="http://tools.ietf.org/html/rfc2397">RFC2397</a>
797     */
798    private static final Pattern dataUrlPattern = Pattern.compile(
799            "^data:([a-zA-Z]+/[a-zA-Z+]+)?(;base64)?,(.+)$");
800
801    /**
802     * Clears the internal image cache.
803     * @since 11021
804     */
805    public static void clearCache() {
806        synchronized (cache) {
807            cache.clear();
808        }
809    }
810
811    /**
812     * Internal implementation of the image request.
813     *
814     * @return the requested image or null if the request failed
815     */
816    private ImageResource getIfAvailableImpl() {
817        synchronized (cache) {
818            // This method is called from different thread and modifying HashMap concurrently can result
819            // for example in loops in map entries (ie freeze when such entry is retrieved)
820            if (name == null)
821                return null;
822
823            String prefix = isDisabled ? "dis:" : "";
824            if (name.startsWith("data:")) {
825                String url = name;
826                ImageResource ir = cache.get(prefix+url);
827                if (ir != null) return ir;
828                ir = getIfAvailableDataUrl(url);
829                if (ir != null) {
830                    cache.put(prefix+url, ir);
831                }
832                return ir;
833            }
834
835            ImageType type = Utils.hasExtension(name, "svg") ? ImageType.SVG : ImageType.OTHER;
836
837            if (name.startsWith(HTTP_PROTOCOL) || name.startsWith(HTTPS_PROTOCOL)) {
838                String url = name;
839                ImageResource ir = cache.get(prefix+url);
840                if (ir != null) return ir;
841                ir = getIfAvailableHttp(url, type);
842                if (ir != null) {
843                    cache.put(prefix+url, ir);
844                }
845                return ir;
846            } else if (name.startsWith(WIKI_PROTOCOL)) {
847                ImageResource ir = cache.get(prefix+name);
848                if (ir != null) return ir;
849                ir = getIfAvailableWiki(name, type);
850                if (ir != null) {
851                    cache.put(prefix+name, ir);
852                }
853                return ir;
854            }
855
856            if (subdir == null) {
857                subdir = "";
858            } else if (!subdir.isEmpty() && !subdir.endsWith("/")) {
859                subdir += '/';
860            }
861            String[] extensions;
862            if (name.indexOf('.') != -1) {
863                extensions = new String[] {""};
864            } else {
865                extensions = new String[] {".png", ".svg"};
866            }
867            final int typeArchive = 0;
868            final int typeLocal = 1;
869            for (int place : new Integer[] {typeArchive, typeLocal}) {
870                for (String ext : extensions) {
871
872                    if (".svg".equals(ext)) {
873                        type = ImageType.SVG;
874                    } else if (".png".equals(ext)) {
875                        type = ImageType.OTHER;
876                    }
877
878                    String fullName = subdir + name + ext;
879                    String cacheName = prefix + fullName;
880                    /* cache separately */
881                    if (dirs != null && !dirs.isEmpty()) {
882                        cacheName = "id:" + id + ':' + fullName;
883                        if (archive != null) {
884                            cacheName += ':' + archive.getName();
885                        }
886                    }
887
888                    switch (place) {
889                    case typeArchive:
890                        if (archive != null) {
891                            cacheName = "zip:"+archive.hashCode()+':'+cacheName;
892                            ImageResource ir = cache.get(cacheName);
893                            if (ir != null) return ir;
894
895                            ir = getIfAvailableZip(fullName, archive, inArchiveDir, type);
896                            if (ir != null) {
897                                cache.put(cacheName, ir);
898                                return ir;
899                            }
900                        }
901                        break;
902                    case typeLocal:
903                        ImageResource ir = cache.get(cacheName);
904                        if (ir != null) return ir;
905
906                        // getImageUrl() does a ton of "stat()" calls and gets expensive
907                        // and redundant when you have a whole ton of objects. So,
908                        // index the cache by the name of the icon we're looking for
909                        // and don't bother to create a URL unless we're actually creating the image.
910                        URL path = getImageUrl(fullName);
911                        if (path == null) {
912                            continue;
913                        }
914                        ir = getIfAvailableLocalURL(path, type);
915                        if (ir != null) {
916                            cache.put(cacheName, ir);
917                            return ir;
918                        }
919                        break;
920                    }
921                }
922            }
923            return null;
924        }
925    }
926
927    /**
928     * Internal implementation of the image request for URL's.
929     *
930     * @param url URL of the image
931     * @param type data type of the image
932     * @return the requested image or null if the request failed
933     */
934    private static ImageResource getIfAvailableHttp(String url, ImageType type) {
935        try (CachedFile cf = new CachedFile(url).setDestDir(
936                new File(Config.getDirs().getCacheDirectory(true), "images").getPath());
937             InputStream is = cf.getInputStream()) {
938            switch (type) {
939            case SVG:
940                SVGDiagram svg = null;
941                synchronized (getSvgUniverse()) {
942                    URI uri = getSvgUniverse().loadSVG(is, Utils.fileToURL(cf.getFile()).toString());
943                    svg = getSvgUniverse().getDiagram(uri);
944                }
945                return svg == null ? null : new ImageResource(svg);
946            case OTHER:
947                BufferedImage img = null;
948                try {
949                    img = read(Utils.fileToURL(cf.getFile()), false, false);
950                } catch (IOException e) {
951                    Logging.log(Logging.LEVEL_WARN, "IOException while reading HTTP image:", e);
952                }
953                return img == null ? null : new ImageResource(img);
954            default:
955                throw new AssertionError("Unsupported type: " + type);
956            }
957        } catch (IOException e) {
958            Logging.debug(e);
959            return null;
960        }
961    }
962
963    /**
964     * Internal implementation of the image request for inline images (<b>data:</b> urls).
965     *
966     * @param url the data URL for image extraction
967     * @return the requested image or null if the request failed
968     */
969    private static ImageResource getIfAvailableDataUrl(String url) {
970        Matcher m = dataUrlPattern.matcher(url);
971        if (m.matches()) {
972            String base64 = m.group(2);
973            String data = m.group(3);
974            byte[] bytes;
975            try {
976                if (";base64".equals(base64)) {
977                    bytes = Base64.getDecoder().decode(data);
978                } else {
979                    bytes = Utils.decodeUrl(data).getBytes(StandardCharsets.UTF_8);
980                }
981            } catch (IllegalArgumentException ex) {
982                Logging.log(Logging.LEVEL_WARN, "Unable to decode URL data part: "+ex.getMessage() + " (" + data + ')', ex);
983                return null;
984            }
985            String mediatype = m.group(1);
986            if ("image/svg+xml".equals(mediatype)) {
987                String s = new String(bytes, StandardCharsets.UTF_8);
988                SVGDiagram svg;
989                synchronized (getSvgUniverse()) {
990                    URI uri = getSvgUniverse().loadSVG(new StringReader(s), Utils.encodeUrl(s));
991                    svg = getSvgUniverse().getDiagram(uri);
992                }
993                if (svg == null) {
994                    Logging.warn("Unable to process svg: "+s);
995                    return null;
996                }
997                return new ImageResource(svg);
998            } else {
999                try {
1000                    // See #10479: for PNG files, always enforce transparency to be sure tNRS chunk is used even not in paletted mode
1001                    // This can be removed if someday Oracle fixes https://bugs.openjdk.java.net/browse/JDK-6788458
1002                    // CHECKSTYLE.OFF: LineLength
1003                    // hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/dc4322602480/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java#l656
1004                    // CHECKSTYLE.ON: LineLength
1005                    Image img = read(new ByteArrayInputStream(bytes), false, true);
1006                    return img == null ? null : new ImageResource(img);
1007                } catch (IOException e) {
1008                    Logging.log(Logging.LEVEL_WARN, "IOException while reading image:", e);
1009                }
1010            }
1011        }
1012        return null;
1013    }
1014
1015    /**
1016     * Internal implementation of the image request for wiki images.
1017     *
1018     * @param name image file name
1019     * @param type data type of the image
1020     * @return the requested image or null if the request failed
1021     */
1022    private static ImageResource getIfAvailableWiki(String name, ImageType type) {
1023        final List<String> defaultBaseUrls = Arrays.asList(
1024                "https://wiki.openstreetmap.org/w/images/",
1025                "https://upload.wikimedia.org/wikipedia/commons/",
1026                "https://wiki.openstreetmap.org/wiki/File:"
1027                );
1028        final Collection<String> baseUrls = Config.getPref().getList("image-provider.wiki.urls", defaultBaseUrls);
1029
1030        final String fn = name.substring(name.lastIndexOf('/') + 1);
1031
1032        ImageResource result = null;
1033        for (String b : baseUrls) {
1034            String url;
1035            if (b.endsWith(":")) {
1036                url = getImgUrlFromWikiInfoPage(b, fn);
1037                if (url == null) {
1038                    continue;
1039                }
1040            } else {
1041                final String fnMD5 = Utils.md5Hex(fn);
1042                url = b + fnMD5.substring(0, 1) + '/' + fnMD5.substring(0, 2) + '/' + fn;
1043            }
1044            result = getIfAvailableHttp(url, type);
1045            if (result != null) {
1046                break;
1047            }
1048        }
1049        return result;
1050    }
1051
1052    /**
1053     * Internal implementation of the image request for images in Zip archives.
1054     *
1055     * @param fullName image file name
1056     * @param archive the archive to get image from
1057     * @param inArchiveDir directory of the image inside the archive or <code>null</code>
1058     * @param type data type of the image
1059     * @return the requested image or null if the request failed
1060     */
1061    private static ImageResource getIfAvailableZip(String fullName, File archive, String inArchiveDir, ImageType type) {
1062        try (ZipFile zipFile = new ZipFile(archive, StandardCharsets.UTF_8)) {
1063            if (inArchiveDir == null || ".".equals(inArchiveDir)) {
1064                inArchiveDir = "";
1065            } else if (!inArchiveDir.isEmpty()) {
1066                inArchiveDir += '/';
1067            }
1068            String entryName = inArchiveDir + fullName;
1069            ZipEntry entry = zipFile.getEntry(entryName);
1070            if (entry != null) {
1071                int size = (int) entry.getSize();
1072                int offs = 0;
1073                byte[] buf = new byte[size];
1074                try (InputStream is = zipFile.getInputStream(entry)) {
1075                    switch (type) {
1076                    case SVG:
1077                        SVGDiagram svg = null;
1078                        synchronized (getSvgUniverse()) {
1079                            URI uri = getSvgUniverse().loadSVG(is, entryName);
1080                            svg = getSvgUniverse().getDiagram(uri);
1081                        }
1082                        return svg == null ? null : new ImageResource(svg);
1083                    case OTHER:
1084                        while (size > 0) {
1085                            int l = is.read(buf, offs, size);
1086                            offs += l;
1087                            size -= l;
1088                        }
1089                        BufferedImage img = null;
1090                        try {
1091                            img = read(new ByteArrayInputStream(buf), false, false);
1092                        } catch (IOException e) {
1093                            Logging.warn(e);
1094                        }
1095                        return img == null ? null : new ImageResource(img);
1096                    default:
1097                        throw new AssertionError("Unknown ImageType: "+type);
1098                    }
1099                }
1100            }
1101        } catch (IOException e) {
1102            Logging.log(Logging.LEVEL_WARN, tr("Failed to handle zip file ''{0}''. Exception was: {1}", archive.getName(), e.toString()), e);
1103        }
1104        return null;
1105    }
1106
1107    /**
1108     * Internal implementation of the image request for local images.
1109     *
1110     * @param path image file path
1111     * @param type data type of the image
1112     * @return the requested image or null if the request failed
1113     */
1114    private static ImageResource getIfAvailableLocalURL(URL path, ImageType type) {
1115        switch (type) {
1116        case SVG:
1117            SVGDiagram svg;
1118            synchronized (getSvgUniverse()) {
1119                URI uri = getSvgUniverse().loadSVG(path);
1120                svg = getSvgUniverse().getDiagram(uri);
1121            }
1122            return svg == null ? null : new ImageResource(svg);
1123        case OTHER:
1124            BufferedImage img = null;
1125            try {
1126                // See #10479: for PNG files, always enforce transparency to be sure tNRS chunk is used even not in paletted mode
1127                // This can be removed if someday Oracle fixes https://bugs.openjdk.java.net/browse/JDK-6788458
1128                // hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/dc4322602480/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java#l656
1129                img = read(path, false, true);
1130                if (Logging.isDebugEnabled() && isTransparencyForced(img)) {
1131                    Logging.debug("Transparency has been forced for image {0}", path);
1132                }
1133            } catch (IOException e) {
1134                Logging.warn(e);
1135            }
1136            return img == null ? null : new ImageResource(img);
1137        default:
1138            throw new AssertionError();
1139        }
1140    }
1141
1142    private URL getImageUrl(String path, String name) {
1143        if (path != null && path.startsWith("resource://")) {
1144            String p = path.substring("resource://".length());
1145            for (ClassLoader source : classLoaders) {
1146                URL res;
1147                if ((res = source.getResource(p + name)) != null)
1148                    return res;
1149            }
1150        } else {
1151            File f = new File(path, name);
1152            if ((path != null || f.isAbsolute()) && f.exists())
1153                return Utils.fileToURL(f);
1154        }
1155        return null;
1156    }
1157
1158    private URL getImageUrl(String imageName) {
1159        URL u;
1160
1161        // Try passed directories first
1162        if (dirs != null) {
1163            for (String name : dirs) {
1164                try {
1165                    u = getImageUrl(name, imageName);
1166                    if (u != null)
1167                        return u;
1168                } catch (SecurityException e) {
1169                    Logging.log(Logging.LEVEL_WARN, tr(
1170                            "Failed to access directory ''{0}'' for security reasons. Exception was: {1}",
1171                            name, e.toString()), e);
1172                }
1173
1174            }
1175        }
1176        // Try user-data directory
1177        if (Config.getDirs() != null) {
1178            String dir = new File(Config.getDirs().getUserDataDirectory(false), "images").getAbsolutePath();
1179            try {
1180                u = getImageUrl(dir, imageName);
1181                if (u != null)
1182                    return u;
1183            } catch (SecurityException e) {
1184                Logging.log(Logging.LEVEL_WARN, tr(
1185                        "Failed to access directory ''{0}'' for security reasons. Exception was: {1}", dir, e
1186                        .toString()), e);
1187            }
1188        }
1189
1190        // Absolute path?
1191        u = getImageUrl(null, imageName);
1192        if (u != null)
1193            return u;
1194
1195        // Try plugins and josm classloader
1196        u = getImageUrl("resource://images/", imageName);
1197        if (u != null)
1198            return u;
1199
1200        // Try all other resource directories
1201        if (Main.pref != null) {
1202            for (String location : Main.pref.getAllPossiblePreferenceDirs()) {
1203                u = getImageUrl(location + "images", imageName);
1204                if (u != null)
1205                    return u;
1206                u = getImageUrl(location, imageName);
1207                if (u != null)
1208                    return u;
1209            }
1210        }
1211
1212        return null;
1213    }
1214
1215    /** Quit parsing, when a certain condition is met */
1216    private static class SAXReturnException extends SAXException {
1217        private final String result;
1218
1219        SAXReturnException(String result) {
1220            this.result = result;
1221        }
1222
1223        public String getResult() {
1224            return result;
1225        }
1226    }
1227
1228    /**
1229     * Reads the wiki page on a certain file in html format in order to find the real image URL.
1230     *
1231     * @param base base URL for Wiki image
1232     * @param fn filename of the Wiki image
1233     * @return image URL for a Wiki image or null in case of error
1234     */
1235    private static String getImgUrlFromWikiInfoPage(final String base, final String fn) {
1236        try {
1237            final XMLReader parser = Utils.newSafeSAXParser().getXMLReader();
1238            parser.setContentHandler(new DefaultHandler() {
1239                @Override
1240                public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
1241                    if ("img".equalsIgnoreCase(localName)) {
1242                        String val = atts.getValue("src");
1243                        if (val.endsWith(fn))
1244                            throw new SAXReturnException(val);  // parsing done, quit early
1245                    }
1246                }
1247            });
1248
1249            parser.setEntityResolver((publicId, systemId) -> new InputSource(new ByteArrayInputStream(new byte[0])));
1250
1251            try (CachedFile cf = new CachedFile(base + fn).setDestDir(
1252                        new File(Config.getDirs().getUserDataDirectory(true), "images").getPath());
1253                 InputStream is = cf.getInputStream()) {
1254                parser.parse(new InputSource(is));
1255            }
1256        } catch (SAXReturnException e) {
1257            Logging.trace(e);
1258            return e.getResult();
1259        } catch (IOException | SAXException | ParserConfigurationException e) {
1260            Logging.warn("Parsing " + base + fn + " failed:\n" + e);
1261            return null;
1262        }
1263        Logging.warn("Parsing " + base + fn + " failed: Unexpected content.");
1264        return null;
1265    }
1266
1267    /**
1268     * Load a cursor with a given file name, optionally decorated with an overlay image.
1269     *
1270     * @param name the cursor image filename in "cursor" directory
1271     * @param overlay optional overlay image
1272     * @return cursor with a given file name, optionally decorated with an overlay image
1273     */
1274    public static Cursor getCursor(String name, String overlay) {
1275        ImageIcon img = get("cursor", name);
1276        if (overlay != null) {
1277            img = new ImageProvider("cursor", name).setMaxSize(ImageSizes.CURSOR)
1278                .addOverlay(new ImageOverlay(new ImageProvider("cursor/modifier/" + overlay)
1279                    .setMaxSize(ImageSizes.CURSOROVERLAY))).get();
1280        }
1281        if (GraphicsEnvironment.isHeadless()) {
1282            Logging.debug("Cursors are not available in headless mode. Returning null for '{0}'", name);
1283            return null;
1284        }
1285        return Toolkit.getDefaultToolkit().createCustomCursor(img.getImage(),
1286                "crosshair".equals(name) ? new Point(10, 10) : new Point(3, 2), "Cursor");
1287    }
1288
1289    /** 90 degrees in radians units */
1290    private static final double DEGREE_90 = 90.0 * Math.PI / 180.0;
1291
1292    /**
1293     * Creates a rotated version of the input image.
1294     *
1295     * @param img the image to be rotated.
1296     * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we
1297     * will mod it with 360 before using it. More over for caching performance, it will be rounded to
1298     * an entire value between 0 and 360.
1299     *
1300     * @return the image after rotating.
1301     * @since 6172
1302     */
1303    public static Image createRotatedImage(Image img, double rotatedAngle) {
1304        return createRotatedImage(img, rotatedAngle, ImageResource.DEFAULT_DIMENSION);
1305    }
1306
1307    /**
1308     * Creates a rotated version of the input image.
1309     *
1310     * @param img the image to be rotated.
1311     * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we
1312     * will mod it with 360 before using it. More over for caching performance, it will be rounded to
1313     * an entire value between 0 and 360.
1314     * @param dimension ignored
1315     * @return the image after rotating and scaling.
1316     * @since 6172
1317     */
1318    public static Image createRotatedImage(Image img, double rotatedAngle, Dimension dimension) {
1319        CheckParameterUtil.ensureParameterNotNull(img, "img");
1320
1321        // convert rotatedAngle to an integer value from 0 to 360
1322        Long angleLong = Math.round(rotatedAngle % 360);
1323        Long originalAngle = rotatedAngle != 0 && angleLong == 0 ? Long.valueOf(360L) : angleLong;
1324
1325        synchronized (ROTATE_CACHE) {
1326            Map<Long, Image> cacheByAngle = ROTATE_CACHE.computeIfAbsent(img, k -> new HashMap<>());
1327            Image rotatedImg = cacheByAngle.get(originalAngle);
1328
1329            if (rotatedImg == null) {
1330                // convert originalAngle to a value from 0 to 90
1331                double angle = originalAngle % 90;
1332                if (originalAngle != 0 && angle == 0) {
1333                    angle = 90.0;
1334                }
1335                double radian = Utils.toRadians(angle);
1336
1337                rotatedImg = HiDPISupport.processMRImage(img, img0 -> {
1338                    new ImageIcon(img0); // load completely
1339                    int iw = img0.getWidth(null);
1340                    int ih = img0.getHeight(null);
1341                    int w;
1342                    int h;
1343
1344                    if ((originalAngle >= 0 && originalAngle <= 90) || (originalAngle > 180 && originalAngle <= 270)) {
1345                        w = (int) (iw * Math.sin(DEGREE_90 - radian) + ih * Math.sin(radian));
1346                        h = (int) (iw * Math.sin(radian) + ih * Math.sin(DEGREE_90 - radian));
1347                    } else {
1348                        w = (int) (ih * Math.sin(DEGREE_90 - radian) + iw * Math.sin(radian));
1349                        h = (int) (ih * Math.sin(radian) + iw * Math.sin(DEGREE_90 - radian));
1350                    }
1351                    Image image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1352                    Graphics g = image.getGraphics();
1353                    Graphics2D g2d = (Graphics2D) g.create();
1354
1355                    // calculate the center of the icon.
1356                    int cx = iw / 2;
1357                    int cy = ih / 2;
1358
1359                    // move the graphics center point to the center of the icon.
1360                    g2d.translate(w / 2, h / 2);
1361
1362                    // rotate the graphics about the center point of the icon
1363                    g2d.rotate(Utils.toRadians(originalAngle));
1364
1365                    g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
1366                    g2d.drawImage(img0, -cx, -cy, null);
1367
1368                    g2d.dispose();
1369                    new ImageIcon(image); // load completely
1370                    return image;
1371                });
1372                cacheByAngle.put(originalAngle, rotatedImg);
1373            }
1374            return rotatedImg;
1375        }
1376    }
1377
1378    /**
1379     * Creates a scaled down version of the input image to fit maximum dimensions. (Keeps aspect ratio)
1380     *
1381     * @param img the image to be scaled down.
1382     * @param maxSize the maximum size in pixels (both for width and height)
1383     *
1384     * @return the image after scaling.
1385     * @since 6172
1386     */
1387    public static Image createBoundedImage(Image img, int maxSize) {
1388        return new ImageResource(img).getImageIconBounded(new Dimension(maxSize, maxSize)).getImage();
1389    }
1390
1391    /**
1392     * Returns a scaled instance of the provided {@code BufferedImage}.
1393     * This method will use a multi-step scaling technique that provides higher quality than the usual
1394     * one-step technique (only useful in downscaling cases, where {@code targetWidth} or {@code targetHeight} is
1395     * smaller than the original dimensions, and generally only when the {@code BILINEAR} hint is specified).
1396     *
1397     * From https://community.oracle.com/docs/DOC-983611: "The Perils of Image.getScaledInstance()"
1398     *
1399     * @param img the original image to be scaled
1400     * @param targetWidth the desired width of the scaled instance, in pixels
1401     * @param targetHeight the desired height of the scaled instance, in pixels
1402     * @param hint one of the rendering hints that corresponds to
1403     * {@code RenderingHints.KEY_INTERPOLATION} (e.g.
1404     * {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},
1405     * {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},
1406     * {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})
1407     * @return a scaled version of the original {@code BufferedImage}
1408     * @since 13038
1409     */
1410    public static BufferedImage createScaledImage(BufferedImage img, int targetWidth, int targetHeight, Object hint) {
1411        int type = (img.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
1412        // start with original size, then scale down in multiple passes with drawImage() until the target size is reached
1413        BufferedImage ret = img;
1414        int w = img.getWidth(null);
1415        int h = img.getHeight(null);
1416        do {
1417            if (w > targetWidth) {
1418                w /= 2;
1419            }
1420            if (w < targetWidth) {
1421                w = targetWidth;
1422            }
1423            if (h > targetHeight) {
1424                h /= 2;
1425            }
1426            if (h < targetHeight) {
1427                h = targetHeight;
1428            }
1429            BufferedImage tmp = new BufferedImage(w, h, type);
1430            Graphics2D g2 = tmp.createGraphics();
1431            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
1432            g2.drawImage(ret, 0, 0, w, h, null);
1433            g2.dispose();
1434            ret = tmp;
1435        } while (w != targetWidth || h != targetHeight);
1436        return ret;
1437    }
1438
1439    /**
1440     * Replies the icon for an OSM primitive type
1441     * @param type the type
1442     * @return the icon
1443     */
1444    public static ImageIcon get(OsmPrimitiveType type) {
1445        CheckParameterUtil.ensureParameterNotNull(type, "type");
1446        return get("data", type.getAPIName());
1447    }
1448
1449    /**
1450     * @param primitive Object for which an icon shall be fetched. The icon is chosen based on tags.
1451     * @param iconSize Target size of icon. Icon is padded if required.
1452     * @return Icon for {@code primitive} that fits in cell.
1453     * @since 8903
1454     */
1455    public static ImageIcon getPadded(OsmPrimitive primitive, Dimension iconSize) {
1456        // Check if the current styles have special icon for tagged nodes.
1457        if (primitive instanceof org.openstreetmap.josm.data.osm.Node) {
1458            Pair<StyleElementList, Range> nodeStyles;
1459            DataSet ds = primitive.getDataSet();
1460            if (ds != null) {
1461                ds.getReadLock().lock();
1462            }
1463            try {
1464                nodeStyles = MapPaintStyles.getStyles().generateStyles(primitive, 100, false);
1465            } finally {
1466                if (ds != null) {
1467                    ds.getReadLock().unlock();
1468                }
1469            }
1470            for (StyleElement style : nodeStyles.a) {
1471                if (style instanceof NodeElement) {
1472                    NodeElement nodeStyle = (NodeElement) style;
1473                    MapImage icon = nodeStyle.mapImage;
1474                    if (icon != null) {
1475                        int backgroundRealWidth = GuiSizesHelper.getSizeDpiAdjusted(iconSize.width);
1476                        int backgroundRealHeight = GuiSizesHelper.getSizeDpiAdjusted(iconSize.height);
1477                        int iconRealWidth = icon.getWidth();
1478                        int iconRealHeight = icon.getHeight();
1479                        BufferedImage image = new BufferedImage(backgroundRealWidth, backgroundRealHeight,
1480                                BufferedImage.TYPE_INT_ARGB);
1481                        double scaleFactor = Math.min(backgroundRealWidth / (double) iconRealWidth, backgroundRealHeight
1482                                / (double) iconRealHeight);
1483                        Image iconImage = icon.getImage(false);
1484                        Image scaledIcon;
1485                        final int scaledWidth;
1486                        final int scaledHeight;
1487                        if (scaleFactor < 1) {
1488                            // Scale icon such that it fits on background.
1489                            scaledWidth = (int) (iconRealWidth * scaleFactor);
1490                            scaledHeight = (int) (iconRealHeight * scaleFactor);
1491                            scaledIcon = iconImage.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH);
1492                        } else {
1493                            // Use original size, don't upscale.
1494                            scaledWidth = iconRealWidth;
1495                            scaledHeight = iconRealHeight;
1496                            scaledIcon = iconImage;
1497                        }
1498                        image.getGraphics().drawImage(scaledIcon, (backgroundRealWidth - scaledWidth) / 2,
1499                                (backgroundRealHeight - scaledHeight) / 2, null);
1500
1501                        return new ImageIcon(image);
1502                    }
1503                }
1504            }
1505        }
1506
1507        // Check if the presets have icons for nodes/relations.
1508        if (!OsmPrimitiveType.WAY.equals(primitive.getType())) {
1509            final Collection<TaggingPreset> presets = new TreeSet<>((o1, o2) -> {
1510                final int o1TypesSize = o1.types == null || o1.types.isEmpty() ? Integer.MAX_VALUE : o1.types.size();
1511                final int o2TypesSize = o2.types == null || o2.types.isEmpty() ? Integer.MAX_VALUE : o2.types.size();
1512                return Integer.compare(o1TypesSize, o2TypesSize);
1513            });
1514            presets.addAll(TaggingPresets.getMatchingPresets(primitive));
1515            for (final TaggingPreset preset : presets) {
1516                if (preset.getIcon() != null) {
1517                    return preset.getIcon();
1518                }
1519            }
1520        }
1521
1522        // Use generic default icon.
1523        return ImageProvider.get(primitive.getDisplayType());
1524    }
1525
1526    /**
1527     * Constructs an image from the given SVG data.
1528     * @param svg the SVG data
1529     * @param dim the desired image dimension
1530     * @return an image from the given SVG data at the desired dimension.
1531     */
1532    public static BufferedImage createImageFromSvg(SVGDiagram svg, Dimension dim) {
1533        if (Logging.isTraceEnabled()) {
1534            Logging.trace("createImageFromSvg: {0} {1}", svg.getXMLBase(), dim);
1535        }
1536        float sourceWidth = svg.getWidth();
1537        float sourceHeight = svg.getHeight();
1538        int realWidth = Math.round(GuiSizesHelper.getSizeDpiAdjusted(sourceWidth));
1539        int realHeight = Math.round(GuiSizesHelper.getSizeDpiAdjusted(sourceHeight));
1540        Double scaleX, scaleY;
1541        if (dim.width != -1) {
1542            realWidth = dim.width;
1543            scaleX = (double) realWidth / sourceWidth;
1544            if (dim.height == -1) {
1545                scaleY = scaleX;
1546                realHeight = (int) Math.round(sourceHeight * scaleY);
1547            } else {
1548                realHeight = dim.height;
1549                scaleY = (double) realHeight / sourceHeight;
1550            }
1551        } else if (dim.height != -1) {
1552            realHeight = dim.height;
1553            scaleX = scaleY = (double) realHeight / sourceHeight;
1554            realWidth = (int) Math.round(sourceWidth * scaleX);
1555        } else {
1556            scaleX = scaleY = (double) realHeight / sourceHeight;
1557        }
1558
1559        if (realWidth == 0 || realHeight == 0) {
1560            return null;
1561        }
1562        BufferedImage img = new BufferedImage(realWidth, realHeight, BufferedImage.TYPE_INT_ARGB);
1563        Graphics2D g = img.createGraphics();
1564        g.setClip(0, 0, realWidth, realHeight);
1565        g.scale(scaleX, scaleY);
1566        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1567        try {
1568            synchronized (getSvgUniverse()) {
1569                svg.render(g);
1570            }
1571        } catch (SVGException ex) {
1572            Logging.log(Logging.LEVEL_ERROR, "Unable to load svg:", ex);
1573            return null;
1574        }
1575        return img;
1576    }
1577
1578    private static synchronized SVGUniverse getSvgUniverse() {
1579        if (svgUniverse == null) {
1580            svgUniverse = new SVGUniverse();
1581        }
1582        return svgUniverse;
1583    }
1584
1585    /**
1586     * Returns a <code>BufferedImage</code> as the result of decoding
1587     * a supplied <code>File</code> with an <code>ImageReader</code>
1588     * chosen automatically from among those currently registered.
1589     * The <code>File</code> is wrapped in an
1590     * <code>ImageInputStream</code>.  If no registered
1591     * <code>ImageReader</code> claims to be able to read the
1592     * resulting stream, <code>null</code> is returned.
1593     *
1594     * <p> The current cache settings from <code>getUseCache</code>and
1595     * <code>getCacheDirectory</code> will be used to control caching in the
1596     * <code>ImageInputStream</code> that is created.
1597     *
1598     * <p> Note that there is no <code>read</code> method that takes a
1599     * filename as a <code>String</code>; use this method instead after
1600     * creating a <code>File</code> from the filename.
1601     *
1602     * <p> This method does not attempt to locate
1603     * <code>ImageReader</code>s that can read directly from a
1604     * <code>File</code>; that may be accomplished using
1605     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
1606     *
1607     * @param input a <code>File</code> to read from.
1608     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color, if any.
1609     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1610     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1611     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1612     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1613     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1614     *
1615     * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
1616     *
1617     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
1618     * @throws IOException if an error occurs during reading.
1619     * @see BufferedImage#getProperty
1620     * @since 7132
1621     */
1622    public static BufferedImage read(File input, boolean readMetadata, boolean enforceTransparency) throws IOException {
1623        CheckParameterUtil.ensureParameterNotNull(input, "input");
1624        if (!input.canRead()) {
1625            throw new IIOException("Can't read input file!");
1626        }
1627
1628        ImageInputStream stream = ImageIO.createImageInputStream(input);
1629        if (stream == null) {
1630            throw new IIOException("Can't create an ImageInputStream!");
1631        }
1632        BufferedImage bi = read(stream, readMetadata, enforceTransparency);
1633        if (bi == null) {
1634            stream.close();
1635        }
1636        return bi;
1637    }
1638
1639    /**
1640     * Returns a <code>BufferedImage</code> as the result of decoding
1641     * a supplied <code>InputStream</code> with an <code>ImageReader</code>
1642     * chosen automatically from among those currently registered.
1643     * The <code>InputStream</code> is wrapped in an
1644     * <code>ImageInputStream</code>.  If no registered
1645     * <code>ImageReader</code> claims to be able to read the
1646     * resulting stream, <code>null</code> is returned.
1647     *
1648     * <p> The current cache settings from <code>getUseCache</code>and
1649     * <code>getCacheDirectory</code> will be used to control caching in the
1650     * <code>ImageInputStream</code> that is created.
1651     *
1652     * <p> This method does not attempt to locate
1653     * <code>ImageReader</code>s that can read directly from an
1654     * <code>InputStream</code>; that may be accomplished using
1655     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
1656     *
1657     * <p> This method <em>does not</em> close the provided
1658     * <code>InputStream</code> after the read operation has completed;
1659     * it is the responsibility of the caller to close the stream, if desired.
1660     *
1661     * @param input an <code>InputStream</code> to read from.
1662     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images, if any.
1663     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1664     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1665     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1666     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1667     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1668     *
1669     * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
1670     *
1671     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
1672     * @throws IOException if an error occurs during reading.
1673     * @since 7132
1674     */
1675    public static BufferedImage read(InputStream input, boolean readMetadata, boolean enforceTransparency) throws IOException {
1676        CheckParameterUtil.ensureParameterNotNull(input, "input");
1677
1678        ImageInputStream stream = ImageIO.createImageInputStream(input);
1679        BufferedImage bi = read(stream, readMetadata, enforceTransparency);
1680        if (bi == null) {
1681            stream.close();
1682        }
1683        return bi;
1684    }
1685
1686    /**
1687     * Returns a <code>BufferedImage</code> as the result of decoding
1688     * a supplied <code>URL</code> with an <code>ImageReader</code>
1689     * chosen automatically from among those currently registered.  An
1690     * <code>InputStream</code> is obtained from the <code>URL</code>,
1691     * which is wrapped in an <code>ImageInputStream</code>.  If no
1692     * registered <code>ImageReader</code> claims to be able to read
1693     * the resulting stream, <code>null</code> is returned.
1694     *
1695     * <p> The current cache settings from <code>getUseCache</code>and
1696     * <code>getCacheDirectory</code> will be used to control caching in the
1697     * <code>ImageInputStream</code> that is created.
1698     *
1699     * <p> This method does not attempt to locate
1700     * <code>ImageReader</code>s that can read directly from a
1701     * <code>URL</code>; that may be accomplished using
1702     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
1703     *
1704     * @param input a <code>URL</code> to read from.
1705     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images, if any.
1706     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1707     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1708     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1709     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1710     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1711     *
1712     * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
1713     *
1714     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
1715     * @throws IOException if an error occurs during reading.
1716     * @since 7132
1717     */
1718    public static BufferedImage read(URL input, boolean readMetadata, boolean enforceTransparency) throws IOException {
1719        CheckParameterUtil.ensureParameterNotNull(input, "input");
1720
1721        InputStream istream = null;
1722        try {
1723            istream = input.openStream();
1724        } catch (IOException e) {
1725            throw new IIOException("Can't get input stream from URL!", e);
1726        }
1727        ImageInputStream stream = ImageIO.createImageInputStream(istream);
1728        BufferedImage bi;
1729        try {
1730            bi = read(stream, readMetadata, enforceTransparency);
1731            if (bi == null) {
1732                stream.close();
1733            }
1734        } finally {
1735            istream.close();
1736        }
1737        return bi;
1738    }
1739
1740    /**
1741     * Returns a <code>BufferedImage</code> as the result of decoding
1742     * a supplied <code>ImageInputStream</code> with an
1743     * <code>ImageReader</code> chosen automatically from among those
1744     * currently registered.  If no registered
1745     * <code>ImageReader</code> claims to be able to read the stream,
1746     * <code>null</code> is returned.
1747     *
1748     * <p> Unlike most other methods in this class, this method <em>does</em>
1749     * close the provided <code>ImageInputStream</code> after the read
1750     * operation has completed, unless <code>null</code> is returned,
1751     * in which case this method <em>does not</em> close the stream.
1752     *
1753     * @param stream an <code>ImageInputStream</code> to read from.
1754     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images, if any.
1755     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1756     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1757     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1758     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1759     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1760     *
1761     * @return a <code>BufferedImage</code> containing the decoded
1762     * contents of the input, or <code>null</code>.
1763     *
1764     * @throws IllegalArgumentException if <code>stream</code> is <code>null</code>.
1765     * @throws IOException if an error occurs during reading.
1766     * @since 7132
1767     */
1768    public static BufferedImage read(ImageInputStream stream, boolean readMetadata, boolean enforceTransparency) throws IOException {
1769        CheckParameterUtil.ensureParameterNotNull(stream, "stream");
1770
1771        Iterator<ImageReader> iter = ImageIO.getImageReaders(stream);
1772        if (!iter.hasNext()) {
1773            return null;
1774        }
1775
1776        ImageReader reader = iter.next();
1777        ImageReadParam param = reader.getDefaultReadParam();
1778        reader.setInput(stream, true, !readMetadata && !enforceTransparency);
1779        BufferedImage bi = null;
1780        try {
1781            bi = reader.read(0, param);
1782            if (bi.getTransparency() != Transparency.TRANSLUCENT && (readMetadata || enforceTransparency)) {
1783                Color color = getTransparentColor(bi.getColorModel(), reader);
1784                if (color != null) {
1785                    Hashtable<String, Object> properties = new Hashtable<>(1);
1786                    properties.put(PROP_TRANSPARENCY_COLOR, color);
1787                    bi = new BufferedImage(bi.getColorModel(), bi.getRaster(), bi.isAlphaPremultiplied(), properties);
1788                    if (enforceTransparency) {
1789                        Logging.trace("Enforcing image transparency of {0} for {1}", stream, color);
1790                        bi = makeImageTransparent(bi, color);
1791                    }
1792                }
1793            }
1794        } catch (LinkageError e) {
1795            // On Windows, ComponentColorModel.getRGBComponent can fail with "UnsatisfiedLinkError: no awt in java.library.path", see #13973
1796            // Then it can leads to "NoClassDefFoundError: Could not initialize class sun.awt.image.ShortInterleavedRaster", see #15079
1797            Logging.error(e);
1798        } finally {
1799            reader.dispose();
1800            stream.close();
1801        }
1802        return bi;
1803    }
1804
1805    // CHECKSTYLE.OFF: LineLength
1806
1807    /**
1808     * Returns the {@code TransparentColor} defined in image reader metadata.
1809     * @param model The image color model
1810     * @param reader The image reader
1811     * @return the {@code TransparentColor} defined in image reader metadata, or {@code null}
1812     * @throws IOException if an error occurs during reading
1813     * @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>
1814     * @since 7499
1815     */
1816    public static Color getTransparentColor(ColorModel model, ImageReader reader) throws IOException {
1817        // CHECKSTYLE.ON: LineLength
1818        try {
1819            IIOMetadata metadata = reader.getImageMetadata(0);
1820            if (metadata != null) {
1821                String[] formats = metadata.getMetadataFormatNames();
1822                if (formats != null) {
1823                    for (String f : formats) {
1824                        if ("javax_imageio_1.0".equals(f)) {
1825                            Node root = metadata.getAsTree(f);
1826                            if (root instanceof Element) {
1827                                NodeList list = ((Element) root).getElementsByTagName("TransparentColor");
1828                                if (list.getLength() > 0) {
1829                                    Node item = list.item(0);
1830                                    if (item instanceof Element) {
1831                                        // Handle different color spaces (tested with RGB and grayscale)
1832                                        String value = ((Element) item).getAttribute("value");
1833                                        if (!value.isEmpty()) {
1834                                            String[] s = value.split(" ");
1835                                            if (s.length == 3) {
1836                                                return parseRGB(s);
1837                                            } else if (s.length == 1) {
1838                                                int pixel = Integer.parseInt(s[0]);
1839                                                int r = model.getRed(pixel);
1840                                                int g = model.getGreen(pixel);
1841                                                int b = model.getBlue(pixel);
1842                                                return new Color(r, g, b);
1843                                            } else {
1844                                                Logging.warn("Unable to translate TransparentColor '"+value+"' with color model "+model);
1845                                            }
1846                                        }
1847                                    }
1848                                }
1849                            }
1850                            break;
1851                        }
1852                    }
1853                }
1854            }
1855        } catch (IIOException | NumberFormatException e) {
1856            // JAI doesn't like some JPEG files with error "Inconsistent metadata read from stream" (see #10267)
1857            Logging.warn(e);
1858        }
1859        return null;
1860    }
1861
1862    private static Color parseRGB(String... s) {
1863        int[] rgb = new int[3];
1864        try {
1865            for (int i = 0; i < 3; i++) {
1866                rgb[i] = Integer.parseInt(s[i]);
1867            }
1868            return new Color(rgb[0], rgb[1], rgb[2]);
1869        } catch (IllegalArgumentException e) {
1870            Logging.error(e);
1871            return null;
1872        }
1873    }
1874
1875    /**
1876     * Returns a transparent version of the given image, based on the given transparent color.
1877     * @param bi The image to convert
1878     * @param color The transparent color
1879     * @return The same image as {@code bi} where all pixels of the given color are transparent.
1880     * This resulting image has also the special property {@link #PROP_TRANSPARENCY_FORCED} set to {@code color}
1881     * @see BufferedImage#getProperty
1882     * @see #isTransparencyForced
1883     * @since 7132
1884     */
1885    public static BufferedImage makeImageTransparent(BufferedImage bi, Color color) {
1886        // the color we are looking for. Alpha bits are set to opaque
1887        final int markerRGB = color.getRGB() | 0xFF000000;
1888        ImageFilter filter = new RGBImageFilter() {
1889            @Override
1890            public int filterRGB(int x, int y, int rgb) {
1891                if ((rgb | 0xFF000000) == markerRGB) {
1892                   // Mark the alpha bits as zero - transparent
1893                   return 0x00FFFFFF & rgb;
1894                } else {
1895                   return rgb;
1896                }
1897            }
1898        };
1899        ImageProducer ip = new FilteredImageSource(bi.getSource(), filter);
1900        Image img = Toolkit.getDefaultToolkit().createImage(ip);
1901        ColorModel colorModel = ColorModel.getRGBdefault();
1902        WritableRaster raster = colorModel.createCompatibleWritableRaster(img.getWidth(null), img.getHeight(null));
1903        String[] names = bi.getPropertyNames();
1904        Hashtable<String, Object> properties = new Hashtable<>(1 + (names != null ? names.length : 0));
1905        if (names != null) {
1906            for (String name : names) {
1907                properties.put(name, bi.getProperty(name));
1908            }
1909        }
1910        properties.put(PROP_TRANSPARENCY_FORCED, Boolean.TRUE);
1911        BufferedImage result = new BufferedImage(colorModel, raster, false, properties);
1912        Graphics2D g2 = result.createGraphics();
1913        g2.drawImage(img, 0, 0, null);
1914        g2.dispose();
1915        return result;
1916    }
1917
1918    /**
1919     * Determines if the transparency of the given {@code BufferedImage} has been enforced by a previous call to {@link #makeImageTransparent}.
1920     * @param bi The {@code BufferedImage} to test
1921     * @return {@code true} if the transparency of {@code bi} has been enforced by a previous call to {@code makeImageTransparent}.
1922     * @see #makeImageTransparent
1923     * @since 7132
1924     */
1925    public static boolean isTransparencyForced(BufferedImage bi) {
1926        return bi != null && !bi.getProperty(PROP_TRANSPARENCY_FORCED).equals(Image.UndefinedProperty);
1927    }
1928
1929    /**
1930     * Determines if the given {@code BufferedImage} has a transparent color determined by a previous call to {@link #read}.
1931     * @param bi The {@code BufferedImage} to test
1932     * @return {@code true} if {@code bi} has a transparent color determined by a previous call to {@code read}.
1933     * @see #read
1934     * @since 7132
1935     */
1936    public static boolean hasTransparentColor(BufferedImage bi) {
1937        return bi != null && !bi.getProperty(PROP_TRANSPARENCY_COLOR).equals(Image.UndefinedProperty);
1938    }
1939
1940    /**
1941     * Shutdown background image fetcher.
1942     * @param now if {@code true}, attempts to stop all actively executing tasks, halts the processing of waiting tasks.
1943     * if {@code false}, initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted
1944     * @since 8412
1945     */
1946    public static void shutdown(boolean now) {
1947        if (now) {
1948            IMAGE_FETCHER.shutdownNow();
1949        } else {
1950            IMAGE_FETCHER.shutdown();
1951        }
1952    }
1953
1954    /**
1955     * Converts an {@link Image} to a {@link BufferedImage} instance.
1956     * @param image image to convert
1957     * @return a {@code BufferedImage} instance for the given {@code Image}.
1958     * @since 13038
1959     */
1960    public static BufferedImage toBufferedImage(Image image) {
1961        if (image instanceof BufferedImage) {
1962            return (BufferedImage) image;
1963        } else {
1964            BufferedImage buffImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
1965            Graphics2D g2 = buffImage.createGraphics();
1966            g2.drawImage(image, 0, 0, null);
1967            g2.dispose();
1968            return buffImage;
1969        }
1970    }
1971
1972    /**
1973     * Converts an {@link Rectangle} area of {@link Image} to a {@link BufferedImage} instance.
1974     * @param image image to convert
1975     * @param crop_area rectangle to crop image with
1976     * @return a {@code BufferedImage} instance for the cropped area of {@code Image}.
1977     * @since 13127
1978     */
1979    public static BufferedImage toBufferedImage(Image image, Rectangle crop_area) {
1980        BufferedImage buffImage = null;
1981        Rectangle r = new Rectangle(image.getWidth(null), image.getHeight(null));
1982        if (r.intersection(crop_area).equals(crop_area)) {
1983            buffImage = new BufferedImage(crop_area.width, crop_area.height, BufferedImage.TYPE_INT_ARGB);
1984            Graphics2D g2 = buffImage.createGraphics();
1985            g2.drawImage(image, 0, 0, crop_area.width, crop_area.height,
1986                crop_area.x, crop_area.y, crop_area.x + crop_area.width, crop_area.y + crop_area.height, null);
1987            g2.dispose();
1988        }
1989        return buffImage;
1990    }
1991}
Note: See TracBrowser for help on using the repository browser.