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