Files
pyramid/lib/utils/clipboard_image.dart
Bernd Steckmeister 2395db50be style: weitere flutter-analyze Infos beheben
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>
2026-07-03 06:32:50 +02:00

183 lines
6.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}