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