source: josm/trunk/src/org/openstreetmap/josm/tools/HiDPISupport.java@ 16980

Last change on this file since 16980 was 16980, checked in by simon04, 4 years ago

see #19706, see #19725 - Fix HiDPI with ImageResizeMode.BOUNDED

Regression of r16978

File size: 12.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import java.awt.Dimension;
5import java.awt.GraphicsConfiguration;
6import java.awt.GraphicsEnvironment;
7import java.awt.Image;
8import java.awt.geom.AffineTransform;
9import java.lang.reflect.Constructor;
10import java.lang.reflect.InvocationTargetException;
11import java.lang.reflect.Method;
12import java.util.Arrays;
13import java.util.Collections;
14import java.util.List;
15import java.util.function.Function;
16import java.util.function.UnaryOperator;
17import java.util.stream.Collectors;
18import java.util.stream.IntStream;
19
20import javax.swing.ImageIcon;
21
22/**
23 * Helper class for HiDPI support.
24 *
25 * Gives access to the class <code>BaseMultiResolutionImage</code> via reflection,
26 * in case it is on classpath. This is to be expected for Java 9, but not for Java 8 runtime.
27 *
28 * @since 12722
29 */
30public final class HiDPISupport {
31
32 private static final Class<? extends Image> baseMultiResolutionImageClass;
33 private static final Constructor<? extends Image> baseMultiResolutionImageConstructor;
34 private static final Method resolutionVariantsMethod;
35 private static final Method resolutionVariantMethod;
36
37 static {
38 baseMultiResolutionImageClass = initBaseMultiResolutionImageClass();
39 baseMultiResolutionImageConstructor = initBaseMultiResolutionImageConstructor();
40 resolutionVariantsMethod = initResolutionVariantsMethod();
41 resolutionVariantMethod = initResolutionVariantMethod();
42 }
43
44 private HiDPISupport() {
45 // Hide default constructor
46 }
47
48 /**
49 * Create a multi-resolution image from a base image and an {@link ImageResource}.
50 * <p>
51 * Will only return multi-resolution image, if HiDPI-mode is detected. Then
52 * the image stack will consist of the base image and one that fits the
53 * HiDPI scale of the main display.
54 * @param base the base image
55 * @param ir a corresponding image resource
56 * @param resizeMode how to size/resize the image
57 * @return multi-resolution image if necessary and possible, the base image otherwise
58 */
59 public static Image getMultiResolutionImage(Image base, ImageResource ir, ImageResizeMode resizeMode) {
60 double uiScale = getHiDPIScale();
61 if (uiScale != 1.0 && baseMultiResolutionImageConstructor != null) {
62 if (resizeMode == ImageResizeMode.BOUNDED) {
63 resizeMode = ImageResizeMode.AUTO;
64 }
65 ImageIcon zoomed = ir.getImageIconAlreadyScaled(new Dimension(
66 (int) Math.round(base.getWidth(null) * uiScale),
67 (int) Math.round(base.getHeight(null) * uiScale)), false, true, resizeMode);
68 Image mrImg = getMultiResolutionImage(Arrays.asList(base, zoomed.getImage()));
69 if (mrImg != null) return mrImg;
70 }
71 return base;
72 }
73
74 /**
75 * Create a multi-resolution image from a list of images.
76 * @param imgs the images, supposedly the same image at different resolutions,
77 * must not be empty
78 * @return corresponding multi-resolution image, if possible, the first image
79 * in the list otherwise
80 */
81 public static Image getMultiResolutionImage(List<Image> imgs) {
82 CheckParameterUtil.ensureThat(!imgs.isEmpty(), "imgs is empty");
83 if (baseMultiResolutionImageConstructor != null) {
84 try {
85 return baseMultiResolutionImageConstructor.newInstance((Object) imgs.toArray(new Image[0]));
86 } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
87 Logging.error("Unexpected error while instantiating object of class BaseMultiResolutionImage: " + ex);
88 }
89 }
90 return imgs.get(0);
91 }
92
93 /**
94 * Wrapper for the method <code>java.awt.image.BaseMultiResolutionImage#getBaseImage()</code>.
95 * <p>
96 * Will return the argument <code>img</code> unchanged, if it is not a multi-resolution image.
97 * @param img the image
98 * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>,
99 * then the base image, otherwise the image itself
100 */
101 public static Image getBaseImage(Image img) {
102 if (baseMultiResolutionImageClass == null || resolutionVariantsMethod == null) {
103 return img;
104 }
105 if (baseMultiResolutionImageClass.isInstance(img)) {
106 try {
107 @SuppressWarnings("unchecked")
108 List<Image> imgVars = (List<Image>) resolutionVariantsMethod.invoke(img);
109 if (!imgVars.isEmpty()) {
110 return imgVars.get(0);
111 }
112 } catch (IllegalAccessException | InvocationTargetException ex) {
113 Logging.error("Unexpected error while calling method: " + ex);
114 }
115 }
116 return img;
117 }
118
119 /**
120 * Wrapper for the method <code>java.awt.image.MultiResolutionImage#getResolutionVariants()</code>.
121 * <p>
122 * Will return the argument as a singleton list, in case it is not a multi-resolution image.
123 * @param img the image
124 * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>,
125 * then the result of the method <code>#getResolutionVariants()</code>, otherwise the image
126 * itself as a singleton list
127 */
128 public static List<Image> getResolutionVariants(Image img) {
129 if (baseMultiResolutionImageClass == null || resolutionVariantsMethod == null) {
130 return Collections.singletonList(img);
131 }
132 if (baseMultiResolutionImageClass.isInstance(img)) {
133 try {
134 @SuppressWarnings("unchecked")
135 List<Image> imgVars = (List<Image>) resolutionVariantsMethod.invoke(img);
136 if (!imgVars.isEmpty()) {
137 return imgVars;
138 }
139 } catch (IllegalAccessException | InvocationTargetException ex) {
140 Logging.error("Unexpected error while calling method: " + ex);
141 }
142 }
143 return Collections.singletonList(img);
144 }
145
146 /**
147 * Wrapper for method <code>java.awt.image.MultiResolutionImage#getResolutionVariant(double destImageWidth, double destImageHeight)</code>.
148 * <p>
149 * Will return the argument, in case it is not a multi-resolution image.
150 * @param img the image
151 * @param destImageWidth the width of the destination image
152 * @param destImageHeight the height of the destination image
153 * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>,
154 * then the result of the method <code>#getResolutionVariant(destImageWidth, destImageHeight)</code>,
155 * otherwise the image itself
156 */
157 public static Image getResolutionVariant(Image img, double destImageWidth, double destImageHeight) {
158 if (baseMultiResolutionImageClass == null || resolutionVariantsMethod == null) {
159 return img;
160 }
161 if (baseMultiResolutionImageClass.isInstance(img)) {
162 try {
163 return (Image) resolutionVariantMethod.invoke(img, destImageWidth, destImageHeight);
164 } catch (IllegalAccessException | InvocationTargetException ex) {
165 Logging.error("Unexpected error while calling method: " + ex);
166 }
167 }
168 return img;
169 }
170
171 /**
172 * Detect the GUI scale for HiDPI mode.
173 * <p>
174 * This method may not work as expected for a multi-monitor setup. It will
175 * only take the default screen device into account.
176 * @return the GUI scale for HiDPI mode, a value of 1.0 means standard mode.
177 */
178 static double getHiDPIScale() {
179 if (GraphicsEnvironment.isHeadless())
180 return 1.0;
181 GraphicsConfiguration gc = GraphicsEnvironment
182 .getLocalGraphicsEnvironment()
183 .getDefaultScreenDevice().
184 getDefaultConfiguration();
185 AffineTransform transform = gc.getDefaultTransform();
186 if (!Utils.equalsEpsilon(transform.getScaleX(), transform.getScaleY())) {
187 Logging.warn("Unexpected ui transform: " + transform);
188 }
189 return transform.getScaleX();
190 }
191
192 /**
193 * Perform an operation on multi-resolution images.
194 *
195 * When input image is not multi-resolution, it will simply apply the processor once.
196 * Otherwise, the processor will be called for each resolution variant and the
197 * resulting images assembled to become the output multi-resolution image.
198 * @param img input image, possibly multi-resolution
199 * @param processor processor taking a plain image as input and returning a single
200 * plain image as output
201 * @return multi-resolution image assembled from the output of calls to <code>processor</code>
202 * for each resolution variant
203 */
204 public static Image processMRImage(Image img, UnaryOperator<Image> processor) {
205 return processMRImages(Collections.singletonList(img), imgs -> processor.apply(imgs.get(0)));
206 }
207
208 /**
209 * Perform an operation on multi-resolution images.
210 *
211 * When input images are not multi-resolution, it will simply apply the processor once.
212 * Otherwise, the processor will be called for each resolution variant and the
213 * resulting images assembled to become the output multi-resolution image.
214 * @param imgs input images, possibly multi-resolution
215 * @param processor processor taking a list of plain images as input and returning
216 * a single plain image as output
217 * @return multi-resolution image assembled from the output of calls to <code>processor</code>
218 * for each resolution variant
219 */
220 public static Image processMRImages(List<Image> imgs, Function<List<Image>, Image> processor) {
221 CheckParameterUtil.ensureThat(!imgs.isEmpty(), "at least one element expected");
222 if (baseMultiResolutionImageClass != null) {
223 return processor.apply(imgs);
224 }
225 List<List<Image>> allVars = imgs.stream().map(HiDPISupport::getResolutionVariants).collect(Collectors.toList());
226 int maxVariants = allVars.stream().mapToInt(List<Image>::size).max().getAsInt();
227 if (maxVariants == 1)
228 return processor.apply(imgs);
229 List<Image> imgsProcessed = IntStream.range(0, maxVariants)
230 .mapToObj(
231 k -> processor.apply(
232 allVars.stream().map(vars -> vars.get(k)).collect(Collectors.toList())
233 )
234 ).collect(Collectors.toList());
235 return getMultiResolutionImage(imgsProcessed);
236 }
237
238 @SuppressWarnings("unchecked")
239 private static Class<? extends Image> initBaseMultiResolutionImageClass() {
240 try {
241 return (Class<? extends Image>) Class.forName("java.awt.image.BaseMultiResolutionImage");
242 } catch (ClassNotFoundException ex) {
243 // class is not present in Java 8
244 Logging.trace(ex);
245 return null;
246 }
247 }
248
249 private static Constructor<? extends Image> initBaseMultiResolutionImageConstructor() {
250 try {
251 return baseMultiResolutionImageClass != null
252 ? baseMultiResolutionImageClass.getConstructor(Image[].class)
253 : null;
254 } catch (NoSuchMethodException ex) {
255 Logging.error("Cannot find expected constructor: " + ex);
256 return null;
257 }
258 }
259
260 private static Method initResolutionVariantsMethod() {
261 try {
262 return baseMultiResolutionImageClass != null
263 ? baseMultiResolutionImageClass.getMethod("getResolutionVariants")
264 : null;
265 } catch (NoSuchMethodException ex) {
266 Logging.error("Cannot find expected method: " + ex);
267 return null;
268 }
269 }
270
271 private static Method initResolutionVariantMethod() {
272 try {
273 return baseMultiResolutionImageClass != null
274 ? baseMultiResolutionImageClass.getMethod("getResolutionVariant", Double.TYPE, Double.TYPE)
275 : null;
276 } catch (NoSuchMethodException ex) {
277 Logging.error("Cannot find expected method: " + ex);
278 return null;
279 }
280 }
281}
Note: See TracBrowser for help on using the repository browser.