Fix cursor decoding

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-08-16 20:36:16 +02:00
parent 7c58811fad
commit 4770b29d98
+82 -32
View File
@@ -45,71 +45,117 @@ class CurDecoder {
final hotspotY = view.getUint16(entry + 6, Endian.little); final hotspotY = view.getUint16(entry + 6, Endian.little);
final size = view.getUint32(entry + 8, Endian.little); final size = view.getUint32(entry + 8, Endian.little);
final offset = view.getUint32(entry + 12, Endian.little); final offset = view.getUint32(entry + 12, Endian.little);
if (offset + size > bytes.length) { if (offset + size > bytes.length) {
throw const FormatException("cursor bitmap data truncated"); throw const FormatException("cursor bitmap data truncated");
} }
final dib = ByteData.sublistView(bytes, offset, offset + size); final dib = ByteData.sublistView(bytes, offset, offset + size);
if (size < 40) { if (dib.lengthInBytes < 40) {
throw const FormatException("cursor DIB data too small");
}
// Read the actual DIB header size (usually 40, but can be larger like 108 or 124)
final dibHeaderSize = dib.getUint32(0, Endian.little);
if (dibHeaderSize < 40) {
throw const FormatException("cursor DIB header too small"); throw const FormatException("cursor DIB header too small");
} }
final width = dib.getInt32(4, Endian.little); final width = dib.getInt32(4, Endian.little);
final imageHeight = dib.getInt32(8, Endian.little); final imageHeight = dib.getInt32(8, Endian.little);
final planes = dib.getUint16(12, Endian.little); final planes = dib.getUint16(12, Endian.little);
final bpp = dib.getUint16(14, Endian.little); final bpp = dib.getUint16(14, Endian.little);
// Read the actual number of colors used from the header
final clrUsed = dib.getUint32(32, Endian.little);
if (planes != 1) throw const FormatException("unsupported cursor planes"); if (planes != 1) throw const FormatException("unsupported cursor planes");
if (width <= 0 || width > 256) if (width <= 0 || width > 256)
throw const FormatException("bad cursor width"); throw const FormatException("bad cursor width");
if (imageHeight <= 0 || imageHeight > 512) { if (imageHeight <= 0 || imageHeight > 1024) {
throw const FormatException("bad cursor height"); throw const FormatException("bad cursor height");
} }
// The DIB height doubles the XOR bitmap with the AND mask underneath. // The DIB height doubles the XOR bitmap with the AND mask underneath.
final height = imageHeight ~/ 2; final height = imageHeight ~/ 2;
final paletteCount = bpp <= 8 ? (1 << bpp) : 0;
const paletteOffset = 40; // Properly calculate palette count relying on header, fallback to bit count.
final paletteCount = (bpp <= 8) ? (clrUsed > 0 ? clrUsed : (1 << bpp)) : 0;
final paletteOffset = dibHeaderSize;
final xorOffset = paletteOffset + paletteCount * 4; final xorOffset = paletteOffset + paletteCount * 4;
final xorStride = ((width * bpp + 31) >> 5) << 2; final xorStride = ((width * bpp + 31) >> 5) << 2;
final andStride = ((width + 31) >> 5) << 2; final andStride = ((width + 31) >> 5) << 2;
final andOffset = xorOffset + xorStride * height; final andOffset = xorOffset + xorStride * height;
// A final bounds check to ensure masks don't overrun the file bytes
if (offset + andOffset + andStride * height > bytes.length) {
throw const FormatException("cursor pixel data bounds truncated");
}
final rgba = Uint8List(width * height * 4); final rgba = Uint8List(width * height * 4);
for (var row = 0; row < height; row++) { for (var row = 0; row < height; row++) {
final srcRow = height - 1 - row; // DIB rows are bottom-up final srcRow = height - 1 - row; // DIB rows are bottom-up
final xorBase = offset + xorOffset + srcRow * xorStride; final xorBase = offset + xorOffset + srcRow * xorStride;
final andBase = offset + andOffset + srcRow * andStride; final andBase = offset + andOffset + srcRow * andStride;
for (var x = 0; x < width; x++) { for (var x = 0; x < width; x++) {
final dest = (row * width + x) * 4; final dest = (row * width + x) * 4;
final andByte = bytes[andBase + x >> 3];
if (((andByte >> (7 - (x & 7))) & 1) != 0) { // FIX: Added parentheses so addition evaluates before bitwise shifting
// Masked out: transparent. final andByte = bytes[andBase + (x >> 3)];
rgba[dest + 3] = 0; final isTransparent = ((andByte >> (7 - (x & 7))) & 1) != 0;
if (isTransparent) {
// Masked out: transparent. (RGBA array is zero-initialized by default)
continue; continue;
} }
if (bpp == 32) { if (bpp == 32) {
final p = xorBase + x * 4; final p = xorBase + x * 4;
rgba[dest] = bytes[p + 2]; rgba[dest] = bytes[p + 2]; // R
rgba[dest + 1] = bytes[p + 1]; rgba[dest + 1] = bytes[p + 1]; // G
rgba[dest + 2] = bytes[p]; rgba[dest + 2] = bytes[p]; // B
rgba[dest + 3] = bytes[p + 3];
// Legacy fallback: If the cursor doesn't utilize a proper alpha channel
// (alpha evaluates to 0) but the AND mask states it's visible, force it to 255.
rgba[dest + 3] = bytes[p + 3] == 0 ? 255 : bytes[p + 3];
continue; continue;
} }
if (bpp == 24) {
final p = xorBase + x * 3;
rgba[dest] = bytes[p + 2]; // R
rgba[dest + 1] = bytes[p + 1]; // G
rgba[dest + 2] = bytes[p]; // B
rgba[dest + 3] = 255; // A
continue;
}
int index; int index;
if (bpp == 1) { if (bpp == 1) {
final byte = bytes[xorBase + x >> 3]; final byte = bytes[xorBase + (x >> 3)]; // FIX: Parentheses
index = (byte >> (7 - (x & 7))) & 1; index = (byte >> (7 - (x & 7))) & 1;
} else if (bpp == 4) { } else if (bpp == 4) {
final byte = bytes[xorBase + x >> 1]; final byte = bytes[xorBase + (x >> 1)]; // FIX: Parentheses
index = x.isEven ? (byte >> 4) & 0xF : byte & 0xF; index = x.isEven ? (byte >> 4) & 0xF : byte & 0xF;
} else { } else if (bpp == 8) {
index = bytes[xorBase + x]; index = bytes[xorBase + x];
} else {
throw FormatException("unsupported bpp: $bpp");
}
final paletteIndex = index * 4;
// Gracefully handle malformed indices that overrun the provided palette
if (paletteIndex + 3 < paletteCount * 4) {
final p = offset + paletteOffset + paletteIndex;
rgba[dest] = bytes[p + 2]; // R
rgba[dest + 1] = bytes[p + 1]; // G
rgba[dest + 2] = bytes[p]; // B
rgba[dest + 3] = 255; // A
} else {
rgba[dest + 3] = 255; // Draw fallback visible pixel (Black)
} }
final paletteIndex = (index * 4).clamp(0, paletteCount * 4 - 4);
final p = offset + paletteOffset + paletteIndex;
rgba[dest] = bytes[p + 2];
rgba[dest + 1] = bytes[p + 1];
rgba[dest + 2] = bytes[p];
rgba[dest + 3] = 255;
} }
} }
@@ -120,16 +166,20 @@ class CurDecoder {
hotspotY: hotspotY.clamp(0, height - 1), hotspotY: hotspotY.clamp(0, height - 1),
); );
} }
}
Future<ui.Image> _rgbaToUiImage(int width, int height, Uint8List rgba) { static Future<ui.Image> _rgbaToUiImage(
final completer = Completer<ui.Image>(); int width,
ui.decodeImageFromPixels( int height,
rgba, Uint8List rgba,
width, ) {
height, final completer = Completer<ui.Image>();
ui.PixelFormat.rgba8888, ui.decodeImageFromPixels(
completer.complete, rgba,
); width,
return completer.future; height,
ui.PixelFormat.rgba8888,
completer.complete,
);
return completer.future;
}
} }