diff --git a/Tests/HdrTests.cs b/Tests/HdrTests.cs
new file mode 100644
index 00000000..7d1ca06a
--- /dev/null
+++ b/Tests/HdrTests.cs
@@ -0,0 +1,160 @@
+using System.Drawing;
+using System.Drawing.Imaging;
+using Text_Grab.Utilities;
+using Xunit;
+
+namespace Tests;
+
+public class HdrTests
+{
+ [Fact]
+ public void ConvertHdrToSdr_WithNullBitmap_ReturnsNull()
+ {
+ // Arrange
+ Bitmap? nullBitmap = null;
+
+ // Act
+ Bitmap? result = HdrUtilities.ConvertHdrToSdr(nullBitmap);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ConvertHdrToSdr_WithValidBitmap_ReturnsNewBitmap()
+ {
+ // Arrange
+ Bitmap testBitmap = new(100, 100, PixelFormat.Format32bppArgb);
+ using Graphics g = Graphics.FromImage(testBitmap);
+ // Fill with white color (simulating HDR bright pixels)
+ g.Clear(Color.White);
+
+ // Act
+ Bitmap result = HdrUtilities.ConvertHdrToSdr(testBitmap);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.NotSame(testBitmap, result);
+ Assert.Equal(testBitmap.Width, result.Width);
+ Assert.Equal(testBitmap.Height, result.Height);
+
+ // Cleanup
+ testBitmap.Dispose();
+ result.Dispose();
+ }
+
+ [Fact]
+ public void ConvertHdrToSdr_WithBrightPixels_ReducesBrightness()
+ {
+ // Arrange
+ Bitmap testBitmap = new(10, 10, PixelFormat.Format32bppArgb);
+ // Fill with very bright color
+ using (Graphics g = Graphics.FromImage(testBitmap))
+ {
+ g.Clear(Color.FromArgb(255, 255, 255, 255));
+ }
+
+ // Act
+ Bitmap result = HdrUtilities.ConvertHdrToSdr(testBitmap);
+
+ // Assert
+ // The conversion should tone map bright values
+ // In this case, pure white (255,255,255) should remain relatively close
+ // but with tone mapping applied
+ Color centerPixel = result.GetPixel(5, 5);
+
+ // After tone mapping, pixels should still be bright but potentially adjusted
+ Assert.True(centerPixel.R >= 200, "Red channel should remain bright");
+ Assert.True(centerPixel.G >= 200, "Green channel should remain bright");
+ Assert.True(centerPixel.B >= 200, "Blue channel should remain bright");
+
+ // Cleanup
+ testBitmap.Dispose();
+ result.Dispose();
+ }
+
+ [Fact]
+ public void ConvertHdrToSdr_WithMixedPixels_ProcessesCorrectly()
+ {
+ // Arrange
+ Bitmap testBitmap = new(10, 10, PixelFormat.Format32bppArgb);
+ using (Graphics g = Graphics.FromImage(testBitmap))
+ {
+ // Fill with different colors to test tone mapping
+ using Brush darkBrush = new SolidBrush(Color.FromArgb(255, 50, 50, 50));
+ using Brush brightBrush = new SolidBrush(Color.FromArgb(255, 250, 250, 250));
+
+ g.FillRectangle(darkBrush, 0, 0, 5, 10);
+ g.FillRectangle(brightBrush, 5, 0, 5, 10);
+ }
+
+ // Act
+ Bitmap result = HdrUtilities.ConvertHdrToSdr(testBitmap);
+
+ // Assert
+ Assert.NotNull(result);
+ Color darkPixel = result.GetPixel(2, 5);
+ Color brightPixel = result.GetPixel(7, 5);
+
+ // Dark pixels should remain relatively dark
+ Assert.True(darkPixel.R < 100, "Dark pixel should remain dark");
+
+ // Bright pixels should be tone mapped
+ Assert.True(brightPixel.R > darkPixel.R, "Bright pixel should be brighter than dark pixel");
+
+ // Cleanup
+ testBitmap.Dispose();
+ result.Dispose();
+ }
+
+ [Fact]
+ public void ConvertHdrToSdr_PreservesAlphaChannel()
+ {
+ // Arrange
+ Bitmap testBitmap = new(10, 10, PixelFormat.Format32bppArgb);
+ using (Graphics g = Graphics.FromImage(testBitmap))
+ {
+ using Brush semiTransparentBrush = new SolidBrush(Color.FromArgb(128, 255, 255, 255));
+ g.FillRectangle(semiTransparentBrush, 0, 0, 10, 10);
+ }
+
+ // Act
+ Bitmap result = HdrUtilities.ConvertHdrToSdr(testBitmap);
+
+ // Assert
+ Color pixel = result.GetPixel(5, 5);
+ Assert.Equal(128, pixel.A);
+
+ // Cleanup
+ testBitmap.Dispose();
+ result.Dispose();
+ }
+
+ [Fact]
+ public void IsHdrEnabledAtPoint_WithValidCoordinates_DoesNotThrow()
+ {
+ // Arrange
+ int x = 100;
+ int y = 100;
+
+ // Act & Assert
+ // Should not throw exception even if HDR is not available
+ var exception = Record.Exception(() => HdrUtilities.IsHdrEnabledAtPoint(x, y));
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void IsHdrEnabledAtPoint_WithNegativeCoordinates_ReturnsFalse()
+ {
+ // Arrange
+ int x = -1;
+ int y = -1;
+
+ // Act
+ bool result = HdrUtilities.IsHdrEnabledAtPoint(x, y);
+
+ // Assert
+ // Should return false for invalid coordinates
+ Assert.False(result);
+ }
+}
diff --git a/Text-Grab/NativeMethods.cs b/Text-Grab/NativeMethods.cs
index cb15c71c..018ef01a 100644
--- a/Text-Grab/NativeMethods.cs
+++ b/Text-Grab/NativeMethods.cs
@@ -22,4 +22,59 @@ internal static partial class NativeMethods
[LibraryImport("shcore.dll")]
public static partial void GetScaleFactorForMonitor(IntPtr hMon, out uint pScale);
+
+ // HDR detection APIs
+ [DllImport("user32.dll")]
+ internal static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags);
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
+
+ [DllImport("gdi32.dll", CharSet = CharSet.Unicode)]
+ internal static extern IntPtr CreateDC(string? lpszDriver, string lpszDevice, string? lpszOutput, IntPtr lpInitData);
+
+ [DllImport("gdi32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool DeleteDC(IntPtr hdc);
+
+ [DllImport("gdi32.dll")]
+ internal static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
+
+ public const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
+ ///
+ /// Device capability index for GetDeviceCaps to query color management capabilities.
+ ///
+ public const int COLORMGMTCAPS = 121;
+ ///
+ /// Flag indicating that the device supports HDR (High Dynamic Range).
+ ///
+ public const int CM_HDR_SUPPORT = 0x00000001;
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct POINT
+ {
+ public int X;
+ public int Y;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct MONITORINFOEX
+ {
+ public uint cbSize;
+ public RECT rcMonitor;
+ public RECT rcWork;
+ public uint dwFlags;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string szDevice;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct RECT
+ {
+ public int Left;
+ public int Top;
+ public int Right;
+ public int Bottom;
+ }
}
\ No newline at end of file
diff --git a/Text-Grab/Utilities/HdrUtilities.cs b/Text-Grab/Utilities/HdrUtilities.cs
new file mode 100644
index 00000000..41befe5c
--- /dev/null
+++ b/Text-Grab/Utilities/HdrUtilities.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Runtime.InteropServices;
+
+namespace Text_Grab.Utilities;
+
+public static class HdrUtilities
+{
+ ///
+ /// Checks if HDR is enabled on the monitor at the specified screen coordinates.
+ ///
+ /// X coordinate on screen
+ /// Y coordinate on screen
+ /// True if HDR is enabled on the monitor, false otherwise
+ public static bool IsHdrEnabledAtPoint(int x, int y)
+ {
+ try
+ {
+ NativeMethods.POINT pt = new() { X = x, Y = y };
+ IntPtr hMonitor = NativeMethods.MonitorFromPoint(pt, NativeMethods.MONITOR_DEFAULTTONEAREST);
+
+ if (hMonitor == IntPtr.Zero)
+ return false;
+
+ NativeMethods.MONITORINFOEX monitorInfo = new()
+ {
+ cbSize = (uint)Marshal.SizeOf()
+ };
+
+ if (!NativeMethods.GetMonitorInfo(hMonitor, ref monitorInfo))
+ return false;
+
+ IntPtr hdc = NativeMethods.CreateDC(null, monitorInfo.szDevice, null, IntPtr.Zero);
+ if (hdc == IntPtr.Zero)
+ return false;
+
+ try
+ {
+ int colorCaps = NativeMethods.GetDeviceCaps(hdc, NativeMethods.COLORMGMTCAPS);
+ return (colorCaps & NativeMethods.CM_HDR_SUPPORT) != 0;
+ }
+ finally
+ {
+ NativeMethods.DeleteDC(hdc);
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Converts an HDR bitmap to SDR (Standard Dynamic Range) by applying tone mapping.
+ /// This fixes the overly bright appearance of screenshots taken on HDR displays.
+ ///
+ /// The bitmap to convert
+ /// A new bitmap with SDR color values, or null if the input is null
+ public static Bitmap? ConvertHdrToSdr(Bitmap? bitmap)
+ {
+ if (bitmap == null)
+ return null;
+
+ try
+ {
+ // Create a new bitmap with the same dimensions
+ Bitmap result = new(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb);
+
+ // Lock both bitmaps for fast pixel access
+ BitmapData sourceData = bitmap.LockBits(
+ new Rectangle(0, 0, bitmap.Width, bitmap.Height),
+ ImageLockMode.ReadOnly,
+ PixelFormat.Format32bppArgb);
+
+ BitmapData resultData = result.LockBits(
+ new Rectangle(0, 0, result.Width, result.Height),
+ ImageLockMode.WriteOnly,
+ PixelFormat.Format32bppArgb);
+
+ try
+ {
+ unsafe
+ {
+ byte* sourcePtr = (byte*)sourceData.Scan0;
+ byte* resultPtr = (byte*)resultData.Scan0;
+
+ int bytes = Math.Abs(sourceData.Stride) * sourceData.Height;
+
+ // Process each pixel
+ for (int i = 0; i < bytes; i += 4)
+ {
+ // Read BGRA values
+ byte b = sourcePtr[i];
+ byte g = sourcePtr[i + 1];
+ byte r = sourcePtr[i + 2];
+ byte a = sourcePtr[i + 3];
+
+ // Convert to linear RGB space (0.0 to 1.0)
+ double rLinear = SrgbToLinear(r / 255.0);
+ double gLinear = SrgbToLinear(g / 255.0);
+ double bLinear = SrgbToLinear(b / 255.0);
+
+ // Apply simple tone mapping (Reinhard operator)
+ // This compresses the HDR range to SDR range
+ rLinear = ToneMap(rLinear);
+ gLinear = ToneMap(gLinear);
+ bLinear = ToneMap(bLinear);
+
+ // Convert back to sRGB space
+ r = (byte)Math.Clamp((int)(LinearToSrgb(rLinear) * 255.0 + 0.5), 0, 255);
+ g = (byte)Math.Clamp((int)(LinearToSrgb(gLinear) * 255.0 + 0.5), 0, 255);
+ b = (byte)Math.Clamp((int)(LinearToSrgb(bLinear) * 255.0 + 0.5), 0, 255);
+
+ // Write BGRA values
+ resultPtr[i] = b;
+ resultPtr[i + 1] = g;
+ resultPtr[i + 2] = r;
+ resultPtr[i + 3] = a;
+ }
+ }
+ }
+ finally
+ {
+ bitmap.UnlockBits(sourceData);
+ result.UnlockBits(resultData);
+ }
+
+ return result;
+ }
+ catch
+ {
+ // If conversion fails, return original bitmap
+ return bitmap;
+ }
+ }
+
+ ///
+ /// Converts sRGB color value to linear RGB.
+ ///
+ private static double SrgbToLinear(double value)
+ {
+ if (value <= 0.04045)
+ return value / 12.92;
+ else
+ return Math.Pow((value + 0.055) / 1.055, 2.4);
+ }
+
+ ///
+ /// Converts linear RGB value to sRGB.
+ ///
+ private static double LinearToSrgb(double value)
+ {
+ if (value <= 0.0031308)
+ return 12.92 * value;
+ else
+ return 1.055 * Math.Pow(value, 1.0 / 2.4) - 0.055;
+ }
+
+ ///
+ /// Applies tone mapping to compress HDR values to SDR range.
+ /// Uses a modified Reinhard operator with exposure adjustment to preserve mid-tones while compressing highlights.
+ /// Formula: L_out = (L_in * exposure) / (1 + L_in * exposure)
+ ///
+ private static double ToneMap(double value)
+ {
+ const double exposure = 0.8; // Adjust exposure to darken the image slightly
+ value *= exposure;
+
+ // Apply tone mapping
+ return value / (1.0 + value);
+ }
+}
diff --git a/Text-Grab/Utilities/ImageMethods.cs b/Text-Grab/Utilities/ImageMethods.cs
index ae43afd5..c61a34c6 100644
--- a/Text-Grab/Utilities/ImageMethods.cs
+++ b/Text-Grab/Utilities/ImageMethods.cs
@@ -18,6 +18,27 @@ namespace Text_Grab;
public static class ImageMethods
{
+ ///
+ /// Converts a bitmap from HDR to SDR if HDR is detected at the specified location.
+ ///
+ /// The bitmap to potentially convert
+ /// X coordinate of the center of the captured region
+ /// Y coordinate of the center of the captured region
+ /// The SDR-converted bitmap if HDR was detected, otherwise the original bitmap
+ private static Bitmap ConvertHdrToSdrIfNeeded(Bitmap bitmap, int centerX, int centerY)
+ {
+ if (HdrUtilities.IsHdrEnabledAtPoint(centerX, centerY))
+ {
+ Bitmap? sdrBitmap = HdrUtilities.ConvertHdrToSdr(bitmap);
+ if (sdrBitmap != null && sdrBitmap != bitmap)
+ {
+ bitmap.Dispose();
+ return sdrBitmap;
+ }
+ }
+ return bitmap;
+ }
+
public static Bitmap PadImage(Bitmap image, int minW = 64, int minH = 64)
{
if (image.Height >= minH && image.Width >= minW)
@@ -96,6 +117,10 @@ public static Bitmap GetRegionOfScreenAsBitmap(Rectangle region)
using Graphics g = Graphics.FromImage(bmp);
g.CopyFromScreen(region.Left, region.Top, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy);
+
+ // Convert HDR to SDR if needed
+ bmp = ConvertHdrToSdrIfNeeded(bmp, region.Left + region.Width / 2, region.Top + region.Height / 2);
+
bmp = PadImage(bmp);
Singleton.Instance.CacheLastBitmap(bmp);
@@ -141,6 +166,10 @@ public static Bitmap GetWindowsBoundsBitmap(Window passedWindow)
using Graphics g = Graphics.FromImage(bmp);
g.CopyFromScreen(thisCorrectedLeft, thisCorrectedTop, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy);
+
+ // Convert HDR to SDR if needed
+ bmp = ConvertHdrToSdrIfNeeded(bmp, thisCorrectedLeft + windowWidth / 2, thisCorrectedTop + windowHeight / 2);
+
return bmp;
}