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}