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