Add Xcursor loading support

This commit is contained in:
Quantum 2020-10-02 23:37:34 -04:00
parent ec9dda6128
commit e7ec91ee18
4 changed files with 124 additions and 2 deletions

View file

@ -3,8 +3,9 @@ from typing import List, Type
from win2xcur.parser.ani import ANIParser from win2xcur.parser.ani import ANIParser
from win2xcur.parser.base import BaseParser from win2xcur.parser.base import BaseParser
from win2xcur.parser.cur import CURParser from win2xcur.parser.cur import CURParser
from win2xcur.parser.xcursor import XCursorParser
PARSERS: List[Type[BaseParser]] = [CURParser, ANIParser] PARSERS: List[Type[BaseParser]] = [CURParser, ANIParser, XCursorParser]
def open_blob(blob: bytes) -> BaseParser: def open_blob(blob: bytes) -> BaseParser:

View file

@ -23,7 +23,10 @@ class ANIParser(BaseParser):
signature: bytes signature: bytes
size: int size: int
subtype: bytes subtype: bytes
signature, size, subtype = cls.RIFF_HEADER.unpack(blob[:cls.RIFF_HEADER.size]) try:
signature, size, subtype = cls.RIFF_HEADER.unpack(blob[:cls.RIFF_HEADER.size])
except struct.error:
return False
return signature == cls.SIGNATURE and size == len(blob) - 8 and subtype == cls.ANI_TYPE return signature == cls.SIGNATURE and size == len(blob) - 8 and subtype == cls.ANI_TYPE
def __init__(self, blob: bytes) -> None: def __init__(self, blob: bytes) -> None:

View file

@ -0,0 +1,98 @@
import struct
from collections import defaultdict
from typing import Any, Dict, Iterator, List, Tuple, cast
from win2xcur.cursor import CursorFrame, CursorImage
from win2xcur.parser.base import BaseParser
from win2xcur.utils import image_from_pixels
class XCursorParser(BaseParser):
MAGIC = b'Xcur'
VERSION = 0x1_0000
FILE_HEADER = struct.Struct('<4sIII')
TOC_CHUNK = struct.Struct('<III')
CHUNK_IMAGE = 0xFFFD0002
IMAGE_HEADER = struct.Struct('<IIIIIIIII')
@classmethod
def can_parse(cls, blob: bytes) -> bool:
return blob[:len(cls.MAGIC)] == cls.MAGIC
def __init__(self, blob: bytes) -> None:
super().__init__(blob)
self.frames = self._parse()
def _unpack(self, struct_cls: struct.Struct, offset: int) -> Tuple[Any, ...]:
return struct_cls.unpack(self.blob[offset:offset + struct_cls.size])
def _parse(self) -> List[CursorFrame]:
magic, header_size, version, toc_size = self._unpack(self.FILE_HEADER, 0)
assert magic == self.MAGIC
if version != self.VERSION:
raise ValueError('Unsupported Xcursor version %r' % version)
offset = self.FILE_HEADER.size
chunks: List[Tuple[int, int, int]] = []
for i in range(toc_size):
chunk_type, chunk_subtype, position = self._unpack(self.TOC_CHUNK, offset)
chunks.append((chunk_type, chunk_subtype, position))
offset += self.TOC_CHUNK.size
images_by_size: Dict[int, List[Tuple[CursorImage, int]]] = defaultdict(list)
for chunk_type, chunk_subtype, position in chunks:
if chunk_type != self.CHUNK_IMAGE:
continue
size, actual_type, nominal_size, version, width, height, x_offset, y_offset, delay = \
self._unpack(self.IMAGE_HEADER, position)
if size != self.IMAGE_HEADER.size:
raise ValueError('Unexpected size: %r, expected %r' % (size, self.IMAGE_HEADER.size))
if actual_type != chunk_type:
raise ValueError('Unexpected chunk type: %r, expected %r' % (actual_type, chunk_type))
if nominal_size != chunk_subtype:
raise ValueError('Unexpected nominal size: %r, expected %r' % (nominal_size, chunk_subtype))
if width > 0x7FFF:
raise ValueError('Image width too large: %r' % width)
if height > 0x7FFF:
raise ValueError('Image height too large: %r' % height)
if x_offset > width:
raise ValueError('Hotspot x-coordinate too large: %r' % x_offset)
if y_offset > height:
raise ValueError('Hotspot x-coordinate too large: %r' % y_offset)
image_start = position + self.IMAGE_HEADER.size
image_size = width * height * 4
blob = self.blob[image_start:image_start + image_size]
if len(blob) != image_size:
raise ValueError('Invalid image at %d: expected %d bytes, got %d bytes' %
(image_size, image_size, len(blob)))
images_by_size[nominal_size].append(
(CursorImage(image_from_pixels(blob, width, height, 'ARGB', 'char'), (x_offset, y_offset)), delay)
)
if len(set(map(len, images_by_size.values()))) != 1:
raise ValueError('win2xcur does not support animations where each size has different number of frames')
result = []
for sequence in cast(Any, zip(*images_by_size.values())):
images: Tuple[CursorImage, ...]
delays: Tuple[int, ...]
images, delays = cast(Any, zip(*sequence))
if len(set(delays)) != 1:
raise ValueError('win2xcur does not support animations where each size has a different frame delay')
result.append(CursorFrame(list(images), delays[0]))
return result

20
win2xcur/utils.py Normal file
View file

@ -0,0 +1,20 @@
import ctypes
from wand.api import library
from wand.image import Image
from wand.sequence import SingleImage
MagickImportImagePixels = library['MagickImportImagePixels']
MagickImportImagePixels.argtypes = (
ctypes.c_void_p, ctypes.c_ssize_t, ctypes.c_ssize_t, ctypes.c_size_t,
ctypes.c_size_t, ctypes.c_char_p, ctypes.c_int, ctypes.c_void_p
)
StorageType = ('undefined', 'char', 'double', 'float',
'integer', 'long', 'quantum', 'short')
def image_from_pixels(blob: bytes, width: int, height: int, pixel_format: str, pixel_size: str) -> SingleImage:
image = Image(width=width, height=height)
MagickImportImagePixels(image.wand, 0, 0, width, height, pixel_format.encode('ascii'),
StorageType.index(pixel_size), blob)
return image.sequence[0]