1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.tools;
|
---|
3 |
|
---|
4 | import java.awt.Dimension;
|
---|
5 | import java.awt.GraphicsConfiguration;
|
---|
6 | import java.awt.GraphicsEnvironment;
|
---|
7 | import java.awt.Image;
|
---|
8 | import java.awt.geom.AffineTransform;
|
---|
9 | import java.lang.reflect.Constructor;
|
---|
10 | import java.lang.reflect.InvocationTargetException;
|
---|
11 | import java.lang.reflect.Method;
|
---|
12 | import java.util.Arrays;
|
---|
13 | import java.util.Collections;
|
---|
14 | import java.util.List;
|
---|
15 | import java.util.function.Function;
|
---|
16 | import java.util.function.UnaryOperator;
|
---|
17 | import java.util.stream.Collectors;
|
---|
18 | import java.util.stream.IntStream;
|
---|
19 |
|
---|
20 | import 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 | */
|
---|
30 | public 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 | }
|
---|