From 05bc9d0e24fcf4c8d2d905aeba595f606601a60f Mon Sep 17 00:00:00 2001 From: Quantum Date: Sat, 3 Oct 2020 01:24:47 -0400 Subject: [PATCH] Add .ani writer --- win2xcur/parser/ani.py | 20 ++++++++++++++------ win2xcur/writer/windows.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/win2xcur/parser/ani.py b/win2xcur/parser/ani.py index 43c4503..812e1c0 100644 --- a/win2xcur/parser/ani.py +++ b/win2xcur/parser/ani.py @@ -10,7 +10,12 @@ from win2xcur.parser.cur import CURParser class ANIParser(BaseParser): SIGNATURE = b'RIFF' ANI_TYPE = b'ACON' + HEADER_CHUNK = b'anih' + LIST_CHUNK = b'LIST' + SEQ_CHUNK = b'seq ' + RATE_CHUNK = b'rate' FRAME_TYPE = b'fram' + ICON_CHUNK = b'icon' RIFF_HEADER = struct.Struct('<4sI4s') CHUNK_HEADER = struct.Struct('<4sI') ANIH_HEADER = struct.Struct(' List[CursorFrame]: - _, size, offset = self._read_chunk(offset, expected=[b'anih']) + _, size, offset = self._read_chunk(offset, expected=[self.HEADER_CHUNK]) if size != self.ANIH_HEADER.size: raise ValueError('Unexpected anih header size %d, expected %d' % (size, self.ANIH_HEADER.size)) @@ -60,6 +65,9 @@ class ANIParser(BaseParser): 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)) + if not flags & self.ICON_FLAG: raise NotImplementedError('Raw BMP images not supported.') @@ -70,8 +78,8 @@ class ANIParser(BaseParser): delays = [display_rate for _ in range(step_count)] while offset < len(self.blob): - name, size, offset = self._read_chunk(offset, expected=[b'LIST', b'seq ', b'rate']) - if name == b'LIST': + name, size, offset = self._read_chunk(offset, expected=[self.LIST_CHUNK, self.SEQ_CHUNK, self.RATE_CHUNK]) + if name == self.LIST_CHUNK: list_end = offset + size if self.blob[offset:offset + 4] != self.FRAME_TYPE: raise ValueError('Unexpected RIFF list type: %r, expected %r' % @@ -79,18 +87,18 @@ class ANIParser(BaseParser): offset += 4 for i in range(frame_count): - _, size, offset = self._read_chunk(offset, expected=[b'icon']) + _, size, offset = self._read_chunk(offset, expected=[self.ICON_CHUNK]) frames.append(CURParser(self.blob[offset:offset + size]).frames[0]) offset += size if offset != list_end: raise ValueError('Wrong RIFF list size: %r, expected %r' % (offset, list_end)) - elif name == b'seq ': + elif name == self.SEQ_CHUNK: 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 == b'rate': + elif name == self.RATE_CHUNK: 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)) diff --git a/win2xcur/writer/windows.py b/win2xcur/writer/windows.py index 25da088..b0a099c 100644 --- a/win2xcur/writer/windows.py +++ b/win2xcur/writer/windows.py @@ -1,8 +1,9 @@ +from io import BytesIO from itertools import chain from typing import List from win2xcur.cursor import CursorFrame -from win2xcur.parser import CURParser +from win2xcur.parser import ANIParser, CURParser def to_cur(frame: CursorFrame) -> bytes: @@ -24,3 +25,38 @@ def to_cur(frame: CursorFrame) -> bytes: offset += len(blob) return b''.join(chain([header], directory, image_data)) + + +def get_ani_cur_list(frames: List[CursorFrame]) -> bytes: + io = BytesIO() + for frame in frames: + cur_file = to_cur(frame) + io.write(ANIParser.CHUNK_HEADER.pack(ANIParser.ICON_CHUNK, len(cur_file))) + io.write(cur_file) + if len(cur_file) & 1: + io.write(b'\0') + return io.getvalue() + + +def get_ani_rate_chunk(frames: List[CursorFrame]) -> bytes: + io = BytesIO() + io.write(ANIParser.CHUNK_HEADER.pack(ANIParser.RATE_CHUNK, ANIParser.UNSIGNED.size * len(frames))) + for frame in frames: + io.write(ANIParser.UNSIGNED.pack(int(round(frame.delay * 60)))) + return io.getvalue() + + +def to_ani(frames: List[CursorFrame]) -> bytes: + ani_header = ANIParser.ANIH_HEADER.pack( + ANIParser.ANIH_HEADER.size, len(frames), len(frames), 0, 0, 32, 1, 1, ANIParser.ICON_FLAG + ) + + cur_list = get_ani_cur_list(frames) + chunks = [ + ANIParser.CHUNK_HEADER.pack(ANIParser.HEADER_CHUNK, len(ani_header)), + ani_header, + ANIParser.RIFF_HEADER.pack(ANIParser.LIST_CHUNK, len(cur_list) + 4, ANIParser.FRAME_TYPE), + cur_list, + ] + body = b''.join(chunks) + return ANIParser.RIFF_HEADER.pack(ANIParser.SIGNATURE, len(body) + 4, ANIParser.ANI_TYPE) + body