2395db50be
Ungenutzte Imports (dart:typed_data, flutter/services.dart), unnötige String-Interpolations-Klammern, führender Unterstrich bei lokaler Funktion, fehlende Blockklammern bei if-Statements und SCREAMING_CASE-Konstanten in clipboard_image.dart auf lowerCamelCase umbenannt. Rein mechanisch, keine Verhaltensänderung. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
183 lines
6.7 KiB
Dart
183 lines
6.7 KiB
Dart
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<Uint8> Function(IntPtr hMem);
|
||
typedef _GlobalLock = Pointer<Uint8> Function(int hMem);
|
||
|
||
typedef _GlobalUnlockNative = Int32 Function(IntPtr hMem);
|
||
typedef _GlobalUnlock = int Function(int hMem);
|
||
|
||
typedef _RegisterFormatNative = Uint32 Function(Pointer<Utf16> name);
|
||
typedef _RegisterFormat = int Function(Pointer<Utf16> 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;
|
||
}
|
||
}
|