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