Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions Tests/HdrTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
55 changes: 55 additions & 0 deletions Text-Grab/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/// <summary>
/// Device capability index for GetDeviceCaps to query color management capabilities.
/// </summary>
public const int COLORMGMTCAPS = 121;
/// <summary>
/// Flag indicating that the device supports HDR (High Dynamic Range).
/// </summary>
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;
}
}
173 changes: 173 additions & 0 deletions Text-Grab/Utilities/HdrUtilities.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Checks if HDR is enabled on the monitor at the specified screen coordinates.
/// </summary>
/// <param name="x">X coordinate on screen</param>
/// <param name="y">Y coordinate on screen</param>
/// <returns>True if HDR is enabled on the monitor, false otherwise</returns>
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<NativeMethods.MONITORINFOEX>()
};

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;
}
}

/// <summary>
/// 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.
/// </summary>
/// <param name="bitmap">The bitmap to convert</param>
/// <returns>A new bitmap with SDR color values, or null if the input is null</returns>
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;
}
}

/// <summary>
/// Converts sRGB color value to linear RGB.
/// </summary>
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);
}

/// <summary>
/// Converts linear RGB value to sRGB.
/// </summary>
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;
}

/// <summary>
/// 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)
/// </summary>
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);
}
}
Loading