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

Last change on this file since 12724 was 12724, checked in by bastiK, 7 years ago

see #9995 - fix headless mode

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