Files
pyramid/lib/utils/clipboard_image.dart
T
Bernd Steckmeister 25ed765a03 wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.)
als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen
Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-03 05:47:18 +02:00

183 lines
6.7 KiB
Dart
Raw 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 _CF_DIB = 8;
const _CF_DIBV4 = 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 [_CF_DIBV4, _CF_DIB]) {
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;
}
}