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

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

see #10684 - fix javadoc

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