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 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.isEmpty(), "at least one 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 | Logging.trace(ex);
|
---|
218 | }
|
---|
219 | }
|
---|
220 | }
|
---|
221 | }
|
---|
222 | return baseMultiResolutionImageClass;
|
---|
223 | }
|
---|
224 |
|
---|
225 | private static Optional<Constructor<? extends Image>> getBaseMultiResolutionImageConstructor() {
|
---|
226 | if (baseMultiResolutionImageConstructor == null) {
|
---|
227 | synchronized (HiDPISupport.class) {
|
---|
228 | if (baseMultiResolutionImageConstructor == null) {
|
---|
229 | getBaseMultiResolutionImageClass().ifPresent(klass -> {
|
---|
230 | try {
|
---|
231 | Constructor<? extends Image> constr = klass.getConstructor(Image[].class);
|
---|
232 | baseMultiResolutionImageConstructor = Optional.ofNullable(constr);
|
---|
233 | } catch (NoSuchMethodException ex) {
|
---|
234 | Logging.error("Cannot find expected constructor: " + ex);
|
---|
235 | }
|
---|
236 | });
|
---|
237 | if (baseMultiResolutionImageConstructor == null) {
|
---|
238 | baseMultiResolutionImageConstructor = Optional.empty();
|
---|
239 | }
|
---|
240 | }
|
---|
241 | }
|
---|
242 | }
|
---|
243 | return baseMultiResolutionImageConstructor;
|
---|
244 | }
|
---|
245 |
|
---|
246 | private static Optional<Method> getResolutionVariantsMethod() {
|
---|
247 | if (resolutionVariantsMethod == null) {
|
---|
248 | synchronized (HiDPISupport.class) {
|
---|
249 | if (resolutionVariantsMethod == null) {
|
---|
250 | getBaseMultiResolutionImageClass().ifPresent(klass -> {
|
---|
251 | try {
|
---|
252 | Method m = klass.getMethod("getResolutionVariants");
|
---|
253 | resolutionVariantsMethod = Optional.ofNullable(m);
|
---|
254 | } catch (NoSuchMethodException ex) {
|
---|
255 | Logging.error("Cannot find expected method: "+ex);
|
---|
256 | }
|
---|
257 | });
|
---|
258 | if (resolutionVariantsMethod == null) {
|
---|
259 | resolutionVariantsMethod = Optional.empty();
|
---|
260 | }
|
---|
261 | }
|
---|
262 | }
|
---|
263 | }
|
---|
264 | return resolutionVariantsMethod;
|
---|
265 | }
|
---|
266 | }
|
---|