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