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