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