001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Cursor;
008import java.awt.Dimension;
009import java.awt.Graphics;
010import java.awt.Graphics2D;
011import java.awt.GraphicsEnvironment;
012import java.awt.Image;
013import java.awt.Point;
014import java.awt.RenderingHints;
015import java.awt.Toolkit;
016import java.awt.Transparency;
017import java.awt.image.BufferedImage;
018import java.awt.image.ColorModel;
019import java.awt.image.FilteredImageSource;
020import java.awt.image.ImageFilter;
021import java.awt.image.ImageProducer;
022import java.awt.image.RGBImageFilter;
023import java.awt.image.WritableRaster;
024import java.io.ByteArrayInputStream;
025import java.io.File;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.StringReader;
029import java.io.UnsupportedEncodingException;
030import java.net.URI;
031import java.net.URL;
032import java.net.URLDecoder;
033import java.net.URLEncoder;
034import java.nio.charset.StandardCharsets;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.Collection;
038import java.util.HashMap;
039import java.util.Hashtable;
040import java.util.Iterator;
041import java.util.Map;
042import java.util.concurrent.ExecutorService;
043import java.util.concurrent.Executors;
044import java.util.regex.Matcher;
045import java.util.regex.Pattern;
046import java.util.zip.ZipEntry;
047import java.util.zip.ZipFile;
048
049import javax.imageio.IIOException;
050import javax.imageio.ImageIO;
051import javax.imageio.ImageReadParam;
052import javax.imageio.ImageReader;
053import javax.imageio.metadata.IIOMetadata;
054import javax.imageio.stream.ImageInputStream;
055import javax.swing.Icon;
056import javax.swing.ImageIcon;
057
058import org.apache.commons.codec.binary.Base64;
059import org.openstreetmap.josm.Main;
060import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
061import org.openstreetmap.josm.io.CachedFile;
062import org.openstreetmap.josm.plugins.PluginHandler;
063import org.w3c.dom.Element;
064import org.w3c.dom.Node;
065import org.w3c.dom.NodeList;
066import org.xml.sax.Attributes;
067import org.xml.sax.EntityResolver;
068import org.xml.sax.InputSource;
069import org.xml.sax.SAXException;
070import org.xml.sax.XMLReader;
071import org.xml.sax.helpers.DefaultHandler;
072import org.xml.sax.helpers.XMLReaderFactory;
073
074import com.kitfox.svg.SVGDiagram;
075import com.kitfox.svg.SVGUniverse;
076
077/**
078 * Helper class to support the application with images.
079 *
080 * How to use:
081 *
082 * <code>ImageIcon icon = new ImageProvider(name).setMaxWidth(24).setMaxHeight(24).get();</code>
083 * (there are more options, see below)
084 *
085 * short form:
086 * <code>ImageIcon icon = ImageProvider.get(name);</code>
087 *
088 * @author imi
089 */
090public class ImageProvider {
091
092    private static final String HTTP_PROTOCOL  = "http://";
093    private static final String HTTPS_PROTOCOL = "https://";
094    private static final String WIKI_PROTOCOL  = "wiki://";
095
096    /**
097     * Position of an overlay icon
098     */
099    public static enum OverlayPosition {
100        /** North west */
101        NORTHWEST,
102        /** North east */
103        NORTHEAST,
104        /** South west */
105        SOUTHWEST,
106        /** South east */
107        SOUTHEAST
108    }
109
110    /**
111     * Supported image types
112     */
113    public static 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 static enum ImageSizes {
125        /** SMALL_ICON value of on Action */
126        SMALLICON,
127        /** LARGE_ICON_KEY value of on Action */
128        LARGEICON,
129        /** MAP icon */
130        MAP,
131        /** MAP icon maximum size */
132        MAPMAX,
133        /** MENU icon size */
134        MENU,
135    }
136
137    /**
138     * Property set on {@code BufferedImage} returned by {@link #makeImageTransparent}.
139     * @since 7132
140     */
141    public static String PROP_TRANSPARENCY_FORCED = "josm.transparency.forced";
142
143    /**
144     * Property set on {@code BufferedImage} returned by {@link #read} if metadata is required.
145     * @since 7132
146     */
147    public static String PROP_TRANSPARENCY_COLOR = "josm.transparency.color";
148
149    protected Collection<String> dirs;
150    protected String id;
151    protected String subdir;
152    protected String name;
153    protected File archive;
154    protected String inArchiveDir;
155    protected int width = -1;
156    protected int height = -1;
157    protected int maxWidth = -1;
158    protected int maxHeight = -1;
159    protected boolean optional;
160    protected boolean suppressWarnings;
161    protected Collection<ClassLoader> additionalClassLoaders;
162
163    private static SVGUniverse svgUniverse;
164
165    /**
166     * The icon cache
167     */
168    private static final Map<String, ImageResource> cache = new HashMap<>();
169
170    /**
171     * Caches the image data for rotated versions of the same image.
172     */
173    private static final Map<Image, Map<Long, ImageResource>> ROTATE_CACHE = new HashMap<>();
174
175    private static final ExecutorService IMAGE_FETCHER = Executors.newSingleThreadExecutor();
176
177    /**
178     * Callback interface for asynchronous image loading.
179     */
180    public interface ImageCallback {
181        /**
182         * Called when image loading has finished.
183         * @param result the loaded image icon
184         */
185        void finished(ImageIcon result);
186    }
187
188    /**
189     * Callback interface for asynchronous image loading (with delayed scaling possibility).
190     * @since 7693
191     */
192    public interface ImageResourceCallback {
193        /**
194         * Called when image loading has finished.
195         * @param result the loaded image resource
196         */
197        void finished(ImageResource result);
198    }
199
200    /**
201     * Constructs a new {@code ImageProvider} from a filename in a given directory.
202     * @param subdir    subdirectory the image lies in
203     * @param name      the name of the image. If it does not end with '.png' or '.svg',
204     *                  both extensions are tried.
205     */
206    public ImageProvider(String subdir, String name) {
207        this.subdir = subdir;
208        this.name = name;
209    }
210
211    /**
212     * Constructs a new {@code ImageProvider} from a filename.
213     * @param name      the name of the image. If it does not end with '.png' or '.svg',
214     *                  both extensions are tried.
215     */
216    public ImageProvider(String name) {
217        this.name = name;
218    }
219
220    /**
221     * Directories to look for the image.
222     * @param dirs The directories to look for.
223     * @return the current object, for convenience
224     */
225    public ImageProvider setDirs(Collection<String> dirs) {
226        this.dirs = dirs;
227        return this;
228    }
229
230    /**
231     * Set an id used for caching.
232     * If name starts with <tt>http://</tt> Id is not used for the cache.
233     * (A URL is unique anyway.)
234     * @return the current object, for convenience
235     */
236    public ImageProvider setId(String id) {
237        this.id = id;
238        return this;
239    }
240
241    /**
242     * Specify a zip file where the image is located.
243     *
244     * (optional)
245     * @return the current object, for convenience
246     */
247    public ImageProvider setArchive(File archive) {
248        this.archive = archive;
249        return this;
250    }
251
252    /**
253     * Specify a base path inside the zip file.
254     *
255     * The subdir and name will be relative to this path.
256     *
257     * (optional)
258     * @return the current object, for convenience
259     */
260    public ImageProvider setInArchiveDir(String inArchiveDir) {
261        this.inArchiveDir = inArchiveDir;
262        return this;
263    }
264
265    /**
266     * Convert enumerated size values to real numbers
267     * @param size the size enumeration
268     * @return dimension of image in pixels
269     * @since 7687
270     */
271    public static Dimension getImageSizes(ImageSizes size) {
272        int sizeval;
273        switch(size) {
274        case MAPMAX: sizeval = Main.pref.getInteger("iconsize.mapmax", 48); break;
275        case MAP: sizeval = Main.pref.getInteger("iconsize.mapmax", 16); break;
276        case LARGEICON: sizeval = Main.pref.getInteger("iconsize.largeicon", 24); break;
277        case MENU: /* MENU is SMALLICON - only provided in case of future changes */
278        case SMALLICON: sizeval = Main.pref.getInteger("iconsize.smallicon", 16); break;
279        default: sizeval = Main.pref.getInteger("iconsize.default", 24); break;
280        }
281        return new Dimension(sizeval, sizeval);
282    }
283
284    /**
285     * Set the dimensions of the image.
286     *
287     * If not specified, the original size of the image is used.
288     * The width part of the dimension can be -1. Then it will only set the height but
289     * keep the aspect ratio. (And the other way around.)
290     * @return the current object, for convenience
291     */
292    public ImageProvider setSize(Dimension size) {
293        this.width = size.width;
294        this.height = size.height;
295        return this;
296    }
297
298    /**
299     * Set the dimensions of the image.
300     *
301     * If not specified, the original size of the image is used.
302     * @return the current object, for convenience
303     * @since 7687
304     */
305    public ImageProvider setSize(ImageSizes size) {
306        return setSize(getImageSizes(size));
307    }
308
309    /**
310     * @see #setSize
311     * @return the current object, for convenience
312     */
313    public ImageProvider setWidth(int width) {
314        this.width = width;
315        return this;
316    }
317
318    /**
319     * @see #setSize
320     * @return the current object, for convenience
321     */
322    public ImageProvider setHeight(int height) {
323        this.height = height;
324        return this;
325    }
326
327    /**
328     * Limit the maximum size of the image.
329     *
330     * It will shrink the image if necessary, but keep the aspect ratio.
331     * The given width or height can be -1 which means this direction is not bounded.
332     *
333     * 'size' and 'maxSize' are not compatible, you should set only one of them.
334     * @return the current object, for convenience
335     */
336    public ImageProvider setMaxSize(Dimension maxSize) {
337        this.maxWidth = maxSize.width;
338        this.maxHeight = maxSize.height;
339        return this;
340    }
341
342    /**
343     * Limit the maximum size of the image.
344     *
345     * It will shrink the image if necessary, but keep the aspect ratio.
346     * The given width or height can be -1 which means this direction is not bounded.
347     *
348     * 'size' and 'maxSize' are not compatible, you should set only one of them.
349     * @return the current object, for convenience
350     * @since 7687
351     */
352    public ImageProvider setMaxSize(ImageSizes size) {
353        return setMaxSize(getImageSizes(size));
354    }
355
356    /**
357     * Convenience method, see {@link #setMaxSize(Dimension)}.
358     * @return the current object, for convenience
359     */
360    public ImageProvider setMaxSize(int maxSize) {
361        return this.setMaxSize(new Dimension(maxSize, maxSize));
362    }
363
364    /**
365     * @see #setMaxSize
366     * @return the current object, for convenience
367     */
368    public ImageProvider setMaxWidth(int maxWidth) {
369        this.maxWidth = maxWidth;
370        return this;
371    }
372
373    /**
374     * @see #setMaxSize
375     * @return the current object, for convenience
376     */
377    public ImageProvider setMaxHeight(int maxHeight) {
378        this.maxHeight = maxHeight;
379        return this;
380    }
381
382    /**
383     * Decide, if an exception should be thrown, when the image cannot be located.
384     *
385     * Set to true, when the image URL comes from user data and the image may be missing.
386     *
387     * @param optional true, if JOSM should <b>not</b> throw a RuntimeException
388     * in case the image cannot be located.
389     * @return the current object, for convenience
390     */
391    public ImageProvider setOptional(boolean optional) {
392        this.optional = optional;
393        return this;
394    }
395
396    /**
397     * Suppresses warning on the command line in case the image cannot be found.
398     *
399     * In combination with setOptional(true);
400     * @return the current object, for convenience
401     */
402    public ImageProvider setSuppressWarnings(boolean suppressWarnings) {
403        this.suppressWarnings = suppressWarnings;
404        return this;
405    }
406
407    /**
408     * Add a collection of additional class loaders to search image for.
409     * @return the current object, for convenience
410     */
411    public ImageProvider setAdditionalClassLoaders(Collection<ClassLoader> additionalClassLoaders) {
412        this.additionalClassLoaders = additionalClassLoaders;
413        return this;
414    }
415
416    /**
417     * Execute the image request and scale result.
418     * @return the requested image or null if the request failed
419     */
420    public ImageIcon get() {
421        ImageResource ir = getResource();
422        if (ir == null)
423            return null;
424        if (maxWidth != -1 || maxHeight != -1)
425            return ir.getImageIconBounded(new Dimension(maxWidth, maxHeight));
426        else
427            return ir.getImageIcon(new Dimension(width, height));
428    }
429
430    /**
431     * Execute the image request.
432     * @return the requested image or null if the request failed
433     * @since 7693
434     */
435    public ImageResource getResource() {
436        ImageResource ir = getIfAvailableImpl(additionalClassLoaders);
437        if (ir == null) {
438            if (!optional) {
439                String ext = name.indexOf('.') != -1 ? "" : ".???";
440                throw new RuntimeException(tr("Fatal: failed to locate image ''{0}''. This is a serious configuration problem. JOSM will stop working.", name + ext));
441            } else {
442                if (!suppressWarnings) {
443                    Main.error(tr("Failed to locate image ''{0}''", name));
444                }
445                return null;
446            }
447        }
448        return ir;
449    }
450
451    /**
452     * Load the image in a background thread.
453     *
454     * This method returns immediately and runs the image request
455     * asynchronously.
456     *
457     * @param callback a callback. It is called, when the image is ready.
458     * This can happen before the call to this method returns or it may be
459     * invoked some time (seconds) later. If no image is available, a null
460     * value is returned to callback (just like {@link #get}).
461     */
462    public void getInBackground(final ImageCallback callback) {
463        if (name.startsWith(HTTP_PROTOCOL) || name.startsWith(WIKI_PROTOCOL)) {
464            Runnable fetch = new Runnable() {
465                @Override
466                public void run() {
467                    ImageIcon result = get();
468                    callback.finished(result);
469                }
470            };
471            IMAGE_FETCHER.submit(fetch);
472        } else {
473            ImageIcon result = get();
474            callback.finished(result);
475        }
476    }
477
478    /**
479     * Load the image in a background thread.
480     *
481     * This method returns immediately and runs the image request
482     * asynchronously.
483     *
484     * @param callback a callback. It is called, when the image is ready.
485     * This can happen before the call to this method returns or it may be
486     * invoked some time (seconds) later. If no image is available, a null
487     * value is returned to callback (just like {@link #get}).
488     * @since 7693
489     */
490    public void getInBackground(final ImageResourceCallback callback) {
491        if (name.startsWith(HTTP_PROTOCOL) || name.startsWith(WIKI_PROTOCOL)) {
492            Runnable fetch = new Runnable() {
493                @Override
494                public void run() {
495                    callback.finished(getResource());
496                }
497            };
498            IMAGE_FETCHER.submit(fetch);
499        } else {
500            callback.finished(getResource());
501        }
502    }
503
504    /**
505     * Load an image with a given file name.
506     *
507     * @param subdir subdirectory the image lies in
508     * @param name The icon name (base name with or without '.png' or '.svg' extension)
509     * @return The requested Image.
510     * @throws RuntimeException if the image cannot be located
511     */
512    public static ImageIcon get(String subdir, String name) {
513        return new ImageProvider(subdir, name).get();
514    }
515
516    /**
517     * Load an image with a given file name.
518     *
519     * @param name The icon name (base name with or without '.png' or '.svg' extension)
520     * @return the requested image or null if the request failed
521     * @see #get(String, String)
522     */
523    public static ImageIcon get(String name) {
524        return new ImageProvider(name).get();
525    }
526
527    /**
528     * Load an image with a given file name, but do not throw an exception
529     * when the image cannot be found.
530     *
531     * @param subdir subdirectory the image lies in
532     * @param name The icon name (base name with or without '.png' or '.svg' extension)
533     * @return the requested image or null if the request failed
534     * @see #get(String, String)
535     */
536    public static ImageIcon getIfAvailable(String subdir, String name) {
537        return new ImageProvider(subdir, name).setOptional(true).get();
538    }
539
540    /**
541     * @param name The icon name (base name with or without '.png' or '.svg' extension)
542     * @return the requested image or null if the request failed
543     * @see #getIfAvailable(String, String)
544     */
545    public static ImageIcon getIfAvailable(String name) {
546        return new ImageProvider(name).setOptional(true).get();
547    }
548
549    /**
550     * {@code data:[<mediatype>][;base64],<data>}
551     * @see <a href="http://tools.ietf.org/html/rfc2397">RFC2397</a>
552     */
553    private static final Pattern dataUrlPattern = Pattern.compile(
554            "^data:([a-zA-Z]+/[a-zA-Z+]+)?(;base64)?,(.+)$");
555
556    private ImageResource getIfAvailableImpl(Collection<ClassLoader> additionalClassLoaders) {
557        synchronized (cache) {
558            // This method is called from different thread and modifying HashMap concurrently can result
559            // for example in loops in map entries (ie freeze when such entry is retrieved)
560            // Yes, it did happen to me :-)
561            if (name == null)
562                return null;
563
564            if (name.startsWith("data:")) {
565                String url = name;
566                ImageResource ir = cache.get(url);
567                if (ir != null) return ir;
568                ir = getIfAvailableDataUrl(url);
569                if (ir != null) {
570                    cache.put(url, ir);
571                }
572                return ir;
573            }
574
575            ImageType type = name.toLowerCase().endsWith(".svg") ? ImageType.SVG : ImageType.OTHER;
576
577            if (name.startsWith(HTTP_PROTOCOL) || name.startsWith(HTTPS_PROTOCOL)) {
578                String url = name;
579                ImageResource ir = cache.get(url);
580                if (ir != null) return ir;
581                ir = getIfAvailableHttp(url, type);
582                if (ir != null) {
583                    cache.put(url, ir);
584                }
585                return ir;
586            } else if (name.startsWith(WIKI_PROTOCOL)) {
587                ImageResource ir = cache.get(name);
588                if (ir != null) return ir;
589                ir = getIfAvailableWiki(name, type);
590                if (ir != null) {
591                    cache.put(name, ir);
592                }
593                return ir;
594            }
595
596            if (subdir == null) {
597                subdir = "";
598            } else if (!subdir.isEmpty()) {
599                subdir += "/";
600            }
601            String[] extensions;
602            if (name.indexOf('.') != -1) {
603                extensions = new String[] { "" };
604            } else {
605                extensions = new String[] { ".png", ".svg"};
606            }
607            final int ARCHIVE = 0, LOCAL = 1;
608            for (int place : new Integer[] { ARCHIVE, LOCAL }) {
609                for (String ext : extensions) {
610
611                    if (".svg".equals(ext)) {
612                        type = ImageType.SVG;
613                    } else if (".png".equals(ext)) {
614                        type = ImageType.OTHER;
615                    }
616
617                    String fullName = subdir + name + ext;
618                    String cacheName = fullName;
619                    /* cache separately */
620                    if (dirs != null && !dirs.isEmpty()) {
621                        cacheName = "id:" + id + ":" + fullName;
622                        if(archive != null) {
623                            cacheName += ":" + archive.getName();
624                        }
625                    }
626
627                    ImageResource ir = cache.get(cacheName);
628                    if (ir != null) return ir;
629
630                    switch (place) {
631                    case ARCHIVE:
632                        if (archive != null) {
633                            ir = getIfAvailableZip(fullName, archive, inArchiveDir, type);
634                            if (ir != null) {
635                                cache.put(cacheName, ir);
636                                return ir;
637                            }
638                        }
639                        break;
640                    case LOCAL:
641                        // getImageUrl() does a ton of "stat()" calls and gets expensive
642                        // and redundant when you have a whole ton of objects. So,
643                        // index the cache by the name of the icon we're looking for
644                        // and don't bother to create a URL unless we're actually
645                        // creating the image.
646                        URL path = getImageUrl(fullName, dirs, additionalClassLoaders);
647                        if (path == null) {
648                            continue;
649                        }
650                        ir = getIfAvailableLocalURL(path, type);
651                        if (ir != null) {
652                            cache.put(cacheName, ir);
653                            return ir;
654                        }
655                        break;
656                    }
657                }
658            }
659            return null;
660        }
661    }
662
663    private static ImageResource getIfAvailableHttp(String url, ImageType type) {
664        CachedFile cf = new CachedFile(url)
665                .setDestDir(new File(Main.pref.getCacheDirectory(), "images").getPath());
666        try (InputStream is = cf.getInputStream()) {
667            switch (type) {
668            case SVG:
669                SVGDiagram svg = null;
670                synchronized (getSvgUniverse()) {
671                    URI uri = getSvgUniverse().loadSVG(is, Utils.fileToURL(cf.getFile()).toString());
672                    svg = getSvgUniverse().getDiagram(uri);
673                }
674                return svg == null ? null : new ImageResource(svg);
675            case OTHER:
676                BufferedImage img = null;
677                try {
678                    img = read(Utils.fileToURL(cf.getFile()), false, false);
679                } catch (IOException e) {
680                    Main.warn("IOException while reading HTTP image: "+e.getMessage());
681                }
682                return img == null ? null : new ImageResource(img);
683            default:
684                throw new AssertionError();
685            }
686        } catch (IOException e) {
687            return null;
688        }
689    }
690
691    private static ImageResource getIfAvailableDataUrl(String url) {
692        try {
693            Matcher m = dataUrlPattern.matcher(url);
694            if (m.matches()) {
695                String mediatype = m.group(1);
696                String base64 = m.group(2);
697                String data = m.group(3);
698                byte[] bytes;
699                if (";base64".equals(base64)) {
700                    bytes = Base64.decodeBase64(data);
701                } else {
702                    try {
703                        bytes = URLDecoder.decode(data, "UTF-8").getBytes(StandardCharsets.UTF_8);
704                    } catch (IllegalArgumentException ex) {
705                        Main.warn("Unable to decode URL data part: "+ex.getMessage() + " (" + data + ")");
706                        return null;
707                    }
708                }
709                if ("image/svg+xml".equals(mediatype)) {
710                    String s = new String(bytes, StandardCharsets.UTF_8);
711                    SVGDiagram svg = null;
712                    synchronized (getSvgUniverse()) {
713                        URI uri = getSvgUniverse().loadSVG(new StringReader(s), URLEncoder.encode(s, "UTF-8"));
714                        svg = getSvgUniverse().getDiagram(uri);
715                    }
716                    if (svg == null) {
717                        Main.warn("Unable to process svg: "+s);
718                        return null;
719                    }
720                    return new ImageResource(svg);
721                } else {
722                    try {
723                        // See #10479: for PNG files, always enforce transparency to be sure tNRS chunk is used even not in paletted mode
724                        // This can be removed if someday Oracle fixes https://bugs.openjdk.java.net/browse/JDK-6788458
725                        // hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/828c4fedd29f/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java#l656
726                        Image img = read(new ByteArrayInputStream(bytes), false, true);
727                        return img == null ? null : new ImageResource(img);
728                    } catch (IOException e) {
729                        Main.warn("IOException while reading image: "+e.getMessage());
730                    }
731                }
732            }
733            return null;
734        } catch (UnsupportedEncodingException ex) {
735            throw new RuntimeException(ex.getMessage(), ex);
736        }
737    }
738
739    private static ImageResource getIfAvailableWiki(String name, ImageType type) {
740        final Collection<String> defaultBaseUrls = Arrays.asList(
741                "http://wiki.openstreetmap.org/w/images/",
742                "http://upload.wikimedia.org/wikipedia/commons/",
743                "http://wiki.openstreetmap.org/wiki/File:"
744                );
745        final Collection<String> baseUrls = Main.pref.getCollection("image-provider.wiki.urls", defaultBaseUrls);
746
747        final String fn = name.substring(name.lastIndexOf('/') + 1);
748
749        ImageResource result = null;
750        for (String b : baseUrls) {
751            String url;
752            if (b.endsWith(":")) {
753                url = getImgUrlFromWikiInfoPage(b, fn);
754                if (url == null) {
755                    continue;
756                }
757            } else {
758                final String fn_md5 = Utils.md5Hex(fn);
759                url = b + fn_md5.substring(0,1) + "/" + fn_md5.substring(0,2) + "/" + fn;
760            }
761            result = getIfAvailableHttp(url, type);
762            if (result != null) {
763                break;
764            }
765        }
766        return result;
767    }
768
769    private static ImageResource getIfAvailableZip(String fullName, File archive, String inArchiveDir, ImageType type) {
770        try (ZipFile zipFile = new ZipFile(archive, StandardCharsets.UTF_8)) {
771            if (inArchiveDir == null || ".".equals(inArchiveDir)) {
772                inArchiveDir = "";
773            } else if (!inArchiveDir.isEmpty()) {
774                inArchiveDir += "/";
775            }
776            String entryName = inArchiveDir + fullName;
777            ZipEntry entry = zipFile.getEntry(entryName);
778            if (entry != null) {
779                int size = (int)entry.getSize();
780                int offs = 0;
781                byte[] buf = new byte[size];
782                try (InputStream is = zipFile.getInputStream(entry)) {
783                    switch (type) {
784                    case SVG:
785                        SVGDiagram svg = null;
786                        synchronized (getSvgUniverse()) {
787                            URI uri = getSvgUniverse().loadSVG(is, entryName);
788                            svg = getSvgUniverse().getDiagram(uri);
789                        }
790                        return svg == null ? null : new ImageResource(svg);
791                    case OTHER:
792                        while(size > 0)
793                        {
794                            int l = is.read(buf, offs, size);
795                            offs += l;
796                            size -= l;
797                        }
798                        BufferedImage img = null;
799                        try {
800                            img = read(new ByteArrayInputStream(buf), false, false);
801                        } catch (IOException e) {
802                            Main.warn(e);
803                        }
804                        return img == null ? null : new ImageResource(img);
805                    default:
806                        throw new AssertionError("Unknown ImageType: "+type);
807                    }
808                }
809            }
810        } catch (Exception e) {
811            Main.warn(tr("Failed to handle zip file ''{0}''. Exception was: {1}", archive.getName(), e.toString()));
812        }
813        return null;
814    }
815
816    private static ImageResource getIfAvailableLocalURL(URL path, ImageType type) {
817        switch (type) {
818        case SVG:
819            SVGDiagram svg = null;
820            synchronized (getSvgUniverse()) {
821                URI uri = getSvgUniverse().loadSVG(path);
822                svg = getSvgUniverse().getDiagram(uri);
823            }
824            return svg == null ? null : new ImageResource(svg);
825        case OTHER:
826            BufferedImage img = null;
827            try {
828                // See #10479: for PNG files, always enforce transparency to be sure tNRS chunk is used even not in paletted mode
829                // This can be removed if someday Oracle fixes https://bugs.openjdk.java.net/browse/JDK-6788458
830                // hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/828c4fedd29f/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java#l656
831                img = read(path, false, true);
832                if (Main.isDebugEnabled() && isTransparencyForced(img)) {
833                    Main.debug("Transparency has been forced for image "+path.toExternalForm());
834                }
835            } catch (IOException e) {
836                Main.warn(e);
837            }
838            return img == null ? null : new ImageResource(img);
839        default:
840            throw new AssertionError();
841        }
842    }
843
844    private static URL getImageUrl(String path, String name, Collection<ClassLoader> additionalClassLoaders) {
845        if (path != null && path.startsWith("resource://")) {
846            String p = path.substring("resource://".length());
847            Collection<ClassLoader> classLoaders = new ArrayList<>(PluginHandler.getResourceClassLoaders());
848            if (additionalClassLoaders != null) {
849                classLoaders.addAll(additionalClassLoaders);
850            }
851            for (ClassLoader source : classLoaders) {
852                URL res;
853                if ((res = source.getResource(p + name)) != null)
854                    return res;
855            }
856        } else {
857            File f = new File(path, name);
858            if ((path != null || f.isAbsolute()) && f.exists())
859                return Utils.fileToURL(f);
860        }
861        return null;
862    }
863
864    private static URL getImageUrl(String imageName, Collection<String> dirs, Collection<ClassLoader> additionalClassLoaders) {
865        URL u = null;
866
867        // Try passed directories first
868        if (dirs != null) {
869            for (String name : dirs) {
870                try {
871                    u = getImageUrl(name, imageName, additionalClassLoaders);
872                    if (u != null)
873                        return u;
874                } catch (SecurityException e) {
875                    Main.warn(tr(
876                            "Failed to access directory ''{0}'' for security reasons. Exception was: {1}",
877                            name, e.toString()));
878                }
879
880            }
881        }
882        // Try user-data directory
883        String dir = new File(Main.pref.getUserDataDirectory(), "images").getAbsolutePath();
884        try {
885            u = getImageUrl(dir, imageName, additionalClassLoaders);
886            if (u != null)
887                return u;
888        } catch (SecurityException e) {
889            Main.warn(tr(
890                    "Failed to access directory ''{0}'' for security reasons. Exception was: {1}", dir, e
891                    .toString()));
892        }
893
894        // Absolute path?
895        u = getImageUrl(null, imageName, additionalClassLoaders);
896        if (u != null)
897            return u;
898
899        // Try plugins and josm classloader
900        u = getImageUrl("resource://images/", imageName, additionalClassLoaders);
901        if (u != null)
902            return u;
903
904        // Try all other resource directories
905        for (String location : Main.pref.getAllPossiblePreferenceDirs()) {
906            u = getImageUrl(location + "images", imageName, additionalClassLoaders);
907            if (u != null)
908                return u;
909            u = getImageUrl(location, imageName, additionalClassLoaders);
910            if (u != null)
911                return u;
912        }
913
914        return null;
915    }
916
917    /** Quit parsing, when a certain condition is met */
918    private static class SAXReturnException extends SAXException {
919        private final String result;
920
921        public SAXReturnException(String result) {
922            this.result = result;
923        }
924
925        public String getResult() {
926            return result;
927        }
928    }
929
930    /**
931     * Reads the wiki page on a certain file in html format in order to find the real image URL.
932     */
933    private static String getImgUrlFromWikiInfoPage(final String base, final String fn) {
934        try {
935            final XMLReader parser = XMLReaderFactory.createXMLReader();
936            parser.setContentHandler(new DefaultHandler() {
937                @Override
938                public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
939                    if ("img".equalsIgnoreCase(localName)) {
940                        String val = atts.getValue("src");
941                        if (val.endsWith(fn))
942                            throw new SAXReturnException(val);  // parsing done, quit early
943                    }
944                }
945            });
946
947            parser.setEntityResolver(new EntityResolver() {
948                @Override
949                public InputSource resolveEntity (String publicId, String systemId) {
950                    return new InputSource(new ByteArrayInputStream(new byte[0]));
951                }
952            });
953
954            CachedFile cf = new CachedFile(base + fn).setDestDir(
955                    new File(Main.pref.getUserDataDirectory(), "images").getPath());
956            try (InputStream is = cf.getInputStream()) {
957                parser.parse(new InputSource(is));
958            }
959        } catch (SAXReturnException r) {
960            return r.getResult();
961        } catch (Exception e) {
962            Main.warn("Parsing " + base + fn + " failed:\n" + e);
963            return null;
964        }
965        Main.warn("Parsing " + base + fn + " failed: Unexpected content.");
966        return null;
967    }
968
969    /**
970     * Load a cursor with a given file name, optionally decorated with an overlay image.
971     *
972     * @param name the cursor image filename in "cursor" directory
973     * @param overlay optional overlay image
974     * @return cursor with a given file name, optionally decorated with an overlay image
975     */
976    public static Cursor getCursor(String name, String overlay) {
977        ImageIcon img = get("cursor", name);
978        if (overlay != null) {
979            img = overlay(img, ImageProvider.get("cursor/modifier/" + overlay), OverlayPosition.SOUTHEAST);
980        }
981        if (GraphicsEnvironment.isHeadless()) {
982            Main.warn("Cursors are not available in headless mode. Returning null for '"+name+"'");
983            return null;
984        }
985        return Toolkit.getDefaultToolkit().createCustomCursor(img.getImage(),
986                "crosshair".equals(name) ? new Point(10, 10) : new Point(3, 2), "Cursor");
987    }
988
989    /**
990     * Decorate one icon with an overlay icon.
991     *
992     * @param ground the base image
993     * @param overlay the overlay image (can be smaller than the base image)
994     * @param pos position of the overlay image inside the base image (positioned
995     * in one of the corners)
996     * @return an icon that represent the overlay of the two given icons. The second icon is layed
997     * on the first relative to the given position.
998     * FIXME: This function does not fit into the ImageProvider concept as public function!
999     * Overlay should be handled like all the other functions only settings arguments and
1000     * overlay must be transparent in the background.
1001     * Also scaling is not cared about with current implementation.
1002     * @deprecated this method will be refactored
1003     */
1004    @Deprecated
1005    public static ImageIcon overlay(Icon ground, Icon overlay, OverlayPosition pos) {
1006        int w = ground.getIconWidth();
1007        int h = ground.getIconHeight();
1008        int wo = overlay.getIconWidth();
1009        int ho = overlay.getIconHeight();
1010        BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1011        Graphics g = img.createGraphics();
1012        ground.paintIcon(null, g, 0, 0);
1013        int x = 0, y = 0;
1014        switch (pos) {
1015        case NORTHWEST:
1016            x = 0;
1017            y = 0;
1018            break;
1019        case NORTHEAST:
1020            x = w - wo;
1021            y = 0;
1022            break;
1023        case SOUTHWEST:
1024            x = 0;
1025            y = h - ho;
1026            break;
1027        case SOUTHEAST:
1028            x = w - wo;
1029            y = h - ho;
1030            break;
1031        }
1032        overlay.paintIcon(null, g, x, y);
1033        return new ImageIcon(img);
1034    }
1035
1036    /** 90 degrees in radians units */
1037    static final double DEGREE_90 = 90.0 * Math.PI / 180.0;
1038
1039    /**
1040     * Creates a rotated version of the input image.
1041     *
1042     * @param img the image to be rotated.
1043     * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we
1044     * will mod it with 360 before using it. More over for caching performance, it will be rounded to
1045     * an entire value between 0 and 360.
1046     *
1047     * @return the image after rotating.
1048     * @since 6172
1049     */
1050    public static Image createRotatedImage(Image img, double rotatedAngle) {
1051        return createRotatedImage(img, rotatedAngle, ImageResource.DEFAULT_DIMENSION);
1052    }
1053
1054    /**
1055     * Creates a rotated version of the input image, scaled to the given dimension.
1056     *
1057     * @param img the image to be rotated.
1058     * @param rotatedAngle the rotated angle, in degree, clockwise. It could be any double but we
1059     * will mod it with 360 before using it. More over for caching performance, it will be rounded to
1060     * an entire value between 0 and 360.
1061     * @param dimension The requested dimensions. Use (-1,-1) for the original size
1062     * and (width, -1) to set the width, but otherwise scale the image proportionally.
1063     * @return the image after rotating and scaling.
1064     * @since 6172
1065     */
1066    public static Image createRotatedImage(Image img, double rotatedAngle, Dimension dimension) {
1067        CheckParameterUtil.ensureParameterNotNull(img, "img");
1068
1069        // convert rotatedAngle to an integer value from 0 to 360
1070        Long originalAngle = Math.round(rotatedAngle % 360);
1071        if (rotatedAngle != 0 && originalAngle == 0) {
1072            originalAngle = 360L;
1073        }
1074
1075        ImageResource imageResource = null;
1076
1077        synchronized (ROTATE_CACHE) {
1078            Map<Long, ImageResource> cacheByAngle = ROTATE_CACHE.get(img);
1079            if (cacheByAngle == null) {
1080                ROTATE_CACHE.put(img, cacheByAngle = new HashMap<>());
1081            }
1082
1083            imageResource = cacheByAngle.get(originalAngle);
1084
1085            if (imageResource == null) {
1086                // convert originalAngle to a value from 0 to 90
1087                double angle = originalAngle % 90;
1088                if (originalAngle != 0.0 && angle == 0.0) {
1089                    angle = 90.0;
1090                }
1091
1092                double radian = Math.toRadians(angle);
1093
1094                new ImageIcon(img); // load completely
1095                int iw = img.getWidth(null);
1096                int ih = img.getHeight(null);
1097                int w;
1098                int h;
1099
1100                if ((originalAngle >= 0 && originalAngle <= 90) || (originalAngle > 180 && originalAngle <= 270)) {
1101                    w = (int) (iw * Math.sin(DEGREE_90 - radian) + ih * Math.sin(radian));
1102                    h = (int) (iw * Math.sin(radian) + ih * Math.sin(DEGREE_90 - radian));
1103                } else {
1104                    w = (int) (ih * Math.sin(DEGREE_90 - radian) + iw * Math.sin(radian));
1105                    h = (int) (ih * Math.sin(radian) + iw * Math.sin(DEGREE_90 - radian));
1106                }
1107                Image image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1108                cacheByAngle.put(originalAngle, imageResource = new ImageResource(image));
1109                Graphics g = image.getGraphics();
1110                Graphics2D g2d = (Graphics2D) g.create();
1111
1112                // calculate the center of the icon.
1113                int cx = iw / 2;
1114                int cy = ih / 2;
1115
1116                // move the graphics center point to the center of the icon.
1117                g2d.translate(w / 2, h / 2);
1118
1119                // rotate the graphics about the center point of the icon
1120                g2d.rotate(Math.toRadians(originalAngle));
1121
1122                g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
1123                g2d.drawImage(img, -cx, -cy, null);
1124
1125                g2d.dispose();
1126                new ImageIcon(image); // load completely
1127            }
1128            return imageResource.getImageIcon(dimension).getImage();
1129        }
1130    }
1131
1132    /**
1133     * Creates a scaled down version of the input image to fit maximum dimensions. (Keeps aspect ratio)
1134     *
1135     * @param img the image to be scaled down.
1136     * @param maxSize the maximum size in pixels (both for width and height)
1137     *
1138     * @return the image after scaling.
1139     * @since 6172
1140     */
1141    public static Image createBoundedImage(Image img, int maxSize) {
1142        return new ImageResource(img).getImageIconBounded(new Dimension(maxSize, maxSize)).getImage();
1143    }
1144
1145    /**
1146     * Replies the icon for an OSM primitive type
1147     * @param type the type
1148     * @return the icon
1149     */
1150    public static ImageIcon get(OsmPrimitiveType type) {
1151        CheckParameterUtil.ensureParameterNotNull(type, "type");
1152        return get("data", type.getAPIName());
1153    }
1154
1155    /**
1156     * Constructs an image from the given SVG data.
1157     * @param svg the SVG data
1158     * @param dim the desired image dimension
1159     * @return an image from the given SVG data at the desired dimension.
1160     */
1161    public static BufferedImage createImageFromSvg(SVGDiagram svg, Dimension dim) {
1162        float realWidth = svg.getWidth();
1163        float realHeight = svg.getHeight();
1164        int width = Math.round(realWidth);
1165        int height = Math.round(realHeight);
1166        Double scaleX = null, scaleY = null;
1167        if (dim.width != -1) {
1168            width = dim.width;
1169            scaleX = (double) width / realWidth;
1170            if (dim.height == -1) {
1171                scaleY = scaleX;
1172                height = (int) Math.round(realHeight * scaleY);
1173            } else {
1174                height = dim.height;
1175                scaleY = (double) height / realHeight;
1176            }
1177        } else if (dim.height != -1) {
1178            height = dim.height;
1179            scaleX = scaleY = (double) height / realHeight;
1180            width = (int) Math.round(realWidth * scaleX);
1181        }
1182        if (width == 0 || height == 0) {
1183            return null;
1184        }
1185        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
1186        Graphics2D g = img.createGraphics();
1187        g.setClip(0, 0, width, height);
1188        if (scaleX != null && scaleY != null) {
1189            g.scale(scaleX, scaleY);
1190        }
1191        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1192        try {
1193            synchronized (getSvgUniverse()) {
1194                svg.render(g);
1195            }
1196        } catch (Exception ex) {
1197            Main.error("Unable to load svg: {0}", ex.getMessage());
1198            return null;
1199        }
1200        return img;
1201    }
1202
1203    private static SVGUniverse getSvgUniverse() {
1204        if (svgUniverse == null) {
1205            svgUniverse = new SVGUniverse();
1206        }
1207        return svgUniverse;
1208    }
1209
1210    /**
1211     * Returns a <code>BufferedImage</code> as the result of decoding
1212     * a supplied <code>File</code> with an <code>ImageReader</code>
1213     * chosen automatically from among those currently registered.
1214     * The <code>File</code> is wrapped in an
1215     * <code>ImageInputStream</code>.  If no registered
1216     * <code>ImageReader</code> claims to be able to read the
1217     * resulting stream, <code>null</code> is returned.
1218     *
1219     * <p> The current cache settings from <code>getUseCache</code>and
1220     * <code>getCacheDirectory</code> will be used to control caching in the
1221     * <code>ImageInputStream</code> that is created.
1222     *
1223     * <p> Note that there is no <code>read</code> method that takes a
1224     * filename as a <code>String</code>; use this method instead after
1225     * creating a <code>File</code> from the filename.
1226     *
1227     * <p> This method does not attempt to locate
1228     * <code>ImageReader</code>s that can read directly from a
1229     * <code>File</code>; that may be accomplished using
1230     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
1231     *
1232     * @param input a <code>File</code> to read from.
1233     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color, if any.
1234     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1235     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1236     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1237     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1238     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1239     *
1240     * @return a <code>BufferedImage</code> containing the decoded
1241     * contents of the input, or <code>null</code>.
1242     *
1243     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
1244     * @throws IOException if an error occurs during reading.
1245     * @since 7132
1246     * @see BufferedImage#getProperty
1247     */
1248    public static BufferedImage read(File input, boolean readMetadata, boolean enforceTransparency) throws IOException {
1249        CheckParameterUtil.ensureParameterNotNull(input, "input");
1250        if (!input.canRead()) {
1251            throw new IIOException("Can't read input file!");
1252        }
1253
1254        ImageInputStream stream = ImageIO.createImageInputStream(input);
1255        if (stream == null) {
1256            throw new IIOException("Can't create an ImageInputStream!");
1257        }
1258        BufferedImage bi = read(stream, readMetadata, enforceTransparency);
1259        if (bi == null) {
1260            stream.close();
1261        }
1262        return bi;
1263    }
1264
1265    /**
1266     * Returns a <code>BufferedImage</code> as the result of decoding
1267     * a supplied <code>InputStream</code> with an <code>ImageReader</code>
1268     * chosen automatically from among those currently registered.
1269     * The <code>InputStream</code> is wrapped in an
1270     * <code>ImageInputStream</code>.  If no registered
1271     * <code>ImageReader</code> claims to be able to read the
1272     * resulting stream, <code>null</code> is returned.
1273     *
1274     * <p> The current cache settings from <code>getUseCache</code>and
1275     * <code>getCacheDirectory</code> will be used to control caching in the
1276     * <code>ImageInputStream</code> that is created.
1277     *
1278     * <p> This method does not attempt to locate
1279     * <code>ImageReader</code>s that can read directly from an
1280     * <code>InputStream</code>; that may be accomplished using
1281     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
1282     *
1283     * <p> This method <em>does not</em> close the provided
1284     * <code>InputStream</code> after the read operation has completed;
1285     * it is the responsibility of the caller to close the stream, if desired.
1286     *
1287     * @param input an <code>InputStream</code> to read from.
1288     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images, if any.
1289     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1290     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1291     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1292     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1293     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1294     *
1295     * @return a <code>BufferedImage</code> containing the decoded
1296     * contents of the input, or <code>null</code>.
1297     *
1298     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
1299     * @throws IOException if an error occurs during reading.
1300     * @since 7132
1301     */
1302    public static BufferedImage read(InputStream input, boolean readMetadata, boolean enforceTransparency) throws IOException {
1303        CheckParameterUtil.ensureParameterNotNull(input, "input");
1304
1305        ImageInputStream stream = ImageIO.createImageInputStream(input);
1306        BufferedImage bi = read(stream, readMetadata, enforceTransparency);
1307        if (bi == null) {
1308            stream.close();
1309        }
1310        return bi;
1311    }
1312
1313    /**
1314     * Returns a <code>BufferedImage</code> as the result of decoding
1315     * a supplied <code>URL</code> with an <code>ImageReader</code>
1316     * chosen automatically from among those currently registered.  An
1317     * <code>InputStream</code> is obtained from the <code>URL</code>,
1318     * which is wrapped in an <code>ImageInputStream</code>.  If no
1319     * registered <code>ImageReader</code> claims to be able to read
1320     * the resulting stream, <code>null</code> is returned.
1321     *
1322     * <p> The current cache settings from <code>getUseCache</code>and
1323     * <code>getCacheDirectory</code> will be used to control caching in the
1324     * <code>ImageInputStream</code> that is created.
1325     *
1326     * <p> This method does not attempt to locate
1327     * <code>ImageReader</code>s that can read directly from a
1328     * <code>URL</code>; that may be accomplished using
1329     * <code>IIORegistry</code> and <code>ImageReaderSpi</code>.
1330     *
1331     * @param input a <code>URL</code> to read from.
1332     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images, if any.
1333     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1334     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1335     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1336     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1337     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1338     *
1339     * @return a <code>BufferedImage</code> containing the decoded
1340     * contents of the input, or <code>null</code>.
1341     *
1342     * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
1343     * @throws IOException if an error occurs during reading.
1344     * @since 7132
1345     */
1346    public static BufferedImage read(URL input, boolean readMetadata, boolean enforceTransparency) throws IOException {
1347        CheckParameterUtil.ensureParameterNotNull(input, "input");
1348
1349        InputStream istream = null;
1350        try {
1351            istream = input.openStream();
1352        } catch (IOException e) {
1353            throw new IIOException("Can't get input stream from URL!", e);
1354        }
1355        ImageInputStream stream = ImageIO.createImageInputStream(istream);
1356        BufferedImage bi;
1357        try {
1358            bi = read(stream, readMetadata, enforceTransparency);
1359            if (bi == null) {
1360                stream.close();
1361            }
1362        } finally {
1363            istream.close();
1364        }
1365        return bi;
1366    }
1367
1368    /**
1369     * Returns a <code>BufferedImage</code> as the result of decoding
1370     * a supplied <code>ImageInputStream</code> with an
1371     * <code>ImageReader</code> chosen automatically from among those
1372     * currently registered.  If no registered
1373     * <code>ImageReader</code> claims to be able to read the stream,
1374     * <code>null</code> is returned.
1375     *
1376     * <p> Unlike most other methods in this class, this method <em>does</em>
1377     * close the provided <code>ImageInputStream</code> after the read
1378     * operation has completed, unless <code>null</code> is returned,
1379     * in which case this method <em>does not</em> close the stream.
1380     *
1381     * @param stream an <code>ImageInputStream</code> to read from.
1382     * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images, if any.
1383     * In that case the color can be retrieved later through {@link #PROP_TRANSPARENCY_COLOR}.
1384     * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
1385     * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
1386     * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
1387     * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
1388     *
1389     * @return a <code>BufferedImage</code> containing the decoded
1390     * contents of the input, or <code>null</code>.
1391     *
1392     * @throws IllegalArgumentException if <code>stream</code> is <code>null</code>.
1393     * @throws IOException if an error occurs during reading.
1394     * @since 7132
1395     */
1396    public static BufferedImage read(ImageInputStream stream, boolean readMetadata, boolean enforceTransparency) throws IOException {
1397        CheckParameterUtil.ensureParameterNotNull(stream, "stream");
1398
1399        Iterator<ImageReader> iter = ImageIO.getImageReaders(stream);
1400        if (!iter.hasNext()) {
1401            return null;
1402        }
1403
1404        ImageReader reader = iter.next();
1405        ImageReadParam param = reader.getDefaultReadParam();
1406        reader.setInput(stream, true, !readMetadata && !enforceTransparency);
1407        BufferedImage bi;
1408        try {
1409            bi = reader.read(0, param);
1410            if (bi.getTransparency() != Transparency.TRANSLUCENT && (readMetadata || enforceTransparency)) {
1411                Color color = getTransparentColor(bi.getColorModel(), reader);
1412                if (color != null) {
1413                    Hashtable<String, Object> properties = new Hashtable<>(1);
1414                    properties.put(PROP_TRANSPARENCY_COLOR, color);
1415                    bi = new BufferedImage(bi.getColorModel(), bi.getRaster(), bi.isAlphaPremultiplied(), properties);
1416                    if (enforceTransparency) {
1417                        if (Main.isTraceEnabled()) {
1418                            Main.trace("Enforcing image transparency of "+stream+" for "+color);
1419                        }
1420                        bi = makeImageTransparent(bi, color);
1421                    }
1422                }
1423            }
1424        } finally {
1425            reader.dispose();
1426            stream.close();
1427        }
1428        return bi;
1429    }
1430
1431    /**
1432     * Returns the {@code TransparentColor} defined in image reader metadata.
1433     * @param model The image color model
1434     * @param reader The image reader
1435     * @return the {@code TransparentColor} defined in image reader metadata, or {@code null}
1436     * @throws IOException if an error occurs during reading
1437     * @since 7499
1438     * @see <a href="http://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/standard_metadata.html">javax_imageio_1.0 metadata</a>
1439     */
1440    public static Color getTransparentColor(ColorModel model, ImageReader reader) throws IOException {
1441        try {
1442            IIOMetadata metadata = reader.getImageMetadata(0);
1443            if (metadata != null) {
1444                String[] formats = metadata.getMetadataFormatNames();
1445                if (formats != null) {
1446                    for (String f : formats) {
1447                        if ("javax_imageio_1.0".equals(f)) {
1448                            Node root = metadata.getAsTree(f);
1449                            if (root instanceof Element) {
1450                                NodeList list = ((Element)root).getElementsByTagName("TransparentColor");
1451                                if (list.getLength() > 0) {
1452                                    Node item = list.item(0);
1453                                    if (item instanceof Element) {
1454                                        // Handle different color spaces (tested with RGB and grayscale)
1455                                        String value = ((Element)item).getAttribute("value");
1456                                        if (!value.isEmpty()) {
1457                                            String[] s = value.split(" ");
1458                                            if (s.length == 3) {
1459                                                return parseRGB(s);
1460                                            } else if (s.length == 1) {
1461                                                int pixel = Integer.parseInt(s[0]);
1462                                                int r = model.getRed(pixel);
1463                                                int g = model.getGreen(pixel);
1464                                                int b = model.getBlue(pixel);
1465                                                return new Color(r,g,b);
1466                                            } else {
1467                                                Main.warn("Unable to translate TransparentColor '"+value+"' with color model "+model);
1468                                            }
1469                                        }
1470                                    }
1471                                }
1472                            }
1473                            break;
1474                        }
1475                    }
1476                }
1477            }
1478        } catch (IIOException | NumberFormatException e) {
1479            // JAI doesn't like some JPEG files with error "Inconsistent metadata read from stream" (see #10267)
1480            Main.warn(e);
1481        }
1482        return null;
1483    }
1484
1485    private static Color parseRGB(String[] s) {
1486        int[] rgb = new int[3];
1487        try {
1488            for (int i = 0; i<3; i++) {
1489                rgb[i] = Integer.parseInt(s[i]);
1490            }
1491            return new Color(rgb[0], rgb[1], rgb[2]);
1492        } catch (IllegalArgumentException e) {
1493            Main.error(e);
1494            return null;
1495        }
1496    }
1497
1498    /**
1499     * Returns a transparent version of the given image, based on the given transparent color.
1500     * @param bi The image to convert
1501     * @param color The transparent color
1502     * @return The same image as {@code bi} where all pixels of the given color are transparent.
1503     * This resulting image has also the special property {@link #PROP_TRANSPARENCY_FORCED} set to {@code color}
1504     * @since 7132
1505     * @see BufferedImage#getProperty
1506     * @see #isTransparencyForced
1507     */
1508    public static BufferedImage makeImageTransparent(BufferedImage bi, Color color) {
1509        // the color we are looking for. Alpha bits are set to opaque
1510        final int markerRGB = color.getRGB() | 0xFF000000;
1511        ImageFilter filter = new RGBImageFilter() {
1512            @Override
1513            public int filterRGB(int x, int y, int rgb) {
1514                if ((rgb | 0xFF000000) == markerRGB) {
1515                   // Mark the alpha bits as zero - transparent
1516                   return 0x00FFFFFF & rgb;
1517                } else {
1518                   return rgb;
1519                }
1520            }
1521        };
1522        ImageProducer ip = new FilteredImageSource(bi.getSource(), filter);
1523        Image img = Toolkit.getDefaultToolkit().createImage(ip);
1524        ColorModel colorModel = ColorModel.getRGBdefault();
1525        WritableRaster raster = colorModel.createCompatibleWritableRaster(img.getWidth(null), img.getHeight(null));
1526        String[] names = bi.getPropertyNames();
1527        Hashtable<String, Object> properties = new Hashtable<>(1 + (names != null ? names.length : 0));
1528        if (names != null) {
1529            for (String name : names) {
1530                properties.put(name, bi.getProperty(name));
1531            }
1532        }
1533        properties.put(PROP_TRANSPARENCY_FORCED, Boolean.TRUE);
1534        BufferedImage result = new BufferedImage(colorModel, raster, false, properties);
1535        Graphics2D g2 = result.createGraphics();
1536        g2.drawImage(img, 0, 0, null);
1537        g2.dispose();
1538        return result;
1539    }
1540
1541    /**
1542     * Determines if the transparency of the given {@code BufferedImage} has been enforced by a previous call to {@link #makeImageTransparent}.
1543     * @param bi The {@code BufferedImage} to test
1544     * @return {@code true} if the transparency of {@code bi} has been enforced by a previous call to {@code makeImageTransparent}.
1545     * @since 7132
1546     * @see #makeImageTransparent
1547     */
1548    public static boolean isTransparencyForced(BufferedImage bi) {
1549        return bi != null && !bi.getProperty(PROP_TRANSPARENCY_FORCED).equals(Image.UndefinedProperty);
1550    }
1551
1552    /**
1553     * Determines if the given {@code BufferedImage} has a transparent color determiend by a previous call to {@link #read}.
1554     * @param bi The {@code BufferedImage} to test
1555     * @return {@code true} if {@code bi} has a transparent color determined by a previous call to {@code read}.
1556     * @since 7132
1557     * @see #read
1558     */
1559    public static boolean hasTransparentColor(BufferedImage bi) {
1560        return bi != null && !bi.getProperty(PROP_TRANSPARENCY_COLOR).equals(Image.UndefinedProperty);
1561    }
1562}