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