001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.awt.Dimension; 005import java.awt.Image; 006import java.awt.image.BufferedImage; 007import java.util.List; 008import java.util.Map; 009import java.util.concurrent.ConcurrentHashMap; 010 011import javax.swing.AbstractAction; 012import javax.swing.Action; 013import javax.swing.Icon; 014import javax.swing.ImageIcon; 015import javax.swing.JPanel; 016import javax.swing.UIManager; 017 018import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 019 020import com.kitfox.svg.SVGDiagram; 021 022/** 023 * Holds data for one particular image. 024 * It can be backed by a svg or raster image. 025 * 026 * In the first case, <code>svg</code> is not <code>null</code> and in the latter case, 027 * <code>baseImage</code> is not <code>null</code>. 028 * @since 4271 029 */ 030public class ImageResource { 031 032 /** 033 * Caches the image data for resized versions of the same image. The key is obtained using {@link ImageResizeMode#cacheKey(Dimension)}. 034 */ 035 private final Map<Integer, BufferedImage> imgCache = new ConcurrentHashMap<>(4); 036 /** 037 * SVG diagram information in case of SVG vector image. 038 */ 039 private SVGDiagram svg; 040 /** 041 * Use this dimension to request original file dimension. 042 */ 043 public static final Dimension DEFAULT_DIMENSION = new Dimension(-1, -1); 044 /** 045 * ordered list of overlay images 046 */ 047 protected List<ImageOverlay> overlayInfo; 048 /** 049 * <code>true</code> if icon must be grayed out 050 */ 051 protected boolean isDisabled; 052 /** 053 * The base raster image for the final output 054 */ 055 private Image baseImage; 056 057 /** 058 * Constructs a new {@code ImageResource} from an image. 059 * @param img the image 060 */ 061 public ImageResource(Image img) { 062 CheckParameterUtil.ensureParameterNotNull(img); 063 baseImage = img; 064 } 065 066 /** 067 * Constructs a new {@code ImageResource} from SVG data. 068 * @param svg SVG data 069 */ 070 public ImageResource(SVGDiagram svg) { 071 CheckParameterUtil.ensureParameterNotNull(svg); 072 this.svg = svg; 073 } 074 075 /** 076 * Constructs a new {@code ImageResource} from another one and sets overlays. 077 * @param res the existing resource 078 * @param overlayInfo the overlay to apply 079 * @since 8095 080 */ 081 public ImageResource(ImageResource res, List<ImageOverlay> overlayInfo) { 082 this.svg = res.svg; 083 this.baseImage = res.baseImage; 084 this.overlayInfo = overlayInfo; 085 } 086 087 /** 088 * Set, if image must be filtered to grayscale so it will look like disabled icon. 089 * 090 * @param disabled true, if image must be grayed out for disabled state 091 * @return the current object, for convenience 092 * @since 10428 093 */ 094 public ImageResource setDisabled(boolean disabled) { 095 this.isDisabled = disabled; 096 return this; 097 } 098 099 /** 100 * Set both icons of an Action 101 * @param a The action for the icons 102 * @since 10369 103 */ 104 public void attachImageIcon(AbstractAction a) { 105 a.putValue(Action.SMALL_ICON, getImageIcon(ImageSizes.SMALLICON.getImageDimension())); 106 a.putValue(Action.LARGE_ICON_KEY, getImageIcon(ImageSizes.LARGEICON.getImageDimension())); 107 } 108 109 /** 110 * Set both icons of an Action 111 * @param a The action for the icons 112 * @param attachImageResource Adds an resource named "ImageResource" if <code>true</code> 113 * @since 10369 114 */ 115 public void attachImageIcon(AbstractAction a, boolean attachImageResource) { 116 attachImageIcon(a); 117 if (attachImageResource) { 118 a.putValue("ImageResource", this); 119 } 120 } 121 122 /** 123 * Returns the {@code ImageResource} attached to the given action, if any. 124 * @param a action 125 * @return the {@code ImageResource} attached to the given action, or {@code null} 126 * @since 18099 127 */ 128 public static ImageResource getAttachedImageResource(Action a) { 129 return (ImageResource) a.getValue("ImageResource"); 130 } 131 132 /** 133 * Returns the image icon at default dimension. 134 * @return the image icon at default dimension 135 */ 136 public ImageIcon getImageIcon() { 137 return getImageIcon(DEFAULT_DIMENSION); 138 } 139 140 /** 141 * Get an ImageIcon object for the image of this resource. 142 * <p> 143 * Will return a multi-resolution image by default (if possible). 144 * @param dim The requested dimensions. Use (-1,-1) for the original size and (width, -1) 145 * to set the width, but otherwise scale the image proportionally. 146 * @return ImageIcon object for the image of this resource, scaled according to dim 147 * @see #getImageIconBounded(java.awt.Dimension) 148 */ 149 public ImageIcon getImageIcon(Dimension dim) { 150 return getImageIcon(dim, true, null); 151 } 152 153 /** 154 * Get an ImageIcon object for the image of this resource. 155 * @param dim The requested dimensions. Use (-1,-1) for the original size and (width, -1) 156 * to set the width, but otherwise scale the image proportionally. 157 * @param multiResolution If true, return a multi-resolution image 158 * (java.awt.image.MultiResolutionImage in Java 9), otherwise a plain {@link BufferedImage}. 159 * When running Java 8, this flag has no effect and a plain image will be returned in any case. 160 * @param resizeMode how to size/resize the image 161 * @return ImageIcon object for the image of this resource, scaled according to dim 162 * @since 12722 163 */ 164 ImageIcon getImageIcon(Dimension dim, boolean multiResolution, ImageResizeMode resizeMode) { 165 return getImageIconAlreadyScaled(GuiSizesHelper.getDimensionDpiAdjusted(dim), multiResolution, false, resizeMode); 166 } 167 168 /** 169 * Get an ImageIcon object for the image of this resource. A potential UI scaling is assumed 170 * to be already taken care of, so dim is already scaled accordingly. 171 * @param dim The requested dimensions. Use (-1,-1) for the original size and (width, -1) 172 * to set the width, but otherwise scale the image proportionally. 173 * @param multiResolution If true, return a multi-resolution image 174 * (java.awt.image.MultiResolutionImage in Java 9), otherwise a plain {@link BufferedImage}. 175 * When running Java 8, this flag has no effect and a plain image will be returned in any case. 176 * @param highResolution whether the high resolution variant should be used for overlays 177 * @param resizeMode how to size/resize the image 178 * @return ImageIcon object for the image of this resource, scaled according to dim 179 */ 180 ImageIcon getImageIconAlreadyScaled(Dimension dim, boolean multiResolution, boolean highResolution, ImageResizeMode resizeMode) { 181 CheckParameterUtil.ensureThat((dim.width > 0 || dim.width == -1) && (dim.height > 0 || dim.height == -1), 182 () -> dim + " is invalid"); 183 184 if (resizeMode == null && svg != null) { 185 // upscale SVG icons 186 resizeMode = ImageResizeMode.AUTO; 187 } else if (resizeMode == null) { 188 resizeMode = ImageResizeMode.BOUNDED; 189 } 190 final int cacheKey = resizeMode.cacheKey(dim); 191 BufferedImage img = imgCache.get(cacheKey); 192 if (img == null) { 193 if (svg != null) { 194 img = ImageProvider.createImageFromSvg(svg, dim, resizeMode); 195 if (img == null) { 196 return null; 197 } 198 } else { 199 if (baseImage == null) throw new AssertionError(); 200 ImageIcon icon = new ImageIcon(baseImage); 201 if (dim.width == icon.getIconWidth() && dim.height == icon.getIconHeight()) { 202 return icon; 203 } 204 205 img = resizeMode.createBufferedImage(dim, new Dimension(icon.getIconWidth(), icon.getIconHeight()), 206 null, icon.getImage()); 207 } 208 if (overlayInfo != null) { 209 for (ImageOverlay o : overlayInfo) { 210 o.process(img, highResolution); 211 } 212 } 213 if (isDisabled) { 214 //Use default Swing functionality to make icon look disabled by applying grayscaling filter. 215 Icon disabledIcon = UIManager.getLookAndFeel().getDisabledIcon(null, new ImageIcon(img)); 216 if (disabledIcon == null) { 217 return null; 218 } 219 220 //Convert Icon to ImageIcon with BufferedImage inside 221 img = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); 222 disabledIcon.paintIcon(new JPanel(), img.getGraphics(), 0, 0); 223 } 224 imgCache.put(cacheKey, img); 225 } 226 227 if (!multiResolution || svg == null) 228 return new ImageIcon(img); 229 else { 230 try { 231 Image mrImg = HiDPISupport.getMultiResolutionImage(img, this, resizeMode); 232 return new ImageIcon(mrImg); 233 } catch (NoClassDefFoundError e) { 234 Logging.trace(e); 235 return new ImageIcon(img); 236 } 237 } 238 } 239 240 /** 241 * Get image icon with a certain maximum size. The image is scaled down 242 * to fit maximum dimensions. (Keeps aspect ratio) 243 * <p> 244 * Will return a multi-resolution image by default (if possible). 245 * 246 * @param maxSize The maximum size. One of the dimensions (width or height) can be -1, 247 * which means it is not bounded. 248 * @return ImageIcon object for the image of this resource, scaled down if needed, according to maxSize 249 */ 250 public ImageIcon getImageIconBounded(Dimension maxSize) { 251 return getImageIcon(maxSize, true, ImageResizeMode.BOUNDED); 252 } 253 254 /** 255 * Returns an {@link ImageIcon} for the given map image, at the specified size. 256 * Uses a cache to improve performance. 257 * @param iconSize size in pixels 258 * @return an {@code ImageIcon} for the given map image, at the specified size 259 */ 260 public ImageIcon getPaddedIcon(Dimension iconSize) { 261 return getImageIcon(iconSize, true, ImageResizeMode.PADDED); 262 } 263 264 @Override 265 public String toString() { 266 return "ImageResource [" 267 + (svg != null ? "svg=" + svg : "") 268 + (baseImage != null ? "baseImage=" + baseImage : "") + ']'; 269 } 270}