From a23f1b18c556d1ed0e803970c13eae57f9fe7958 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 10 Feb 2026 12:00:08 +0000 Subject: [PATCH] Windows: Fix clipboard bitmap pixel shift (#20654) * Windows: Fix clipboard bitmap pixel shift * Remove BITMAPINFO --- .../Interop/UnmanagedMethods.cs | 40 ++---------- .../Avalonia.Win32/OleDataObjectHelper.cs | 62 +++++++++++++------ 2 files changed, 48 insertions(+), 54 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b76af4fd20..f41603b229 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1173,32 +1173,6 @@ namespace Avalonia.Win32.Interop public int ciexyzZ; } - [StructLayout(LayoutKind.Sequential)] - public struct BITMAPINFO - { - // C# cannot inlay structs in structs so must expand directly here - // - //[StructLayout(LayoutKind.Sequential)] - //public struct BITMAPINFOHEADER - //{ - public uint biSize; - public int biWidth; - public int biHeight; - public ushort biPlanes; - public ushort biBitCount; - public BitmapCompressionMode biCompression; - public uint biSizeImage; - public int biXPelsPerMeter; - public int biYPelsPerMeter; - public uint biClrUsed; - public uint biClrImportant; - //} - public uint biRedMask; - public uint biGreenMask; - public uint biBlueMask; - - } - [StructLayout(LayoutKind.Sequential)] public struct MINMAXINFO { @@ -1266,16 +1240,10 @@ namespace Avalonia.Win32.Interop uint dwWidth, uint dwHeight, int XSrc, int YSrc, uint uStartScan, uint cScanLines, - IntPtr lpvBits, [In] ref BITMAPINFO lpbmi, uint fuColorUse); - - [DllImport("gdi32.dll")] - public static extern int SetDIBits(IntPtr hdc, IntPtr hbm, uint start, uint cLines, IntPtr lpBits, in BITMAPINFO lpbmi, uint fuColorUse); - - [DllImport("gdi32.dll")] - public static extern int SetDIBits(IntPtr hdc, IntPtr hbm, uint start, uint cLines, IntPtr lpBits, in BITMAPINFOHEADER lpbmi, uint fuColorUse); + IntPtr lpvBits, IntPtr lpbmi, uint fuColorUse); [DllImport("gdi32.dll")] - public static extern int SetDIBits(IntPtr hdc, IntPtr hbm, uint start, uint cLines, IntPtr lpBits, in BITMAPV5HEADER lpbmi, uint fuColorUse); + public static extern int SetDIBits(IntPtr hdc, IntPtr hbm, uint start, uint cLines, IntPtr lpBits, IntPtr lpbmi, uint fuColorUse); [DllImport("gdi32.dll", SetLastError = false, ExactSpelling = true)] public static extern IntPtr CreateRectRgn(int x1, int y1, int x2, int y2); @@ -1776,7 +1744,7 @@ namespace Avalonia.Win32.Interop [DllImport("gdi32.dll")] public static extern IntPtr CreateDIBSection(IntPtr hDC, in BITMAPV5HEADER pBitmapInfo, DIBColorTable usage, out IntPtr lplpVoid, IntPtr handle, int dw); [DllImport("gdi32.dll")] - public static extern IntPtr CreateDIBitmap(IntPtr hDC, in BITMAPV5HEADER pBitmapInfo, int flInit, IntPtr lplpVoid, in BITMAPINFO pbmi, DIBColorTable iUsage); + public static extern IntPtr CreateDIBitmap(IntPtr hDC, in BITMAPV5HEADER pBitmapInfo, int flInit, IntPtr lplpVoid, IntPtr pbmi, DIBColorTable iUsage); [DllImport("gdi32.dll")] public static extern bool GdiFlush(); @@ -1811,7 +1779,7 @@ namespace Avalonia.Win32.Interop public static extern int DescribePixelFormat(IntPtr hdc, int iPixelFormat, int bytes, ref PixelFormatDescriptor pfd); [DllImport("gdi32.dll")] - public static extern int StretchDIBits(IntPtr hdc, int xDest, int yDest, int DestWidth, int DestHeight, int xSrc, int ySrc, int SrcWidth, int SrcHeight, IntPtr lpBits, [In] ref BITMAPINFO lpbmi, uint iUsage, uint rop); + public static extern int StretchDIBits(IntPtr hdc, int xDest, int yDest, int DestWidth, int DestHeight, int xSrc, int ySrc, int SrcWidth, int SrcHeight, IntPtr lpBits, IntPtr lpbmi, uint iUsage, uint rop); [DllImport("gdi32.dll")] public static extern int StretchBlt(IntPtr hdc, int xDest, int yDest, int DestWidth, int DestHeight, IntPtr hdcSrc, int xSrc, int ySrc, int SrcWidth, int SrcHeight, uint rop); diff --git a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs index d603b3497b..6104613761 100644 --- a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs +++ b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs @@ -193,19 +193,19 @@ internal static class OleDataObjectHelper var data = ReadBytesFromHGlobal(hGlobal); fixed (byte* ptr = data) { - var bitmapInfo = Marshal.PtrToStructure((IntPtr)ptr); + var sourceHeader = Marshal.PtrToStructure((IntPtr)ptr); - var bitmapInfoHeader = new BITMAPINFOHEADER() + var destHeader = new BITMAPINFOHEADER() { - biWidth = bitmapInfo.biWidth, - biHeight = bitmapInfo.biHeight, - biPlanes = bitmapInfo.biPlanes, + biWidth = sourceHeader.biWidth, + biHeight = sourceHeader.biHeight, + biPlanes = sourceHeader.biPlanes, biBitCount = 32, - biCompression = 0, - biSizeImage = (uint)(bitmapInfo.biWidth * 4 * Math.Abs(bitmapInfo.biHeight)) + biCompression = BitmapCompressionMode.BI_RGB, + biSizeImage = (uint)(sourceHeader.biWidth * 4 * Math.Abs(sourceHeader.biHeight)) }; - bitmapInfoHeader.Init(); + destHeader.Init(); IntPtr hdc = IntPtr.Zero, compatDc = IntPtr.Zero, section = IntPtr.Zero; try @@ -218,31 +218,33 @@ internal static class OleDataObjectHelper if (compatDc == IntPtr.Zero) return null; - section = CreateDIBSection(compatDc, ref bitmapInfoHeader, 0, out var lbBits, IntPtr.Zero, 0); + section = CreateDIBSection(compatDc, ref destHeader, 0, out var lbBits, IntPtr.Zero, 0); if (section == IntPtr.Zero) return null; + var extraSourceHeaderSize = GetExtraHeaderSize(sourceHeader); + SelectObject(compatDc, section); if (StretchDIBits(compatDc, 0, - bitmapInfo.biHeight, - bitmapInfo.biWidth, - -bitmapInfo.biHeight, + sourceHeader.biHeight - 1, + sourceHeader.biWidth, + -sourceHeader.biHeight, 0, 0, - bitmapInfoHeader.biWidth, - bitmapInfoHeader.biHeight, - (IntPtr)(ptr + bitmapInfo.biSize), - ref bitmapInfo, + destHeader.biWidth, + destHeader.biHeight, + (IntPtr)(ptr + (sourceHeader.biSize + extraSourceHeaderSize)), + (IntPtr)ptr, 0, SRCCOPY ) != 0) return new Bitmap(Platform.PixelFormats.Bgra8888, Platform.AlphaFormat.Opaque, lbBits, - new PixelSize(bitmapInfoHeader.biWidth, bitmapInfoHeader.biHeight), + new PixelSize(destHeader.biWidth, destHeader.biHeight), new Vector(96, 96), - bitmapInfoHeader.biWidth * 4); + destHeader.biWidth * 4); } finally { @@ -274,6 +276,30 @@ internal static class OleDataObjectHelper return null; } + private static int GetExtraHeaderSize(in BITMAPINFOHEADER header) + { + // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader + switch (header.biCompression) + { + // If biCompression equals BI_RGB and the bitmap uses 8 bpp or less, the bitmap has a color table immediately + // following the BITMAPINFOHEADER structure. The color table consists of an array of RGBQUAD values. The size + // of the array is given by the biClrUsed member. + // If biClrUsed is zero, the array contains the maximum number of colors for the given bitdepth; that is, + // 2^biBitCount colors. + case BitmapCompressionMode.BI_RGB when header.biBitCount <= 8: + return (header.biClrUsed == 0 ? 1 << header.biBitCount : (int)header.biClrUsed) * 4; + + // If biCompression equals BI_BITFIELDS, the bitmap uses three DWORD color masks (red, green, and blue, + // respectively), which specify the byte layout of the pixels. The 1 bits in each mask indicate the bits for + // that color within the pixel. + case BitmapCompressionMode.BI_BITFIELDS: + return 3 * 4; + + default: + return 0; + } + } + private static string? ReadStringFromHGlobal(IntPtr hGlobal) { var sourcePtr = GlobalLock(hGlobal);