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