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

Last change on this file since 12855 was 12798, checked in by Don-vip, 7 years ago

see #14794 - checkstyle

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