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}