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