[12722] | 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,
|
---|
[12732] | 26 | * in case it is on classpath. This is to be expected for Java 9, but not for Java 8 runtime.
|
---|
[12722] | 27 | *
|
---|
| 28 | * @since 12722
|
---|
| 29 | */
|
---|
[12798] | 30 | public final class HiDPISupport {
|
---|
[12722] | 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 |
|
---|
[12798] | 36 | private HiDPISupport() {
|
---|
| 37 | // Hide default constructor
|
---|
| 38 | }
|
---|
| 39 |
|
---|
[12722] | 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());
|
---|
[12732] | 71 | Optional<Constructor<? extends Image>> baseMrImageConstructor = getBaseMultiResolutionImageConstructor();
|
---|
| 72 | if (baseMrImageConstructor.isPresent()) {
|
---|
[12722] | 73 | try {
|
---|
[12732] | 74 | return baseMrImageConstructor.get().newInstance((Object) imgs.toArray(new Image[0]));
|
---|
[12722] | 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>
|
---|
[12732] | 85 | * Will return the argument <code>img</code> unchanged, if it is not a multi-resolution image.
|
---|
[12722] | 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) {
|
---|
[12732] | 91 | Optional<Class<? extends Image>> baseMrImageClass = getBaseMultiResolutionImageClass();
|
---|
| 92 | Optional<Method> resVariantsMethod = getResolutionVariantsMethod();
|
---|
| 93 | if (!baseMrImageClass.isPresent() || !resVariantsMethod.isPresent()) {
|
---|
[12722] | 94 | return img;
|
---|
| 95 | }
|
---|
[12732] | 96 | if (baseMrImageClass.get().isInstance(img)) {
|
---|
[12722] | 97 | try {
|
---|
| 98 | @SuppressWarnings("unchecked")
|
---|
[12732] | 99 | List<Image> imgVars = (List<Image>) resVariantsMethod.get().invoke(img);
|
---|
[12722] | 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>
|
---|
[12732] | 113 | * Will return the argument as a singleton list, in case it is not a multi-resolution image.
|
---|
[12722] | 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) {
|
---|
[12732] | 120 | Optional<Class<? extends Image>> baseMrImageClass = getBaseMultiResolutionImageClass();
|
---|
| 121 | Optional<Method> resVariantsMethod = getResolutionVariantsMethod();
|
---|
| 122 | if (!baseMrImageClass.isPresent() || !resVariantsMethod.isPresent()) {
|
---|
[12722] | 123 | return Collections.singletonList(img);
|
---|
| 124 | }
|
---|
[12732] | 125 | if (baseMrImageClass.get().isInstance(img)) {
|
---|
[12722] | 126 | try {
|
---|
| 127 | @SuppressWarnings("unchecked")
|
---|
[12732] | 128 | List<Image> imgVars = (List<Image>) resVariantsMethod.get().invoke(img);
|
---|
[12722] | 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() {
|
---|
[12724] | 147 | if (GraphicsEnvironment.isHeadless())
|
---|
| 148 | return 1.0;
|
---|
[12722] | 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")
|
---|
[12732] | 212 | Class<? extends Image> c = (Class<? extends Image>) Class.forName("java.awt.image.BaseMultiResolutionImage");
|
---|
[12722] | 213 | baseMultiResolutionImageClass = Optional.ofNullable(c);
|
---|
| 214 | } catch (ClassNotFoundException ex) {
|
---|
| 215 | // class is not present in Java 8
|
---|
| 216 | baseMultiResolutionImageClass = Optional.empty();
|
---|
[12868] | 217 | Logging.trace(ex);
|
---|
[12722] | 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 | }
|
---|