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