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