1 /*******************************************************************************
2 * Copyright (c) 2017 IBM Corporation and others.
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/
9 * SPDX-License-Identifier: EPL-2.0
12 * IBM Corporation - initial API and implementation
13 *******************************************************************************/
14 package org.eclipse.swt.internal;
16 import org.eclipse.swt.*;
17 import org.eclipse.swt.graphics.*;
20 * This class hold common constants and utility functions w.r.t. to SWT high DPI
23 * The {@code autoScaleUp(..)} methods convert from API coordinates (in
24 * SWT points) to internal high DPI coordinates (in pixels) that interface with
28 * The {@code autoScaleDown(..)} convert from high DPI pixels to API coordinates
34 public class DPIUtil {
36 private static final int DPI_ZOOM_100 = 96;
38 private static int deviceZoom = 100;
39 private static int nativeDeviceZoom = 100;
41 private static enum AutoScaleMethod { AUTO, NEAREST, SMOOTH }
42 private static AutoScaleMethod autoScaleMethodSetting = AutoScaleMethod.AUTO;
43 private static AutoScaleMethod autoScaleMethod = AutoScaleMethod.NEAREST;
45 private static String autoScaleValue;
46 private static boolean useCairoAutoScale = false;
49 * System property that controls the autoScale functionality.
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
62 * <li><i><value></i>: deviceZoom uses the given integer value in
63 * percent as zoom level.</li>
65 * The current default is "integer200".
67 private static final String SWT_AUTOSCALE = "swt.autoScale";
70 * System property that controls the method for scaling images:
72 * <li>"nearest": nearest-neighbor interpolation, may look jagged</li>
73 * <li>"smooth": smooth edges, may look blurry</li>
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>.
80 private static final String SWT_AUTOSCALE_METHOD = "swt.autoScale.method";
82 autoScaleValue = System.getProperty (SWT_AUTOSCALE);
84 String value = System.getProperty (SWT_AUTOSCALE_METHOD);
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;
95 * Auto-scale down 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);
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);
113 public static int[] autoScaleDown(Drawable drawable, int[] pointArray) {
114 if (drawable != null && !drawable.isAutoScalable ()) return pointArray;
115 return autoScaleDown (pointArray);
119 * Auto-scale up float array dimensions.
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;
132 * Auto-scale up float array dimensions if enabled for Drawable class.
134 public static float[] autoScaleDown (Drawable drawable, float size[]) {
135 if (drawable != null && !drawable.isAutoScalable ()) return size;
136 return autoScaleDown (size);
140 * Auto-scale down int dimensions.
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);
148 * Auto-scale down int dimensions if enabled for Drawable class.
150 public static int autoScaleDown (Drawable drawable, int size) {
151 if (drawable != null && !drawable.isAutoScalable ()) return size;
152 return autoScaleDown (size);
156 * Auto-scale down float dimensions.
158 public static float autoScaleDown (float size) {
159 if (deviceZoom == 100 || size == SWT.DEFAULT) return size;
160 float scaleFactor = getScalingFactor ();
161 return (size / scaleFactor);
165 * Auto-scale down float dimensions if enabled for Drawable class.
167 public static float autoScaleDown (Drawable drawable, float size) {
168 if (drawable != null && !drawable.isAutoScalable ()) return size;
169 return autoScaleDown (size);
173 * Returns a new scaled down 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);
185 * Returns a new scaled down Point if enabled for Drawable class.
187 public static Point autoScaleDown (Drawable drawable, Point point) {
188 if (drawable != null && !drawable.isAutoScalable ()) return point;
189 return autoScaleDown (point);
193 * Returns a new scaled down Rectangle.
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));
201 scaledRect.x = scaledTopLeft.x;
202 scaledRect.y = scaledTopLeft.y;
203 scaledRect.width = scaledBottomRight.x - scaledTopLeft.x;
204 scaledRect.height = scaledBottomRight.y - scaledTopLeft.y;
208 * Returns a new scaled down Rectangle if enabled for Drawable class.
210 public static Rectangle autoScaleDown (Drawable drawable, Rectangle rect) {
211 if (drawable != null && !drawable.isAutoScalable ()) return rect;
212 return autoScaleDown (rect);
216 * Auto-scale image with ImageData
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);
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) {
232 Image original = new Image (device, (ImageDataProvider) zoom -> imageData);
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];
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(..).
245 0, 0, Math.round (DPIUtil.autoScaleDown ((float) width * scaleFactor)), Math.round (DPIUtil.autoScaleDown ((float) height * scaleFactor)));
248 ImageData result = resultImage.getImageData (DPIUtil.getDeviceZoom ());
249 resultImage.dispose ();
253 return imageData.scaledTo (scaledWidth, scaledHeight);
258 * Returns a new rectangle as per the scaleFactor.
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);
272 * Auto-scale up 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);
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);
290 public static int[] autoScaleUp(Drawable drawable, int[] pointArray) {
291 if (drawable != null && !drawable.isAutoScalable ()) return pointArray;
292 return autoScaleUp (pointArray);
296 * Auto-scale up int dimensions.
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);
305 * Auto-scale up int dimensions using Native DPI
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);
314 * Auto-scale up int dimensions if enabled for Drawable class.
316 public static int autoScaleUp (Drawable drawable, int size) {
317 if (drawable != null && !drawable.isAutoScalable ()) return size;
318 return autoScaleUp (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);
327 public static float autoScaleUp(Drawable drawable, float size) {
328 if (drawable != null && !drawable.isAutoScalable ()) return size;
329 return autoScaleUp (size);
333 * Returns a new scaled up 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);
345 * Returns a new scaled up Point if enabled for Drawable class.
347 public static Point autoScaleUp (Drawable drawable, Point point) {
348 if (drawable != null && !drawable.isAutoScalable ()) return point;
349 return autoScaleUp (point);
353 * Returns a new scaled up Rectangle.
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));
361 scaledRect.x = scaledTopLeft.x;
362 scaledRect.y = scaledTopLeft.y;
363 scaledRect.width = scaledBottomRight.x - scaledTopLeft.x;
364 scaledRect.height = scaledBottomRight.y - scaledTopLeft.y;
369 * Returns a new scaled up Rectangle if enabled for Drawable class.
371 public static Rectangle autoScaleUp (Drawable drawable, Rectangle rect) {
372 if (drawable != null && !drawable.isAutoScalable ()) return rect;
373 return autoScaleUp (rect);
377 * Returns Scaling factor from the display
378 * @return float scaling factor
380 private static float getScalingFactor () {
381 if (useCairoAutoScale) {
384 return deviceZoom / 100f;
388 * Compute the zoom value based on the DPI value.
392 public static int mapDPIToZoom (int dpi) {
393 double zoom = (double) dpi * 100 / DPI_ZOOM_100;
394 int roundedZoom = (int) Math.round (zoom);
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,
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.");
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,
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.");
427 public static int getDeviceZoom() {
431 public static void setDeviceZoom (int nativeDeviceZoom) {
432 DPIUtil.nativeDeviceZoom = nativeDeviceZoom;
433 int deviceZoom = getZoomForAutoscaleProperty (nativeDeviceZoom);
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;
441 autoScaleMethod = AutoScaleMethod.SMOOTH;
446 public static void setUseCairoAutoScale (boolean cairoAutoScale) {
447 useCairoAutoScale = cairoAutoScale;
450 public static boolean useCairoAutoScale() {
451 return useCairoAutoScale;
454 public static int getZoomForAutoscaleProperty (int nativeDeviceZoom) {
456 if (autoScaleValue != null) {
457 if ("false".equalsIgnoreCase (autoScaleValue)) {
459 } else if ("quarter".equalsIgnoreCase (autoScaleValue)) {
460 zoom = (int) (Math.round (nativeDeviceZoom / 25f) * 25);
461 } else if ("exact".equalsIgnoreCase (autoScaleValue)) {
462 zoom = nativeDeviceZoom;
465 int zoomValue = Integer.parseInt (autoScaleValue);
466 zoom = Math.max (Math.min (zoomValue, 1600), 25);
467 } catch (NumberFormatException e) {
468 // unsupported value, use default
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);
483 * AutoScale ImageDataProvider.
485 public static final class AutoScaleImageDataProvider implements ImageDataProvider {
489 public AutoScaleImageDataProvider(Device device, ImageData data, int zoom){
490 this.device = device;
491 this.imageData = data;
492 this.currentZoom = zoom;
495 public ImageData getImageData(int zoom) {
496 return DPIUtil.autoScaleImageData(device, imageData, zoom, currentZoom);