import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'package:image/image.dart' as img; // ── Windows constants ──────────────────────────────────────────────────────── const _cfDib = 8; const _cfDibV4 = 20; // ── Native function typedefs ───────────────────────────────────────────────── typedef _OpenClipboardNative = Int32 Function(IntPtr hwnd); typedef _OpenClipboard = int Function(int hwnd); typedef _CloseClipboardNative = Int32 Function(); typedef _CloseClipboard = int Function(); typedef _IsFormatAvailableNative = Int32 Function(Uint32 format); typedef _IsFormatAvailable = int Function(int format); typedef _GetClipboardDataNative = IntPtr Function(Uint32 format); typedef _GetClipboardData = int Function(int format); typedef _GlobalSizeNative = IntPtr Function(IntPtr hMem); typedef _GlobalSize = int Function(int hMem); typedef _GlobalLockNative = Pointer Function(IntPtr hMem); typedef _GlobalLock = Pointer Function(int hMem); typedef _GlobalUnlockNative = Int32 Function(IntPtr hMem); typedef _GlobalUnlock = int Function(int hMem); typedef _RegisterFormatNative = Uint32 Function(Pointer name); typedef _RegisterFormat = int Function(Pointer name); // ── Public API ─────────────────────────────────────────────────────────────── /// Returns image bytes (PNG) from the Windows clipboard, or null if there is /// no image or the platform is not Windows. /// /// Checks the following formats in order: /// 1. "PNG" – custom registered format used by browsers / screenshot tools /// 2. CF_DIBV4 / CF_DIB – Device-Independent Bitmap (Win+Shift+S, etc.) Uint8List? getClipboardImageBytes() { if (!Platform.isWindows) return null; try { return _readClipboardImage(); } catch (_) { return null; } } // ── Implementation ─────────────────────────────────────────────────────────── Uint8List? _readClipboardImage() { final user32 = DynamicLibrary.open('user32.dll'); final kernel32 = DynamicLibrary.open('kernel32.dll'); final openClipboard = user32.lookupFunction<_OpenClipboardNative, _OpenClipboard>('OpenClipboard'); final closeClipboard = user32.lookupFunction<_CloseClipboardNative, _CloseClipboard>('CloseClipboard'); final isFormatAvailable = user32.lookupFunction<_IsFormatAvailableNative, _IsFormatAvailable>( 'IsClipboardFormatAvailable'); final getClipboardData = user32.lookupFunction<_GetClipboardDataNative, _GetClipboardData>('GetClipboardData'); final registerFormat = user32.lookupFunction<_RegisterFormatNative, _RegisterFormat>( 'RegisterClipboardFormatW'); final globalSize = kernel32.lookupFunction<_GlobalSizeNative, _GlobalSize>('GlobalSize'); final globalLock = kernel32.lookupFunction<_GlobalLockNative, _GlobalLock>('GlobalLock'); final globalUnlock = kernel32.lookupFunction<_GlobalUnlockNative, _GlobalUnlock>('GlobalUnlock'); // Register custom "PNG" format (Chrome, Edge, Firefox, Snipping Tool export) final pngNamePtr = 'PNG'.toNativeUtf16(); final pngFormat = registerFormat(pngNamePtr); malloc.free(pngNamePtr); if (openClipboard(0) == 0) return null; try { // 1. Try custom PNG format — bytes are ready-to-use PNG if (pngFormat != 0 && isFormatAvailable(pngFormat) != 0) { final bytes = _readGlobalBytes( getClipboardData(pngFormat), globalSize, globalLock, globalUnlock); if (bytes != null && bytes.isNotEmpty) return bytes; } // 2. Try CF_DIBV4 then CF_DIB — convert DIB to PNG via the image package for (final fmt in [_cfDibV4, _cfDib]) { if (isFormatAvailable(fmt) != 0) { final dibBytes = _readGlobalBytes( getClipboardData(fmt), globalSize, globalLock, globalUnlock); if (dibBytes != null && dibBytes.isNotEmpty) { final png = _dibToPng(dibBytes); if (png != null) return png; } } } return null; } finally { closeClipboard(); } } /// Copy the bytes pointed to by a global memory handle. Uint8List? _readGlobalBytes( int handle, _GlobalSize globalSize, _GlobalLock globalLock, _GlobalUnlock globalUnlock, ) { if (handle == 0) return null; final size = globalSize(handle); if (size == 0) return null; final ptr = globalLock(handle); if (ptr.address == 0) return null; try { return Uint8List.fromList(ptr.asTypedList(size)); } finally { globalUnlock(handle); } } /// Convert a CF_DIB / CF_DIBV4 block to PNG bytes using the `image` package. /// /// A DIB is essentially a BMP without the 14-byte file header. We prepend the /// header and let the `image` package decode it. Uint8List? _dibToPng(Uint8List dib) { if (dib.length < 40) return null; // minimum BITMAPINFOHEADER size final bd = ByteData.sublistView(dib); final headerSize = bd.getUint32(0, Endian.little); // biSize final bitCount = bd.getUint16(14, Endian.little); // biBitCount final compression = bd.getUint32(16, Endian.little); // biCompression final clrUsed = bd.getUint32(32, Endian.little); // biClrUsed // Calculate color table size (in bytes, each entry = 4 bytes RGBQUAD) int colorTableEntries; if (bitCount <= 8) { colorTableEntries = clrUsed != 0 ? clrUsed : (1 << bitCount); } else if (compression == 3 /* BI_BITFIELDS */ || compression == 6 /* BI_ALPHABITFIELDS */) { colorTableEntries = 0; } else { colorTableEntries = clrUsed; } final colorTableBytes = colorTableEntries * 4; // Offset to pixel data from start of FILE (= 14 file header bytes + DIB) final pixelOffset = 14 + headerSize + colorTableBytes; // Build the 14-byte BITMAPFILEHEADER final fileSize = 14 + dib.length; final fileHeader = Uint8List(14); final fh = ByteData.sublistView(fileHeader); fileHeader[0] = 0x42; // 'B' fileHeader[1] = 0x4D; // 'M' fh.setUint32(2, fileSize, Endian.little); fh.setUint32(6, 0, Endian.little); fh.setUint32(10, pixelOffset, Endian.little); final bmp = Uint8List(fileHeader.length + dib.length); bmp.setRange(0, fileHeader.length, fileHeader); bmp.setRange(fileHeader.length, bmp.length, dib); try { final decoded = img.decodeBmp(bmp); if (decoded == null) return null; return Uint8List.fromList(img.encodePng(decoded)); } catch (_) { return null; } }