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