source: josm/trunk/src/org/openstreetmap/josm/tools/ImageResource.java@ 17364

Last change on this file since 17364 was 17364, checked in by simon04, 3 years ago

see #20141 - ImageProvider: cache rendered SVG images using JCS

This experimental feature is disabled by default. Set the advanced preference jcs.cache.use_image_resource_cache=true to enable. No cache eviction for altered images is implemented at the moment.

  • Property svn:eol-style set to native
File size: 11.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import java.awt.Dimension;
5import java.awt.Image;
6import java.awt.image.BufferedImage;
7import java.io.IOException;
8import java.io.UncheckedIOException;
9import java.util.List;
10import java.util.Locale;
11import java.util.Objects;
12import java.util.function.Supplier;
13
14import javax.swing.AbstractAction;
15import javax.swing.Action;
16import javax.swing.Icon;
17import javax.swing.ImageIcon;
18import javax.swing.JPanel;
19import javax.swing.UIManager;
20
21import org.apache.commons.jcs3.access.behavior.ICacheAccess;
22import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
23import org.openstreetmap.josm.data.cache.JCSCacheManager;
24
25import com.kitfox.svg.SVGDiagram;
26
27/**
28 * Holds data for one particular image.
29 * It can be backed by a svg or raster image.
30 *
31 * In the first case, <code>svg</code> is not <code>null</code> and in the latter case,
32 * <code>baseImage</code> is not <code>null</code>.
33 * @since 4271
34 */
35public class ImageResource {
36
37 /**
38 * Caches the image data for resized versions of the same image.
39 * Depending on {@link JCSCacheManager#USE_IMAGE_RESOURCE_CACHE}, a combined memory/disk, or an in-memory-cache is used.
40 */
41 private static final ICacheAccess<String, BufferedImageCacheEntry> imgCache = JCSCacheManager.getImageResourceCache();
42
43 private final String cacheKey;
44 /**
45 * SVG diagram information in case of SVG vector image.
46 */
47 private SVGDiagram svg;
48 /**
49 * Supplier for SVG diagram information in case of possibly cached SVG vector image.
50 */
51 private Supplier<SVGDiagram> svgSupplier;
52 /**
53 * Use this dimension to request original file dimension.
54 */
55 public static final Dimension DEFAULT_DIMENSION = new Dimension(-1, -1);
56 /**
57 * ordered list of overlay images
58 */
59 protected List<ImageOverlay> overlayInfo;
60 /**
61 * <code>true</code> if icon must be grayed out
62 */
63 protected boolean isDisabled;
64 /**
65 * The base raster image for the final output
66 */
67 private Image baseImage;
68
69 /**
70 * Constructs a new {@code ImageResource} from an image.
71 * @param cacheKey the caching identifier of the image
72 * @param img the image
73 */
74 public ImageResource(String cacheKey, Image img) {
75 this.cacheKey = Objects.requireNonNull(cacheKey);
76 this.baseImage = Objects.requireNonNull(img);
77 }
78
79 /**
80 * Constructs a new {@code ImageResource} from SVG data.
81 * @param cacheKey the caching identifier of the image
82 * @param svg SVG data
83 */
84 public ImageResource(String cacheKey, SVGDiagram svg) {
85 this.cacheKey = Objects.requireNonNull(cacheKey);
86 this.svg = Objects.requireNonNull(svg);
87 }
88
89 public ImageResource(String cacheKey, Supplier<SVGDiagram> svgSupplier) {
90 this.cacheKey = Objects.requireNonNull(cacheKey);
91 this.svgSupplier = Objects.requireNonNull(svgSupplier);
92 }
93
94 /**
95 * Constructs a new {@code ImageResource} from another one and sets overlays.
96 * @param res the existing resource
97 * @param overlayInfo the overlay to apply
98 * @since 8095
99 */
100 public ImageResource(ImageResource res, List<ImageOverlay> overlayInfo) {
101 this.cacheKey = res.cacheKey;
102 this.svg = res.svg;
103 this.svgSupplier = res.svgSupplier;
104 this.baseImage = res.baseImage;
105 this.overlayInfo = overlayInfo;
106 }
107
108 /**
109 * Set, if image must be filtered to grayscale so it will look like disabled icon.
110 *
111 * @param disabled true, if image must be grayed out for disabled state
112 * @return the current object, for convenience
113 * @since 10428
114 */
115 public ImageResource setDisabled(boolean disabled) {
116 this.isDisabled = disabled;
117 return this;
118 }
119
120 /**
121 * Set both icons of an Action
122 * @param a The action for the icons
123 * @since 10369
124 */
125 public void attachImageIcon(AbstractAction a) {
126 Dimension iconDimension = ImageProvider.ImageSizes.SMALLICON.getImageDimension();
127 ImageIcon icon = getImageIcon(iconDimension);
128 a.putValue(Action.SMALL_ICON, icon);
129
130 iconDimension = ImageProvider.ImageSizes.LARGEICON.getImageDimension();
131 icon = getImageIcon(iconDimension);
132 a.putValue(Action.LARGE_ICON_KEY, icon);
133 }
134
135 /**
136 * Set both icons of an Action
137 * @param a The action for the icons
138 * @param attachImageResource Adds an resource named "ImageResource" if <code>true</code>
139 * @since 10369
140 */
141 public void attachImageIcon(AbstractAction a, boolean attachImageResource) {
142 attachImageIcon(a);
143 if (attachImageResource) {
144 a.putValue("ImageResource", this);
145 }
146 }
147
148 /**
149 * Returns the image icon at default dimension.
150 * @return the image icon at default dimension
151 */
152 public ImageIcon getImageIcon() {
153 return getImageIcon(DEFAULT_DIMENSION);
154 }
155
156 /**
157 * Get an ImageIcon object for the image of this resource.
158 * <p>
159 * Will return a multi-resolution image by default (if possible).
160 * @param dim The requested dimensions. Use (-1,-1) for the original size and (width, -1)
161 * to set the width, but otherwise scale the image proportionally.
162 * @return ImageIcon object for the image of this resource, scaled according to dim
163 * @see #getImageIconBounded(java.awt.Dimension)
164 */
165 public ImageIcon getImageIcon(Dimension dim) {
166 return getImageIcon(dim, true, null);
167 }
168
169 /**
170 * Get an ImageIcon object for the image of this resource.
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 resizeMode how to size/resize the image
177 * @return ImageIcon object for the image of this resource, scaled according to dim
178 * @since 12722
179 */
180 ImageIcon getImageIcon(Dimension dim, boolean multiResolution, ImageResizeMode resizeMode) {
181 return getImageIconAlreadyScaled(GuiSizesHelper.getDimensionDpiAdjusted(dim), multiResolution, false, resizeMode);
182 }
183
184 private BufferedImage getImageFromCache(String cacheKey) {
185 try {
186 BufferedImageCacheEntry image = imgCache.get(cacheKey);
187 if (image == null || image.getImage() == null) {
188 return null;
189 }
190 Logging.trace("{0} is in cache :-)", cacheKey);
191 return image.getImage();
192 } catch (IOException e) {
193 throw new UncheckedIOException(e);
194 }
195 }
196
197 /**
198 * Get an ImageIcon object for the image of this resource. A potential UI scaling is assumed
199 * to be already taken care of, so dim is already scaled accordingly.
200 * @param dim The requested dimensions. Use (-1,-1) for the original size and (width, -1)
201 * to set the width, but otherwise scale the image proportionally.
202 * @param multiResolution If true, return a multi-resolution image
203 * (java.awt.image.MultiResolutionImage in Java 9), otherwise a plain {@link BufferedImage}.
204 * When running Java 8, this flag has no effect and a plain image will be returned in any case.
205 * @param highResolution whether the high resolution variant should be used for overlays
206 * @param resizeMode how to size/resize the image
207 * @return ImageIcon object for the image of this resource, scaled according to dim
208 */
209 ImageIcon getImageIconAlreadyScaled(Dimension dim, boolean multiResolution, boolean highResolution, ImageResizeMode resizeMode) {
210 CheckParameterUtil.ensureThat((dim.width > 0 || dim.width == -1) && (dim.height > 0 || dim.height == -1),
211 () -> dim + " is invalid");
212
213 if (resizeMode == null && svg != null) {
214 // upscale SVG icons
215 resizeMode = ImageResizeMode.AUTO;
216 } else if (resizeMode == null) {
217 resizeMode = ImageResizeMode.BOUNDED;
218 }
219 final String cacheKey = String.format(Locale.ROOT, "%s--%s--%d--%d",
220 this.cacheKey, resizeMode.name(), dim.width, dim.height);
221 BufferedImage img = getImageFromCache(cacheKey);
222 if (img == null) {
223 if (svgSupplier != null) {
224 svg = svgSupplier.get();
225 Logging.trace("{0} is not in cache :-(", cacheKey);
226 svgSupplier = null;
227 }
228 if (svg != null) {
229 img = ImageProvider.createImageFromSvg(svg, dim, resizeMode);
230 if (img == null) {
231 return null;
232 }
233 } else {
234 if (baseImage == null) throw new AssertionError();
235 ImageIcon icon = new ImageIcon(baseImage);
236 img = resizeMode.createBufferedImage(dim, new Dimension(icon.getIconWidth(), icon.getIconHeight()),
237 null, icon.getImage());
238 }
239 if (overlayInfo != null) {
240 for (ImageOverlay o : overlayInfo) {
241 o.process(img, highResolution);
242 }
243 }
244 if (isDisabled) {
245 //Use default Swing functionality to make icon look disabled by applying grayscaling filter.
246 Icon disabledIcon = UIManager.getLookAndFeel().getDisabledIcon(null, new ImageIcon(img));
247 if (disabledIcon == null) {
248 return null;
249 }
250
251 //Convert Icon to ImageIcon with BufferedImage inside
252 img = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
253 disabledIcon.paintIcon(new JPanel(), img.getGraphics(), 0, 0);
254 }
255 if (img == null) {
256 return null;
257 }
258 BufferedImageCacheEntry cacheEntry = BufferedImageCacheEntry.pngEncoded(img);
259 Logging.trace("Storing {0} ({1} bytes) in cache...", cacheKey, cacheEntry.getContent().length);
260 imgCache.put(cacheKey, cacheEntry);
261 }
262
263 if (!multiResolution)
264 return new ImageIcon(img);
265 else {
266 try {
267 Image mrImg = HiDPISupport.getMultiResolutionImage(img, this, resizeMode);
268 return new ImageIcon(mrImg);
269 } catch (NoClassDefFoundError e) {
270 Logging.trace(e);
271 return new ImageIcon(img);
272 }
273 }
274 }
275
276 /**
277 * Get image icon with a certain maximum size. The image is scaled down
278 * to fit maximum dimensions. (Keeps aspect ratio)
279 * <p>
280 * Will return a multi-resolution image by default (if possible).
281 *
282 * @param maxSize The maximum size. One of the dimensions (width or height) can be -1,
283 * which means it is not bounded.
284 * @return ImageIcon object for the image of this resource, scaled down if needed, according to maxSize
285 */
286 public ImageIcon getImageIconBounded(Dimension maxSize) {
287 return getImageIcon(maxSize, true, ImageResizeMode.BOUNDED);
288 }
289
290 /**
291 * Returns an {@link ImageIcon} for the given map image, at the specified size.
292 * Uses a cache to improve performance.
293 * @param iconSize size in pixels
294 * @return an {@code ImageIcon} for the given map image, at the specified size
295 */
296 public ImageIcon getPaddedIcon(Dimension iconSize) {
297 return getImageIcon(iconSize, true, ImageResizeMode.PADDED);
298 }
299
300 static String statistics() {
301 return String.format("ImageResource cache: [%s]", imgCache.getStatistics());
302 }
303
304 @Override
305 public String toString() {
306 return "ImageResource ["
307 + (svg != null ? "svg=" + svg : "")
308 + (baseImage != null ? "baseImage=" + baseImage : "") + ']';
309 }
310}
Note: See TracBrowser for help on using the repository browser.