win2xcur/win2xcur/parser/ani.py

117 lines
4.6 KiB
Python
Raw Normal View History

2020-09-26 18:14:55 -04:00
import struct
from copy import copy
2020-09-27 00:53:23 -04:00
from typing import Any, Iterable, List, Tuple
2020-09-26 18:14:55 -04:00
2020-09-27 00:53:23 -04:00
from win2xcur.cursor import CursorFrame
from win2xcur.parser.base import BaseParser
2020-09-26 18:14:55 -04:00
from win2xcur.parser.cur import CURParser
2020-09-27 00:53:23 -04:00
class ANIParser(BaseParser):
2020-09-26 18:14:55 -04:00
SIGNATURE = b'RIFF'
ANI_TYPE = b'ACON'
HEADER_CHUNK = b'anih'
LIST_CHUNK = b'LIST'
SEQ_CHUNK = b'seq '
RATE_CHUNK = b'rate'
2020-09-26 18:14:55 -04:00
FRAME_TYPE = b'fram'
ICON_CHUNK = b'icon'
2020-09-26 18:14:55 -04:00
RIFF_HEADER = struct.Struct('<4sI4s')
CHUNK_HEADER = struct.Struct('<4sI')
ANIH_HEADER = struct.Struct('<IIIIIIIII')
UNSIGNED = struct.Struct('<I')
SEQUENCE_FLAG = 0x2
ICON_FLAG = 0x1
@classmethod
2020-09-27 00:53:23 -04:00
def can_parse(cls, blob: bytes) -> bool:
2020-09-27 13:56:54 -04:00
signature: bytes
size: int
subtype: bytes
try:
signature, size, subtype = cls.RIFF_HEADER.unpack(blob[:cls.RIFF_HEADER.size])
except struct.error:
return False
2020-09-26 18:14:55 -04:00
return signature == cls.SIGNATURE and size == len(blob) - 8 and subtype == cls.ANI_TYPE
2020-09-27 00:53:23 -04:00
def __init__(self, blob: bytes) -> None:
super().__init__(blob)
2020-09-26 18:14:55 -04:00
if not self.can_parse(blob):
raise ValueError('Not a .ani file')
self.frames = self._parse(self.RIFF_HEADER.size)
2020-09-27 00:53:23 -04:00
def _unpack(self, struct_cls: struct.Struct, offset: int) -> Tuple[Any, ...]:
2020-09-26 18:14:55 -04:00
return struct_cls.unpack(self.blob[offset:offset + struct_cls.size])
2020-09-27 14:40:16 -04:00
def _read_chunk(self, offset: int, expected: Iterable[bytes]) -> Tuple[bytes, int, int]:
found = []
while True:
name, size = self._unpack(self.CHUNK_HEADER, offset)
offset += self.CHUNK_HEADER.size
if name in expected:
break
found += [name]
offset += size
if offset >= len(self.blob):
raise ValueError('Expected chunk %r, found %r' % (expected, found))
return name, size, offset
2020-09-26 18:14:55 -04:00
2020-09-27 00:53:23 -04:00
def _parse(self, offset: int) -> List[CursorFrame]:
_, size, offset = self._read_chunk(offset, expected=[self.HEADER_CHUNK])
2020-09-26 18:14:55 -04:00
if size != self.ANIH_HEADER.size:
raise ValueError('Unexpected anih header size %d, expected %d' % (size, self.ANIH_HEADER.size))
size, frame_count, step_count, width, height, bit_count, planes, display_rate, flags = self.ANIH_HEADER.unpack(
self.blob[offset:offset + self.ANIH_HEADER.size])
if size != self.ANIH_HEADER.size:
raise ValueError('Unexpected size in anih header %d, expected %d' % (size, self.ANIH_HEADER.size))
2020-09-26 18:14:55 -04:00
if not flags & self.ICON_FLAG:
raise NotImplementedError('Raw BMP images not supported.')
offset += self.ANIH_HEADER.size
frames = []
2020-09-27 14:40:16 -04:00
order = list(range(frame_count))
2020-09-26 18:14:55 -04:00
delays = [display_rate for _ in range(step_count)]
2020-09-27 14:40:16 -04:00
while offset < len(self.blob):
name, size, offset = self._read_chunk(offset, expected=[self.LIST_CHUNK, self.SEQ_CHUNK, self.RATE_CHUNK])
if name == self.LIST_CHUNK:
2020-09-27 14:40:16 -04:00
list_end = offset + size
if self.blob[offset:offset + 4] != self.FRAME_TYPE:
raise ValueError('Unexpected RIFF list type: %r, expected %r' %
(self.blob[offset:offset + 4], self.FRAME_TYPE))
offset += 4
for i in range(frame_count):
_, size, offset = self._read_chunk(offset, expected=[self.ICON_CHUNK])
2020-09-27 14:40:16 -04:00
frames.append(CURParser(self.blob[offset:offset + size]).frames[0])
offset += size
if offset & 1:
offset += 1
2020-09-27 14:40:16 -04:00
if offset != list_end:
raise ValueError('Wrong RIFF list size: %r, expected %r' % (offset, list_end))
elif name == self.SEQ_CHUNK:
2020-09-27 14:40:16 -04:00
order = [i for i, in self.UNSIGNED.iter_unpack(self.blob[offset:offset + size])]
if len(order) != step_count:
raise ValueError('Wrong animation sequence size: %r, expected %r' % (len(order), step_count))
offset += size
elif name == self.RATE_CHUNK:
2020-09-27 14:40:16 -04:00
delays = [i for i, in self.UNSIGNED.iter_unpack(self.blob[offset:offset + size])]
if len(delays) != step_count:
raise ValueError('Wrong animation rate size: %r, expected %r' % (len(delays), step_count))
offset += size
if len(order) != step_count:
raise ValueError('Required chunk "seq " not found.')
sequence = [copy(frames[i]) for i in order]
2020-09-26 18:14:55 -04:00
for frame, delay in zip(sequence, delays):
frame.delay = delay / 60
2020-09-26 20:42:03 -04:00
return sequence