Fix cursor decoding in some special cases
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -260,6 +260,11 @@ void _startAnimation(String name, bool rtl) {
|
||||
_animationTimer?.cancel();
|
||||
final registration = _animated[name];
|
||||
if (registration == null) return;
|
||||
registration.index = 0;
|
||||
// Show the first frame immediately; a held frame (e.g. a long first delay)
|
||||
// should still be visible the moment the cursor is activated.
|
||||
final suffix = rtl ? _rtlSuffix : "";
|
||||
unawaited(setCursor("$name#0$suffix"));
|
||||
_scheduleFrame(name, rtl);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ import "cur_decoder.dart";
|
||||
/// Decodes Windows animated cursor (`.ani`) resources.
|
||||
///
|
||||
/// An `.ani` file is a RIFF `ACON` container holding an `anih` header, a
|
||||
/// `LIST fram` of `icon` subchunks (each a single-frame `.cur`), and an
|
||||
/// optional `seq ` chunk that plays frames out of order. This decoder reads
|
||||
/// `LIST fram` of `icon` subchunks (each a single-frame `.cur`), an optional
|
||||
/// `seq ` chunk that plays frames out of order, and an optional `rate` chunk
|
||||
/// giving each play step's duration in 1/60ths of a second. This decoder reads
|
||||
/// those structures and produces an [AnimatedCursor].
|
||||
class AniDecoder {
|
||||
AniDecoder._();
|
||||
@@ -28,7 +29,8 @@ class AniDecoder {
|
||||
// Walk the top-level RIFF chunks.
|
||||
var baseDelayMicros = 16 * 1000;
|
||||
final frames = <Cursor>[];
|
||||
List<(int, int)>? seq;
|
||||
List<int>? seq;
|
||||
List<int>? rates;
|
||||
for (var offset = 12; offset + 8 <= bytes.length;) {
|
||||
final id = _fourCc(view, offset);
|
||||
final size = view.getUint32(offset + 4, Endian.little);
|
||||
@@ -49,15 +51,18 @@ class AniDecoder {
|
||||
frames.addAll(await _parseFrames(view, body + 4, body + size));
|
||||
}
|
||||
} else if (id == "seq ") {
|
||||
// The seq chunk is a list of (frameIndex, delay) pairs; the delay is
|
||||
// a per-step rate in 1/60ths of a second (0 meaning "use iDispRate").
|
||||
final pairCount = size ~/ 4;
|
||||
// The seq chunk is an array of frame indices into the fram LIST,
|
||||
// defining the playback order (frames may repeat).
|
||||
seq = [
|
||||
for (var i = 0; i < pairCount; i++)
|
||||
(
|
||||
view.getUint16(body + i * 4, Endian.little),
|
||||
view.getUint16(body + i * 4 + 2, Endian.little),
|
||||
),
|
||||
for (var i = 0; i < size ~/ 4; i++)
|
||||
view.getUint32(body + i * 4, Endian.little),
|
||||
];
|
||||
} else if (id == "rate") {
|
||||
// The rate chunk arrays a display rate in 1/60ths of a second for each
|
||||
// step of the seq chunk (or each frame in index order when absent).
|
||||
rates = [
|
||||
for (var i = 0; i < size ~/ 4; i++)
|
||||
view.getUint32(body + i * 4, Endian.little),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -67,20 +72,16 @@ class AniDecoder {
|
||||
|
||||
if (frames.isEmpty) throw const FormatException("ani has no frames");
|
||||
|
||||
final play = seq ?? List<int>.generate(frames.length, (i) => i);
|
||||
final ordered = <Cursor>[];
|
||||
final delays = <Duration>[];
|
||||
if (seq != null) {
|
||||
for (final (index, delay) in seq) {
|
||||
if (index < 0 || index >= frames.length) continue;
|
||||
ordered.add(frames[index]);
|
||||
final micros = delay > 0 ? delay * 16666 : baseDelayMicros;
|
||||
delays.add(Duration(microseconds: micros));
|
||||
}
|
||||
} else {
|
||||
ordered.addAll(frames);
|
||||
for (var i = 0; i < frames.length; i++) {
|
||||
delays.add(Duration(microseconds: baseDelayMicros));
|
||||
}
|
||||
for (var i = 0; i < play.length; i++) {
|
||||
final index = play[i];
|
||||
if (index < 0 || index >= frames.length) continue;
|
||||
ordered.add(frames[index]);
|
||||
final rate = rates != null && i < rates.length ? rates[i] : 0;
|
||||
final micros = rate > 0 ? rate * 16666 : baseDelayMicros;
|
||||
delays.add(Duration(microseconds: micros));
|
||||
}
|
||||
if (ordered.isEmpty)
|
||||
throw const FormatException("ani has no playable frames");
|
||||
@@ -89,6 +90,11 @@ class AniDecoder {
|
||||
}
|
||||
|
||||
/// Decodes [bytes] into an animated cursor, scaling frames per [options].
|
||||
///
|
||||
/// A `seq ` chunk may reference the same frame (and so the same
|
||||
/// [Cursor.image]) multiple times in different play steps. Scaling reuses
|
||||
/// the result for each duplicate so the shared source image is consumed
|
||||
/// exactly once.
|
||||
static Future<AnimatedCursor> decodeWithOptions(
|
||||
Uint8List bytes,
|
||||
DecodeOptions options,
|
||||
@@ -97,31 +103,42 @@ class AniDecoder {
|
||||
final target = options.targetSize;
|
||||
if (target == null && options.systemScale == 1.0) return animated;
|
||||
|
||||
final cache = <Cursor, Future<Cursor>>{};
|
||||
final frames = <Cursor>[];
|
||||
for (final cursor in animated.frames) {
|
||||
if (target != null) {
|
||||
frames.add(
|
||||
await CursorScaler.scaleTo(
|
||||
cursor.image,
|
||||
cursor.hotspotX,
|
||||
cursor.hotspotY,
|
||||
target,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
frames.add(
|
||||
await CursorScaler.scaleTo(
|
||||
cursor.image,
|
||||
cursor.hotspotX,
|
||||
cursor.hotspotY,
|
||||
cursor.image.width * options.systemScale,
|
||||
),
|
||||
);
|
||||
final cached = cache[cursor];
|
||||
if (cached != null) {
|
||||
frames.add(await cached);
|
||||
continue;
|
||||
}
|
||||
final scaled = _scaleFrame(cursor, target, options.systemScale);
|
||||
cache[cursor] = scaled;
|
||||
frames.add(await scaled);
|
||||
}
|
||||
return AnimatedCursor(frames: frames, frameDelays: animated.frameDelays);
|
||||
}
|
||||
|
||||
static Future<Cursor> _scaleFrame(
|
||||
Cursor cursor,
|
||||
double? target,
|
||||
double systemScale,
|
||||
) async {
|
||||
if (target != null) {
|
||||
return CursorScaler.scaleTo(
|
||||
cursor.image,
|
||||
cursor.hotspotX,
|
||||
cursor.hotspotY,
|
||||
target,
|
||||
);
|
||||
}
|
||||
return CursorScaler.scaleTo(
|
||||
cursor.image,
|
||||
cursor.hotspotX,
|
||||
cursor.hotspotY,
|
||||
cursor.image.width * systemScale,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses consecutive `icon` subchunks starting at [start] (inclusive) up to
|
||||
/// [end] (exclusive) within the `LIST fram` body.
|
||||
static Future<List<Cursor>> _parseFrames(
|
||||
|
||||
@@ -71,6 +71,23 @@ class CurDecoder {
|
||||
throw const FormatException("cursor bitmap data truncated");
|
||||
}
|
||||
|
||||
final data = Uint8List.sublistView(bytes, offset, offset + size);
|
||||
// Modern cursors may embed a full PNG (or other compressed image) instead
|
||||
// of a raw DIB bitmap. Detect it and hand it to the image codec directly.
|
||||
if (data.length >= 8 &&
|
||||
data[0] == 0x89 && // PNG magic
|
||||
data[1] == 0x50 && // 'P'
|
||||
data[2] == 0x4e && // 'N'
|
||||
data[3] == 0x47) {
|
||||
// 'G'
|
||||
final image = await _decodeCompressed(data);
|
||||
return Cursor(
|
||||
image: image,
|
||||
hotspotX: hotspotX.clamp(0, image.width - 1),
|
||||
hotspotY: hotspotY.clamp(0, image.height - 1),
|
||||
);
|
||||
}
|
||||
|
||||
final dib = ByteData.sublistView(bytes, offset, offset + size);
|
||||
if (dib.lengthInBytes < 40) {
|
||||
throw const FormatException("cursor DIB data too small");
|
||||
@@ -208,4 +225,18 @@ class CurDecoder {
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static Future<ui.Image> _decodeCompressed(Uint8List bytes) async {
|
||||
try {
|
||||
final codec = await ui.instantiateImageCodec(bytes);
|
||||
try {
|
||||
final frame = await codec.getNextFrame();
|
||||
return frame.image;
|
||||
} finally {
|
||||
codec.dispose();
|
||||
}
|
||||
} on Exception {
|
||||
throw const FormatException("unsupported compressed cursor data");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user