source: josm/trunk/src/org/openstreetmap/josm/tools/ImageProvider.java

Last change on this file was 19553, checked in by stoecker, 3 weeks ago

fix #24677 - applied modified patch by dnet

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