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
279    @Override
280    public String toString() {
281        return "ImageResource ["
282                + (svg != null ? "svg=" + svg : "")
283                + (baseImage != null ? "baseImage=" + baseImage : "") + ']';
284    }
285}