| File: misc\DpiHelper.cs | Web Access |
| Project: src\winforms\src\System.Drawing.Common\src\System.Drawing.Common.csproj (System.Drawing.Common) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace System.Drawing; /// <summary> /// Helper class for scaling coordinates and images according to current DPI scaling set in Windows for the primary screen. /// </summary> internal static class DpiHelper { private const double LogicalDpi = 96.0; private static bool s_isInitialized; /// <summary> /// The primary screen's (device) current horizontal DPI /// </summary> private static double s_deviceDpiX = LogicalDpi; /// <summary> /// The primary screen's (device) current vertical DPI /// </summary> private static double s_deviceDpiY = LogicalDpi; private static double s_logicalToDeviceUnitsScalingFactorX; private static double s_logicalToDeviceUnitsScalingFactorY; private static Drawing2D.InterpolationMode s_interpolationMode = Drawing2D.InterpolationMode.Invalid; private static void Initialize() { if (s_isInitialized) { return; } using var hdc = GetDcScope.ScreenDC; s_deviceDpiX = PInvokeCore.GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.LOGPIXELSX); s_deviceDpiY = PInvokeCore.GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.LOGPIXELSY); s_isInitialized = true; } private static double LogicalToDeviceUnitsScalingFactorX { get { if (s_logicalToDeviceUnitsScalingFactorX == 0.0) { Initialize(); s_logicalToDeviceUnitsScalingFactorX = s_deviceDpiX / LogicalDpi; } return s_logicalToDeviceUnitsScalingFactorX; } } private static double LogicalToDeviceUnitsScalingFactorY { get { if (s_logicalToDeviceUnitsScalingFactorY == 0.0) { Initialize(); s_logicalToDeviceUnitsScalingFactorY = s_deviceDpiY / LogicalDpi; } return s_logicalToDeviceUnitsScalingFactorY; } } private static Drawing2D.InterpolationMode InterpolationMode { get { if (s_interpolationMode == Drawing2D.InterpolationMode.Invalid) { int dpiScalePercent = (int)Math.Round(LogicalToDeviceUnitsScalingFactorX * 100); // We will prefer NearestNeighbor algorithm for 200, 300, 400, etc zoom factors, // in which each pixel become a 2x2, 3x3, 4x4, etc rectangle. // This produces sharp edges in the scaled image and doesn't cause distortions of the original image. // For any other scale factors we will prefer a high quality resizing algorithm. // While that introduces fuzziness in the resulting image, it will not distort the original // (which is extremely important for small zoom factors like 125%, 150%). // We'll use Bicubic in those cases, except on reducing (zoom < 100, which we shouldn't have anyway), // in which case Linear produces better results because it uses less neighboring pixels. if ((dpiScalePercent % 100) == 0) { s_interpolationMode = Drawing2D.InterpolationMode.NearestNeighbor; } else if (dpiScalePercent < 100) { s_interpolationMode = Drawing2D.InterpolationMode.HighQualityBilinear; } else { s_interpolationMode = Drawing2D.InterpolationMode.HighQualityBicubic; } } return s_interpolationMode; } } private static Bitmap ScaleBitmapToSize(Bitmap logicalImage, Size deviceImageSize) { Bitmap deviceImage = new(deviceImageSize.Width, deviceImageSize.Height, logicalImage.PixelFormat); using var graphics = Graphics.FromImage(deviceImage); graphics.InterpolationMode = InterpolationMode; RectangleF sourceRect = new(0, 0, logicalImage.Size.Width, logicalImage.Size.Height); RectangleF destRect = new(0, 0, deviceImageSize.Width, deviceImageSize.Height); // Specify a source rectangle shifted by half of pixel to account for GDI+ considering // the source origin the center of top-left pixel // Failing to do so will result in the right and bottom of the bitmap lines being // interpolated with the graphics' background color, // and will appear black even if we cleared the background with transparent color. // The apparition of these artifacts depends on the interpolation mode, on the dpi scaling factor, etc. // E.g. at 150% DPI, Bicubic produces them and NearestNeighbor is fine, // but at 200% DPI NearestNeighbor also shows them. sourceRect.Offset(-0.5f, -0.5f); graphics.DrawImage(logicalImage, destRect, sourceRect, GraphicsUnit.Pixel); return deviceImage; } private static Bitmap CreateScaledBitmap(Bitmap logicalImage) { Size deviceImageSize = LogicalToDeviceUnits(logicalImage.Size); return ScaleBitmapToSize(logicalImage, deviceImageSize); } /// <summary> /// Returns whether scaling is required when converting between logical-device units, /// if the application opted in the automatic scaling in the .config file. /// </summary> public static bool IsScalingRequired { get { Initialize(); return s_deviceDpiX != LogicalDpi || s_deviceDpiY != LogicalDpi; } } /// <summary> /// Transforms a horizontal integer coordinate from logical to device units /// by scaling it up for current DPI and rounding to nearest integer value /// Note: this method should be called only inside an if (DpiHelper.IsScalingRequired) clause /// </summary> /// <param name="value">The horizontal value in logical units</param> /// <returns>The horizontal value in device units</returns> public static int LogicalToDeviceUnitsX(int value) => (int)Math.Round(LogicalToDeviceUnitsScalingFactorX * value); /// <summary> /// Transforms a vertical integer coordinate from logical to device units /// by scaling it up for current DPI and rounding to nearest integer value /// Note: this method should be called only inside an if (DpiHelper.IsScalingRequired) clause /// </summary> /// <param name="value">The vertical value in logical units</param> /// <returns>The vertical value in device units</returns> public static int ScaleToInitialSystemDpi(int value) => (int)Math.Round(LogicalToDeviceUnitsScalingFactorY * value); /// <summary> /// Returns a new Size with the input's /// dimensions converted from logical units to device units. /// Note: this method should be called only inside an if (DpiHelper.IsScalingRequired) clause /// </summary> /// <param name="logicalSize">Size in logical units</param> /// <returns>Size in device units</returns> public static Size LogicalToDeviceUnits(Size logicalSize) => new Size(LogicalToDeviceUnitsX(logicalSize.Width), ScaleToInitialSystemDpi(logicalSize.Height)); /// <summary> /// Create and return a new bitmap scaled to the specified size. /// Note: this method should be called only inside an if (DpiHelper.IsScalingRequired) clause /// </summary> /// <param name="logicalImage">The image to scale from logical units to device units</param> /// <param name="targetImageSize">The size to scale image to</param> [return: NotNullIfNotNull(nameof(logicalImage))] public static Bitmap? CreateResizedBitmap(Bitmap? logicalImage, Size targetImageSize) { if (logicalImage is null) { return null; } return ScaleBitmapToSize(logicalImage, targetImageSize); } /// <summary> /// Create a new bitmap scaled for the device units. /// When displayed on the device, the scaled image will have same size as the original image would have when displayed at 96dpi. /// Note: this method should be called only inside an if (DpiHelper.IsScalingRequired) clause /// </summary> /// <param name="logicalBitmap">The image to scale from logical units to device units</param> public static void ScaleBitmapLogicalToDevice([NotNullIfNotNull(nameof(logicalBitmap))] ref Bitmap? logicalBitmap) { if (logicalBitmap is null) { return; } Bitmap deviceBitmap = CreateScaledBitmap(logicalBitmap); logicalBitmap.Dispose(); logicalBitmap = deviceBitmap; } }