1 /*******************************************************************************
2  * Copyright (c) 2017 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.swt.internal;
15 
16 import org.eclipse.swt.*;
17 import org.eclipse.swt.graphics.*;
18 
19 /**
20  * This class hold common constants and utility functions w.r.t. to SWT high DPI
21  * functionality.
22  * <p>
23  * The {@code autoScaleUp(..)} methods convert from API coordinates (in
24  * SWT points) to internal high DPI coordinates (in pixels) that interface with
25  * native widgets.
26  * </p>
27  * <p>
28  * The {@code autoScaleDown(..)} convert from high DPI pixels to API coordinates
29  * (in SWT points).
30  * </p>
31  *
32  * @since 3.105
33  */
34 public class DPIUtil {
35 
36 	private static final int DPI_ZOOM_100 = 96;
37 
38 	private static int deviceZoom = 100;
39 	private static int nativeDeviceZoom = 100;
40 
41 	private static enum AutoScaleMethod { AUTO, NEAREST, SMOOTH }
42 	private static AutoScaleMethod autoScaleMethodSetting = AutoScaleMethod.AUTO;
43 	private static AutoScaleMethod autoScaleMethod = AutoScaleMethod.NEAREST;
44 
45 	private static String autoScaleValue;
46 	private static boolean useCairoAutoScale = false;
47 
48 	/**
49 	 * System property that controls the autoScale functionality.
50 	 * <ul>
51 	 * <li><b>false</b>: deviceZoom is set to 100%</li>
52 	 * <li><b>integer</b>: deviceZoom depends on the current display resolution,
53 	 *     but only uses integer multiples of 100%. The detected native zoom is
54 	 *     generally rounded down (e.g. at 150%, will use 100%), unless close to
55 	 *     the next integer multiple (currently at 175%, will use 200%).</li>
56 	 * <li><b>integer200</b>: like <b>integer</b>, but the maximal zoom level is 200%.</li>
57 	 * <li><b>quarter</b>: deviceZoom depends on the current display resolution,
58 	 *     but only uses integer multiples of 25%. The detected native zoom is
59 	 *     rounded to the closest permissible value.</li>
60 	 * <li><b>exact</b>: deviceZoom uses the native zoom (with 1% as minimal
61 	 *     step).</li>
62 	 * <li><i>&lt;value&gt;</i>: deviceZoom uses the given integer value in
63 	 *     percent as zoom level.</li>
64 	 * </ul>
65 	 * The current default is "integer200".
66 	 */
67 	private static final String SWT_AUTOSCALE = "swt.autoScale";
68 
69 	/**
70 	 * System property that controls the method for scaling images:
71 	 * <ul>
72 	 * <li>"nearest": nearest-neighbor interpolation, may look jagged</li>
73 	 * <li>"smooth": smooth edges, may look blurry</li>
74 	 * </ul>
75 	 * The current default is to use "nearest", except on
76 	 * GTK when the deviceZoom is not an integer multiple of 100%.
77 	 * The smooth strategy currently doesn't work on Win32 and Cocoa, see
78 	 * <a href="https://bugs.eclipse.org/493455">bug 493455</a>.
79 	 */
80 	private static final String SWT_AUTOSCALE_METHOD = "swt.autoScale.method";
81 	static {
82 		autoScaleValue = System.getProperty (SWT_AUTOSCALE);
83 
84 		String value = System.getProperty (SWT_AUTOSCALE_METHOD);
85 		if (value != null) {
86 			if (AutoScaleMethod.NEAREST.name().equalsIgnoreCase(value)) {
87 				autoScaleMethod = autoScaleMethodSetting = AutoScaleMethod.NEAREST;
88 			} else if (AutoScaleMethod.SMOOTH.name().equalsIgnoreCase(value)) {
89 				autoScaleMethod = autoScaleMethodSetting = AutoScaleMethod.SMOOTH;
90 			}
91 		}
92 	}
93 
94 /**
95  * Auto-scale down ImageData
96  */
autoScaleDown(Device device, final ImageData imageData)97 public static ImageData autoScaleDown (Device device, final ImageData imageData) {
98 	if (deviceZoom == 100 || imageData == null || (device != null && !device.isAutoScalable())) return imageData;
99 	float scaleFactor = 1.0f / getScalingFactor ();
100 	return autoScaleImageData(device, imageData, scaleFactor);
101 }
102 
autoScaleDown(int[] pointArray)103 public static int[] autoScaleDown(int[] pointArray) {
104 	if (deviceZoom == 100 || pointArray == null) return pointArray;
105 	float scaleFactor = getScalingFactor ();
106 	int [] returnArray = new int[pointArray.length];
107 	for (int i = 0; i < pointArray.length; i++) {
108 		returnArray [i] =  Math.round (pointArray [i] / scaleFactor);
109 	}
110 	return returnArray;
111 }
112 
autoScaleDown(Drawable drawable, int[] pointArray)113 public static int[] autoScaleDown(Drawable drawable, int[] pointArray) {
114 	if (drawable != null && !drawable.isAutoScalable ()) return pointArray;
115 	return autoScaleDown (pointArray);
116 }
117 
118 /**
119  * Auto-scale up float array dimensions.
120  */
autoScaleDown(float size[])121 public static float[] autoScaleDown (float size[]) {
122 	if (deviceZoom == 100 || size == null) return size;
123 	float scaleFactor = getScalingFactor ();
124 	float scaledSize[] = new float[size.length];
125 	for (int i = 0; i < scaledSize.length; i++) {
126 		scaledSize[i] = size[i] / scaleFactor;
127 	}
128 	return scaledSize;
129 }
130 
131 /**
132  * Auto-scale up float array dimensions if enabled for Drawable class.
133  */
autoScaleDown(Drawable drawable, float size[])134 public static float[] autoScaleDown (Drawable drawable, float size[]) {
135 	if (drawable != null && !drawable.isAutoScalable ()) return size;
136 	return autoScaleDown (size);
137 }
138 
139 /**
140  * Auto-scale down int dimensions.
141  */
autoScaleDown(int size)142 public static int autoScaleDown (int size) {
143 	if (deviceZoom == 100 || size == SWT.DEFAULT) return size;
144 	float scaleFactor = getScalingFactor ();
145 	return Math.round (size / scaleFactor);
146 }
147 /**
148  * Auto-scale down int dimensions if enabled for Drawable class.
149  */
autoScaleDown(Drawable drawable, int size)150 public static int autoScaleDown (Drawable drawable, int size) {
151 	if (drawable != null && !drawable.isAutoScalable ()) return size;
152 	return autoScaleDown (size);
153 }
154 
155 /**
156  * Auto-scale down float dimensions.
157  */
autoScaleDown(float size)158 public static float autoScaleDown (float size) {
159 	if (deviceZoom == 100 || size == SWT.DEFAULT) return size;
160 	float scaleFactor = getScalingFactor ();
161 	return (size / scaleFactor);
162 }
163 
164 /**
165  * Auto-scale down float dimensions if enabled for Drawable class.
166  */
autoScaleDown(Drawable drawable, float size)167 public static float autoScaleDown (Drawable drawable, float size) {
168 	if (drawable != null && !drawable.isAutoScalable ()) return size;
169 	return autoScaleDown (size);
170 }
171 
172 /**
173  * Returns a new scaled down Point.
174  */
autoScaleDown(Point point)175 public static Point autoScaleDown (Point point) {
176 	if (deviceZoom == 100 || point == null) return point;
177 	float scaleFactor = getScalingFactor ();
178 	Point scaledPoint = new Point (0,0);
179 	scaledPoint.x = Math.round (point.x / scaleFactor);
180 	scaledPoint.y = Math.round (point.y / scaleFactor);
181 	return scaledPoint;
182 }
183 
184 /**
185  * Returns a new scaled down Point if enabled for Drawable class.
186  */
autoScaleDown(Drawable drawable, Point point)187 public static Point autoScaleDown (Drawable drawable, Point point) {
188 	if (drawable != null && !drawable.isAutoScalable ()) return point;
189 	return autoScaleDown (point);
190 }
191 
192 /**
193  * Returns a new scaled down Rectangle.
194  */
autoScaleDown(Rectangle rect)195 public static Rectangle autoScaleDown (Rectangle rect) {
196 	if (deviceZoom == 100 || rect == null) return rect;
197 	Rectangle scaledRect = new Rectangle (0,0,0,0);
198 	Point scaledTopLeft = DPIUtil.autoScaleDown (new Point (rect.x, rect.y));
199 	Point scaledBottomRight = DPIUtil.autoScaleDown (new Point (rect.x + rect.width, rect.y + rect.height));
200 
201 	scaledRect.x = scaledTopLeft.x;
202 	scaledRect.y = scaledTopLeft.y;
203 	scaledRect.width = scaledBottomRight.x - scaledTopLeft.x;
204 	scaledRect.height = scaledBottomRight.y - scaledTopLeft.y;
205 	return scaledRect;
206 }
207 /**
208  * Returns a new scaled down Rectangle if enabled for Drawable class.
209  */
autoScaleDown(Drawable drawable, Rectangle rect)210 public static Rectangle autoScaleDown (Drawable drawable, Rectangle rect) {
211 	if (drawable != null && !drawable.isAutoScalable ()) return rect;
212 	return autoScaleDown (rect);
213 }
214 
215 /**
216  * Auto-scale image with ImageData
217  */
autoScaleImageData(Device device, final ImageData imageData, int targetZoom, int currentZoom)218 public static ImageData autoScaleImageData (Device device, final ImageData imageData, int targetZoom, int currentZoom) {
219 	if (imageData == null || targetZoom == currentZoom || (device != null && !device.isAutoScalable())) return imageData;
220 	float scaleFactor = (float) targetZoom / (float) currentZoom;
221 	return autoScaleImageData(device, imageData, scaleFactor);
222 }
223 
autoScaleImageData(Device device, final ImageData imageData, float scaleFactor)224 private static ImageData autoScaleImageData (Device device, final ImageData imageData, float scaleFactor) {
225 	// Guards are already implemented in callers: if (deviceZoom == 100 || imageData == null || scaleFactor == 1.0f) return imageData;
226 	int width = imageData.width;
227 	int height = imageData.height;
228 	int scaledWidth = Math.round ((float) width * scaleFactor);
229 	int scaledHeight = Math.round ((float) height * scaleFactor);
230 	switch (autoScaleMethod) {
231 	case SMOOTH:
232 		Image original = new Image (device, (ImageDataProvider) zoom -> imageData);
233 
234 		/* Create a 24 bit image data with alpha channel */
235 		final ImageData resultData = new ImageData (scaledWidth, scaledHeight, 24, new PaletteData (0xFF, 0xFF00, 0xFF0000));
236 		resultData.alphaData = new byte [scaledWidth * scaledHeight];
237 
238 		Image resultImage = new Image (device, (ImageDataProvider) zoom -> resultData);
239 		GC gc = new GC (resultImage);
240 		gc.setAntialias (SWT.ON);
241 		gc.drawImage (original, 0, 0, DPIUtil.autoScaleDown (width), DPIUtil.autoScaleDown (height),
242 				/* E.g. destWidth here is effectively DPIUtil.autoScaleDown (scaledWidth), but avoiding rounding errors.
243 				 * Nevertheless, we still have some rounding errors due to the point-based API GC#drawImage(..).
244 				 */
245 				0, 0, Math.round (DPIUtil.autoScaleDown ((float) width * scaleFactor)), Math.round (DPIUtil.autoScaleDown ((float) height * scaleFactor)));
246 		gc.dispose ();
247 		original.dispose ();
248 		ImageData result = resultImage.getImageData (DPIUtil.getDeviceZoom ());
249 		resultImage.dispose ();
250 		return result;
251 	case NEAREST:
252 	default:
253 		return imageData.scaledTo (scaledWidth, scaledHeight);
254 	}
255 }
256 
257 /**
258  * Returns a new rectangle as per the scaleFactor.
259  */
autoScaleBounds(Rectangle rect, int targetZoom, int currentZoom)260 public static Rectangle autoScaleBounds (Rectangle rect, int targetZoom, int currentZoom) {
261 	if (deviceZoom == 100 || rect == null || targetZoom == currentZoom) return rect;
262 	float scaleFactor = ((float)targetZoom) / (float)currentZoom;
263 	Rectangle returnRect = new Rectangle (0,0,0,0);
264 	returnRect.x = Math.round (rect.x * scaleFactor);
265 	returnRect.y = Math.round (rect.y * scaleFactor);
266 	returnRect.width = Math.round (rect.width * scaleFactor);
267 	returnRect.height = Math.round (rect.height * scaleFactor);
268 	return returnRect;
269 }
270 
271 /**
272  * Auto-scale up ImageData
273  */
autoScaleUp(Device device, final ImageData imageData)274 public static ImageData autoScaleUp (Device device, final ImageData imageData) {
275 	if (deviceZoom == 100 || imageData == null || (device != null && !device.isAutoScalable())) return imageData;
276 	float scaleFactor = deviceZoom / 100f;
277 	return autoScaleImageData(device, imageData, scaleFactor);
278 }
279 
autoScaleUp(int[] pointArray)280 public static int[] autoScaleUp(int[] pointArray) {
281 	if (deviceZoom == 100 || pointArray == null) return pointArray;
282 	float scaleFactor = getScalingFactor ();
283 	int [] returnArray = new int[pointArray.length];
284 	for (int i = 0; i < pointArray.length; i++) {
285 		returnArray [i] =  Math.round (pointArray [i] * scaleFactor);
286 	}
287 	return returnArray;
288 }
289 
autoScaleUp(Drawable drawable, int[] pointArray)290 public static int[] autoScaleUp(Drawable drawable, int[] pointArray) {
291 	if (drawable != null && !drawable.isAutoScalable ()) return pointArray;
292 	return autoScaleUp (pointArray);
293 }
294 
295 /**
296  * Auto-scale up int dimensions.
297  */
autoScaleUp(int size)298 public static int autoScaleUp (int size) {
299 	if (deviceZoom == 100 || size == SWT.DEFAULT) return size;
300 	float scaleFactor = getScalingFactor ();
301 	return Math.round (size * scaleFactor);
302 }
303 
304 /**
305  * Auto-scale up int dimensions using Native DPI
306  */
autoScaleUpUsingNativeDPI(int size)307 public static int autoScaleUpUsingNativeDPI (int size) {
308 	if (nativeDeviceZoom == 100 || size == SWT.DEFAULT) return size;
309 	float nativeScaleFactor = nativeDeviceZoom / 100f;
310 	return Math.round (size * nativeScaleFactor);
311 }
312 
313 /**
314  * Auto-scale up int dimensions if enabled for Drawable class.
315  */
autoScaleUp(Drawable drawable, int size)316 public static int autoScaleUp (Drawable drawable, int size) {
317 	if (drawable != null && !drawable.isAutoScalable ()) return size;
318 	return autoScaleUp (size);
319 }
320 
autoScaleUp(float size)321 public static float autoScaleUp(float size) {
322 	if (deviceZoom == 100 || size == SWT.DEFAULT) return size;
323 	float scaleFactor = getScalingFactor ();
324 	return (size * scaleFactor);
325 }
326 
autoScaleUp(Drawable drawable, float size)327 public static float autoScaleUp(Drawable drawable, float size) {
328 	if (drawable != null && !drawable.isAutoScalable ()) return size;
329 	return autoScaleUp (size);
330 }
331 
332 /**
333  * Returns a new scaled up Point.
334  */
autoScaleUp(Point point)335 public static Point autoScaleUp (Point point) {
336 	if (deviceZoom == 100 || point == null) return point;
337 	float scaleFactor = getScalingFactor ();
338 	Point scaledPoint = new Point (0,0);
339 	scaledPoint.x = Math.round (point.x * scaleFactor);
340 	scaledPoint.y = Math.round (point.y * scaleFactor);
341 	return scaledPoint;
342 }
343 
344 /**
345  * Returns a new scaled up Point if enabled for Drawable class.
346  */
autoScaleUp(Drawable drawable, Point point)347 public static Point autoScaleUp (Drawable drawable, Point point) {
348 	if (drawable != null && !drawable.isAutoScalable ()) return point;
349 	return autoScaleUp (point);
350 }
351 
352 /**
353  * Returns a new scaled up Rectangle.
354  */
autoScaleUp(Rectangle rect)355 public static Rectangle autoScaleUp (Rectangle rect) {
356 	if (deviceZoom == 100 || rect == null) return rect;
357 	Rectangle scaledRect = new Rectangle (0,0,0,0);
358 	Point scaledTopLeft = DPIUtil.autoScaleUp (new Point (rect.x, rect.y));
359 	Point scaledBottomRight = DPIUtil.autoScaleUp (new Point (rect.x + rect.width, rect.y + rect.height));
360 
361 	scaledRect.x = scaledTopLeft.x;
362 	scaledRect.y = scaledTopLeft.y;
363 	scaledRect.width = scaledBottomRight.x - scaledTopLeft.x;
364 	scaledRect.height = scaledBottomRight.y - scaledTopLeft.y;
365 	return scaledRect;
366 }
367 
368 /**
369  * Returns a new scaled up Rectangle if enabled for Drawable class.
370  */
autoScaleUp(Drawable drawable, Rectangle rect)371 public static Rectangle autoScaleUp (Drawable drawable, Rectangle rect) {
372 	if (drawable != null && !drawable.isAutoScalable ()) return rect;
373 	return autoScaleUp (rect);
374 }
375 
376 /**
377  * Returns Scaling factor from the display
378  * @return float scaling factor
379  */
getScalingFactor()380 private static float getScalingFactor () {
381 	if (useCairoAutoScale) {
382 		return 1;
383 	}
384 	return deviceZoom / 100f;
385 }
386 
387 /**
388  * Compute the zoom value based on the DPI value.
389  *
390  * @return zoom
391  */
mapDPIToZoom(int dpi)392 public static int mapDPIToZoom (int dpi) {
393 	double zoom = (double) dpi * 100 / DPI_ZOOM_100;
394 	int roundedZoom = (int) Math.round (zoom);
395 	return roundedZoom;
396 }
397 /**
398  * Gets Image data at specified zoom level, if image is missing then
399  * fall-back to 100% image. If provider or fall-back image is not available,
400  * throw error.
401  */
validateAndGetImageDataAtZoom(ImageDataProvider provider, int zoom, boolean[] found)402 public static ImageData validateAndGetImageDataAtZoom (ImageDataProvider provider, int zoom, boolean[] found) {
403 	if (provider == null) SWT.error (SWT.ERROR_NULL_ARGUMENT);
404 	ImageData data = provider.getImageData (zoom);
405 	found [0] = (data != null);
406 	/* If image is null when (zoom != 100%), fall-back to image at 100% zoom */
407 	if (zoom != 100 && !found [0]) data = provider.getImageData (100);
408 	if (data == null) SWT.error (SWT.ERROR_INVALID_ARGUMENT, null, ": ImageDataProvider [" + provider + "] returns null ImageData at 100% zoom.");
409 	return data;
410 }
411 
412 /**
413  * Gets Image file path at specified zoom level, if image is missing then
414  * fall-back to 100% image. If provider or fall-back image is not available,
415  * throw error.
416  */
validateAndGetImagePathAtZoom(ImageFileNameProvider provider, int zoom, boolean[] found)417 public static String validateAndGetImagePathAtZoom (ImageFileNameProvider provider, int zoom, boolean[] found) {
418 	if (provider == null) SWT.error (SWT.ERROR_NULL_ARGUMENT);
419 	String filename = provider.getImagePath (zoom);
420 	found [0] = (filename != null);
421 	/* If image is null when (zoom != 100%), fall-back to image at 100% zoom */
422 	if (zoom != 100 && !found [0]) filename = provider.getImagePath (100);
423 	if (filename == null) SWT.error (SWT.ERROR_INVALID_ARGUMENT, null, ": ImageFileNameProvider [" + provider + "] returns null filename at 100% zoom.");
424 	return filename;
425 }
426 
getDeviceZoom()427 public static int getDeviceZoom() {
428 	return deviceZoom;
429 }
430 
setDeviceZoom(int nativeDeviceZoom)431 public static void setDeviceZoom (int nativeDeviceZoom) {
432 	DPIUtil.nativeDeviceZoom = nativeDeviceZoom;
433 	int deviceZoom = getZoomForAutoscaleProperty (nativeDeviceZoom);
434 
435 	DPIUtil.deviceZoom = deviceZoom;
436 	System.setProperty("org.eclipse.swt.internal.deviceZoom", Integer.toString(deviceZoom));
437 	if (deviceZoom != 100 && autoScaleMethodSetting == AutoScaleMethod.AUTO) {
438 		if (deviceZoom / 100 * 100 == deviceZoom || !"gtk".equals(SWT.getPlatform())) {
439 			autoScaleMethod = AutoScaleMethod.NEAREST;
440 		} else {
441 			autoScaleMethod = AutoScaleMethod.SMOOTH;
442 		}
443 	}
444 }
445 
setUseCairoAutoScale(boolean cairoAutoScale)446 public static void setUseCairoAutoScale (boolean cairoAutoScale) {
447 	useCairoAutoScale = cairoAutoScale;
448 }
449 
useCairoAutoScale()450 public static boolean useCairoAutoScale() {
451 	return useCairoAutoScale;
452 }
453 
getZoomForAutoscaleProperty(int nativeDeviceZoom)454 public static int getZoomForAutoscaleProperty (int nativeDeviceZoom) {
455 	int zoom = 0;
456 	if (autoScaleValue != null) {
457 		if ("false".equalsIgnoreCase (autoScaleValue)) {
458 			zoom = 100;
459 		} else if ("quarter".equalsIgnoreCase (autoScaleValue)) {
460 			zoom = (int) (Math.round (nativeDeviceZoom / 25f) * 25);
461 		} else if ("exact".equalsIgnoreCase (autoScaleValue)) {
462 			zoom = nativeDeviceZoom;
463 		} else {
464 			try {
465 				int zoomValue = Integer.parseInt (autoScaleValue);
466 				zoom = Math.max (Math.min (zoomValue, 1600), 25);
467 			} catch (NumberFormatException e) {
468 				// unsupported value, use default
469 			}
470 		}
471 	}
472 	if (zoom == 0) { // || "integer".equalsIgnoreCase (value) || "integer200".equalsIgnoreCase (value)
473 		zoom = Math.max ((nativeDeviceZoom + 25) / 100 * 100, 100);
474 		if (!"integer".equalsIgnoreCase(autoScaleValue)) {
475 			// integer200, or default
476 			zoom = Math.min (zoom, 200);
477 		}
478 	}
479 	return zoom;
480 }
481 
482 /**
483  * AutoScale ImageDataProvider.
484  */
485 public static final class AutoScaleImageDataProvider implements ImageDataProvider {
486 	Device device;
487 	ImageData imageData;
488 	int currentZoom;
AutoScaleImageDataProvider(Device device, ImageData data, int zoom)489 	public AutoScaleImageDataProvider(Device device, ImageData data, int zoom){
490 		this.device = device;
491 		this.imageData = data;
492 		this.currentZoom = zoom;
493 	}
494 	@Override
getImageData(int zoom)495 	public ImageData getImageData(int zoom) {
496 		return DPIUtil.autoScaleImageData(device, imageData, zoom, currentZoom);
497 	}
498 }
499 }
500