001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.awt.Dimension; 005import java.awt.GraphicsConfiguration; 006import java.awt.GraphicsEnvironment; 007import java.awt.Image; 008import java.awt.geom.AffineTransform; 009import java.lang.reflect.Constructor; 010import java.lang.reflect.InvocationTargetException; 011import java.lang.reflect.Method; 012import java.util.Arrays; 013import java.util.Collections; 014import java.util.List; 015import java.util.function.Function; 016import java.util.function.UnaryOperator; 017import java.util.stream.Collectors; 018import java.util.stream.IntStream; 019 020import javax.swing.ImageIcon; 021 022/** 023 * Helper class for HiDPI support. 024 * 025 * Gives access to the class <code>BaseMultiResolutionImage</code> via reflection, 026 * in case it is on classpath. This is to be expected for Java 9, but not for Java 8 runtime. 027 * 028 * @since 12722 029 */ 030public final class HiDPISupport { 031 032 private static final Class<? extends Image> baseMultiResolutionImageClass; 033 private static final Constructor<? extends Image> baseMultiResolutionImageConstructor; 034 private static final Method resolutionVariantsMethod; 035 private static final Method resolutionVariantMethod; 036 037 static { 038 baseMultiResolutionImageClass = initBaseMultiResolutionImageClass(); 039 baseMultiResolutionImageConstructor = initBaseMultiResolutionImageConstructor(); 040 resolutionVariantsMethod = initResolutionVariantsMethod(); 041 resolutionVariantMethod = initResolutionVariantMethod(); 042 } 043 044 private HiDPISupport() { 045 // Hide default constructor 046 } 047 048 /** 049 * Create a multi-resolution image from a base image and an {@link ImageResource}. 050 * <p> 051 * Will only return multi-resolution image, if HiDPI-mode is detected. Then 052 * the image stack will consist of the base image and one that fits the 053 * HiDPI scale of the main display. 054 * @param base the base image 055 * @param ir a corresponding image resource 056 * @param resizeMode how to size/resize the image 057 * @return multi-resolution image if necessary and possible, the base image otherwise 058 */ 059 public static Image getMultiResolutionImage(Image base, ImageResource ir, ImageResizeMode resizeMode) { 060 double uiScale = getHiDPIScale(); 061 if (uiScale != 1.0 && baseMultiResolutionImageConstructor != null) { 062 if (resizeMode == ImageResizeMode.BOUNDED) { 063 resizeMode = ImageResizeMode.AUTO; 064 } 065 ImageIcon zoomed = ir.getImageIconAlreadyScaled(new Dimension( 066 (int) Math.round(base.getWidth(null) * uiScale), 067 (int) Math.round(base.getHeight(null) * uiScale)), false, true, resizeMode); 068 Image mrImg = getMultiResolutionImage(Arrays.asList(base, zoomed.getImage())); 069 if (mrImg != null) return mrImg; 070 } 071 return base; 072 } 073 074 /** 075 * Create a multi-resolution image from a list of images. 076 * @param imgs the images, supposedly the same image at different resolutions, 077 * must not be empty 078 * @return corresponding multi-resolution image, if possible, the first image 079 * in the list otherwise 080 */ 081 public static Image getMultiResolutionImage(List<Image> imgs) { 082 CheckParameterUtil.ensureThat(!imgs.isEmpty(), "imgs is empty"); 083 if (baseMultiResolutionImageConstructor != null) { 084 try { 085 return baseMultiResolutionImageConstructor.newInstance((Object) imgs.toArray(new Image[0])); 086 } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { 087 Logging.error("Unexpected error while instantiating object of class BaseMultiResolutionImage: " + ex); 088 } 089 } 090 return imgs.get(0); 091 } 092 093 /** 094 * Wrapper for the method <code>java.awt.image.BaseMultiResolutionImage#getBaseImage()</code>. 095 * <p> 096 * Will return the argument <code>img</code> unchanged, if it is not a multi-resolution image. 097 * @param img the image 098 * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>, 099 * then the base image, otherwise the image itself 100 */ 101 public static Image getBaseImage(Image img) { 102 if (baseMultiResolutionImageClass == null || resolutionVariantsMethod == null) { 103 return img; 104 } 105 if (baseMultiResolutionImageClass.isInstance(img)) { 106 try { 107 @SuppressWarnings("unchecked") 108 List<Image> imgVars = (List<Image>) resolutionVariantsMethod.invoke(img); 109 if (!imgVars.isEmpty()) { 110 return imgVars.get(0); 111 } 112 } catch (IllegalAccessException | InvocationTargetException ex) { 113 Logging.error("Unexpected error while calling method: " + ex); 114 } 115 } 116 return img; 117 } 118 119 /** 120 * Wrapper for the method <code>java.awt.image.MultiResolutionImage#getResolutionVariants()</code>. 121 * <p> 122 * Will return the argument as a singleton list, in case it is not a multi-resolution image. 123 * @param img the image 124 * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>, 125 * then the result of the method <code>#getResolutionVariants()</code>, otherwise the image 126 * itself as a singleton list 127 */ 128 public static List<Image> getResolutionVariants(Image img) { 129 if (baseMultiResolutionImageClass == null || resolutionVariantsMethod == null) { 130 return Collections.singletonList(img); 131 } 132 if (baseMultiResolutionImageClass.isInstance(img)) { 133 try { 134 @SuppressWarnings("unchecked") 135 List<Image> imgVars = (List<Image>) resolutionVariantsMethod.invoke(img); 136 if (!imgVars.isEmpty()) { 137 return imgVars; 138 } 139 } catch (IllegalAccessException | InvocationTargetException ex) { 140 Logging.error("Unexpected error while calling method: " + ex); 141 } 142 } 143 return Collections.singletonList(img); 144 } 145 146 /** 147 * Wrapper for method <code>java.awt.image.MultiResolutionImage#getResolutionVariant(double destImageWidth, double destImageHeight)</code>. 148 * <p> 149 * Will return the argument, in case it is not a multi-resolution image. 150 * @param img the image 151 * @param destImageWidth the width of the destination image 152 * @param destImageHeight the height of the destination image 153 * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>, 154 * then the result of the method <code>#getResolutionVariant(destImageWidth, destImageHeight)</code>, 155 * otherwise the image itself 156 */ 157 public static Image getResolutionVariant(Image img, double destImageWidth, double destImageHeight) { 158 if (baseMultiResolutionImageClass == null || resolutionVariantsMethod == null) { 159 return img; 160 } 161 if (baseMultiResolutionImageClass.isInstance(img)) { 162 try { 163 return (Image) resolutionVariantMethod.invoke(img, destImageWidth, destImageHeight); 164 } catch (IllegalAccessException | InvocationTargetException ex) { 165 Logging.error("Unexpected error while calling method: " + ex); 166 } 167 } 168 return img; 169 } 170 171 /** 172 * Detect the GUI scale for HiDPI mode. 173 * <p> 174 * This method may not work as expected for a multi-monitor setup. It will 175 * only take the default screen device into account. 176 * @return the GUI scale for HiDPI mode, a value of 1.0 means standard mode. 177 */ 178 public static double getHiDPIScale() { 179 if (GraphicsEnvironment.isHeadless()) 180 return 1.0; 181 GraphicsConfiguration gc = GraphicsEnvironment 182 .getLocalGraphicsEnvironment() 183 .getDefaultScreenDevice(). 184 getDefaultConfiguration(); 185 AffineTransform transform = gc.getDefaultTransform(); 186 if (!Utils.equalsEpsilon(transform.getScaleX(), transform.getScaleY())) { 187 Logging.warn("Unexpected ui transform: " + transform); 188 } 189 return transform.getScaleX(); 190 } 191 192 /** 193 * Perform an operation on multi-resolution images. 194 * 195 * When input image is not multi-resolution, it will simply apply the processor once. 196 * Otherwise, the processor will be called for each resolution variant and the 197 * resulting images assembled to become the output multi-resolution image. 198 * @param img input image, possibly multi-resolution 199 * @param processor processor taking a plain image as input and returning a single 200 * plain image as output 201 * @return multi-resolution image assembled from the output of calls to <code>processor</code> 202 * for each resolution variant 203 */ 204 public static Image processMRImage(Image img, UnaryOperator<Image> processor) { 205 return processMRImages(Collections.singletonList(img), imgs -> processor.apply(imgs.get(0))); 206 } 207 208 /** 209 * Perform an operation on multi-resolution images. 210 * 211 * When input images are not multi-resolution, it will simply apply the processor once. 212 * Otherwise, the processor will be called for each resolution variant and the 213 * resulting images assembled to become the output multi-resolution image. 214 * @param imgs input images, possibly multi-resolution 215 * @param processor processor taking a list of plain images as input and returning 216 * a single plain image as output 217 * @return multi-resolution image assembled from the output of calls to <code>processor</code> 218 * for each resolution variant 219 */ 220 public static Image processMRImages(List<Image> imgs, Function<List<Image>, Image> processor) { 221 CheckParameterUtil.ensureThat(!imgs.isEmpty(), "at least one element expected"); 222 if (baseMultiResolutionImageClass != null) { 223 return processor.apply(imgs); 224 } 225 List<List<Image>> allVars = imgs.stream().map(HiDPISupport::getResolutionVariants).collect(Collectors.toList()); 226 int maxVariants = allVars.stream().mapToInt(List<Image>::size).max().getAsInt(); 227 if (maxVariants == 1) 228 return processor.apply(imgs); 229 List<Image> imgsProcessed = IntStream.range(0, maxVariants) 230 .mapToObj( 231 k -> processor.apply( 232 allVars.stream().map(vars -> vars.get(k)).collect(Collectors.toList()) 233 ) 234 ).collect(Collectors.toList()); 235 return getMultiResolutionImage(imgsProcessed); 236 } 237 238 @SuppressWarnings("unchecked") 239 private static Class<? extends Image> initBaseMultiResolutionImageClass() { 240 try { 241 return (Class<? extends Image>) Class.forName("java.awt.image.BaseMultiResolutionImage"); 242 } catch (ClassNotFoundException ex) { 243 // class is not present in Java 8 244 Logging.trace(ex); 245 return null; 246 } 247 } 248 249 private static Constructor<? extends Image> initBaseMultiResolutionImageConstructor() { 250 try { 251 return baseMultiResolutionImageClass != null 252 ? baseMultiResolutionImageClass.getConstructor(Image[].class) 253 : null; 254 } catch (NoSuchMethodException ex) { 255 Logging.error("Cannot find expected constructor: " + ex); 256 return null; 257 } 258 } 259 260 private static Method initResolutionVariantsMethod() { 261 try { 262 return baseMultiResolutionImageClass != null 263 ? baseMultiResolutionImageClass.getMethod("getResolutionVariants") 264 : null; 265 } catch (NoSuchMethodException ex) { 266 Logging.error("Cannot find expected method: " + ex); 267 return null; 268 } 269 } 270 271 private static Method initResolutionVariantMethod() { 272 try { 273 return baseMultiResolutionImageClass != null 274 ? baseMultiResolutionImageClass.getMethod("getResolutionVariant", Double.TYPE, Double.TYPE) 275 : null; 276 } catch (NoSuchMethodException ex) { 277 Logging.error("Cannot find expected method: " + ex); 278 return null; 279 } 280 } 281}