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

Last change on this file since 13647 was 13647, checked in by Don-vip, 6 years ago

see #16204 - Allow to start and close JOSM in WebStart sandbox mode (where every external access is denied). This was very useful to reproduce some very tricky bugs that occured in real life but were almost impossible to diagnose.

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