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

Last change on this file since 12841 was 12841, checked in by bastiK, 6 weeks ago

see #15229 - fix deprecations caused by [12840]

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