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