@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user