diff --git a/packages/dartamp_skin/lib/src/cur_decoder.dart b/packages/dartamp_skin/lib/src/cur_decoder.dart index 5f62c3d..16e1d56 100644 --- a/packages/dartamp_skin/lib/src/cur_decoder.dart +++ b/packages/dartamp_skin/lib/src/cur_decoder.dart @@ -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 _rgbaToUiImage(int width, int height, Uint8List rgba) { - final completer = Completer(); - ui.decodeImageFromPixels( - rgba, - width, - height, - ui.PixelFormat.rgba8888, - completer.complete, - ); - return completer.future; + static Future _rgbaToUiImage( + int width, + int height, + Uint8List rgba, + ) { + final completer = Completer(); + ui.decodeImageFromPixels( + rgba, + width, + height, + ui.PixelFormat.rgba8888, + completer.complete, + ); + return completer.future; + } }