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.Optional; 016import java.util.function.Function; 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 volatile Optional<Class<? extends Image>> baseMultiResolutionImageClass; 033 private static volatile Optional<Constructor<? extends Image>> baseMultiResolutionImageConstructor; 034 private static volatile Optional<Method> resolutionVariantsMethod; 035 036 private HiDPISupport() { 037 // Hide default constructor 038 } 039 040 /** 041 * Create a multi-resolution image from a base image and an {@link ImageResource}. 042 * <p> 043 * Will only return multi-resolution image, if HiDPI-mode is detected. Then 044 * the image stack will consist of the base image and one that fits the 045 * HiDPI scale of the main display. 046 * @param base the base image 047 * @param ir a corresponding image resource 048 * @return multi-resolution image if necessary and possible, the base image otherwise 049 */ 050 public static Image getMultiResolutionImage(Image base, ImageResource ir) { 051 double uiScale = getHiDPIScale(); 052 if (uiScale != 1.0 && getBaseMultiResolutionImageConstructor().isPresent()) { 053 ImageIcon zoomed = ir.getImageIcon(new Dimension( 054 (int) Math.round(base.getWidth(null) * uiScale), 055 (int) Math.round(base.getHeight(null) * uiScale)), false); 056 Image mrImg = getMultiResolutionImage(Arrays.asList(base, zoomed.getImage())); 057 if (mrImg != null) return mrImg; 058 } 059 return base; 060 } 061 062 /** 063 * Create a multi-resolution image from a list of images. 064 * @param imgs the images, supposedly the same image at different resolutions, 065 * must not be empty 066 * @return corresponding multi-resolution image, if possible, the first image 067 * in the list otherwise 068 */ 069 public static Image getMultiResolutionImage(List<Image> imgs) { 070 CheckParameterUtil.ensure(imgs, "imgs", "not empty", ls -> !ls.isEmpty()); 071 Optional<Constructor<? extends Image>> baseMrImageConstructor = getBaseMultiResolutionImageConstructor(); 072 if (baseMrImageConstructor.isPresent()) { 073 try { 074 return baseMrImageConstructor.get().newInstance((Object) imgs.toArray(new Image[0])); 075 } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { 076 Logging.error("Unexpected error while instantiating object of class BaseMultiResolutionImage: " + ex); 077 } 078 } 079 return imgs.get(0); 080 } 081 082 /** 083 * Wrapper for the method <code>java.awt.image.BaseMultiResolutionImage#getBaseImage()</code>. 084 * <p> 085 * Will return the argument <code>img</code> unchanged, if it is not a multi-resolution image. 086 * @param img the image 087 * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>, 088 * then the base image, otherwise the image itself 089 */ 090 public static Image getBaseImage(Image img) { 091 Optional<Class<? extends Image>> baseMrImageClass = getBaseMultiResolutionImageClass(); 092 Optional<Method> resVariantsMethod = getResolutionVariantsMethod(); 093 if (!baseMrImageClass.isPresent() || !resVariantsMethod.isPresent()) { 094 return img; 095 } 096 if (baseMrImageClass.get().isInstance(img)) { 097 try { 098 @SuppressWarnings("unchecked") 099 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}