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 size = view.getUint32(entry + 8, Endian.little);
final offset = view.getUint32(entry + 12, Endian.little);
if (offset + size > bytes.length) {
throw const FormatException("cursor bitmap data truncated");
}
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");
}
final width = dib.getInt32(4, Endian.little);
final imageHeight = dib.getInt32(8, Endian.little);
final planes = dib.getUint16(12, 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 (width <= 0 || width > 256)
throw const FormatException("bad cursor width");
if (imageHeight <= 0 || imageHeight > 512) {
if (imageHeight <= 0 || imageHeight > 1024) {
throw const FormatException("bad cursor height");
}
// The DIB height doubles the XOR bitmap with the AND mask underneath.
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 xorStride = ((width * bpp + 31) >> 5) << 2;
final andStride = ((width + 31) >> 5) << 2;
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);
for (var row = 0; row < height; row++) {
final srcRow = height - 1 - row; // DIB rows are bottom-up
final xorBase = offset + xorOffset + srcRow * xorStride;
final andBase = offset + andOffset + srcRow * andStride;
for (var x = 0; x < width; x++) {
final dest = (row * width + x) * 4;
final andByte = bytes[andBase + x >> 3];
if (((andByte >> (7 - (x & 7))) & 1) != 0) {
// Masked out: transparent.
rgba[dest + 3] = 0;
// FIX: Added parentheses so addition evaluates before bitwise shifting
final andByte = bytes[andBase + (x >> 3)];
final isTransparent = ((andByte >> (7 - (x & 7))) & 1) != 0;
if (isTransparent) {
// Masked out: transparent. (RGBA array is zero-initialized by default)
continue;
}
if (bpp == 32) {
final p = xorBase + x * 4;
rgba[dest] = bytes[p + 2];
rgba[dest + 1] = bytes[p + 1];
rgba[dest + 2] = bytes[p];
rgba[dest + 3] = bytes[p + 3];
rgba[dest] = bytes[p + 2]; // R
rgba[dest + 1] = bytes[p + 1]; // G
rgba[dest + 2] = bytes[p]; // B
// 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;
}
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;
if (bpp == 1) {
final byte = bytes[xorBase + x >> 3];
final byte = bytes[xorBase + (x >> 3)]; // FIX: Parentheses
index = (byte >> (7 - (x & 7))) & 1;
} 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;
} else {
} else if (bpp == 8) {
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),
);
}
}
Future<ui.Image> _rgbaToUiImage(int width, int height, Uint8List rgba) {
final completer = Completer<ui.Image>();
ui.decodeImageFromPixels(
rgba,
width,
height,
ui.PixelFormat.rgba8888,
completer.complete,
);
return completer.future;
static Future<ui.Image> _rgbaToUiImage(
int width,
int height,
Uint8List rgba,
) {
final completer = Completer<ui.Image>();
ui.decodeImageFromPixels(
rgba,
width,
height,
ui.PixelFormat.rgba8888,
completer.complete,
);
return completer.future;
}
}