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
|
---|
27 | * runtime.
|
---|
28 | *
|
---|
29 | * @since 12722
|
---|
30 | */
|
---|
31 | public 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 | }
|
---|