commit b1cd2f0539be77857c75cc3fe7a7c995bd1e4054 Author: Quantum Date: Sat Sep 26 18:14:55 2020 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36f9861 --- /dev/null +++ b/.gitignore @@ -0,0 +1,145 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea + +# For testing +input/ +output/ diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..976ba02 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2b36eb4 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup + +setup( + name='win2xcur', + version='0.3.1', + packages=['win2xcur'], + install_requires=['Wand'], + + entry_points={ + 'console_scripts': [ + 'win2xcur = win2xcur.main:main', + ], + }, + + author='quantum', + author_email='quantum2048@gmail.com', +) diff --git a/win2xcur/__init__.py b/win2xcur/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/win2xcur/cursor.py b/win2xcur/cursor.py new file mode 100644 index 0000000..49e9087 --- /dev/null +++ b/win2xcur/cursor.py @@ -0,0 +1,30 @@ +from typing import Iterator, List, Tuple + +from wand.sequence import SingleImage + + +class CursorImage: + def __init__(self, image: SingleImage, hotspot: Tuple[int, int]) -> None: + self.image = image + self.hotspot = hotspot + + def __repr__(self) -> str: + return 'CursorImage(image=%r, hotspot=%r)' % (self.image, self.hotspot) + + +class CursorFrame: + def __init__(self, images: List[CursorImage], delay=0) -> None: + self.images = images + self.delay = delay + + def __getitem__(self, item) -> CursorImage: + return self.images[item] + + def __len__(self) -> int: + return len(self.images) + + def __iter__(self) -> Iterator[CursorImage]: + return iter(self.images) + + def __repr__(self) -> str: + return 'CursorFrame(images=%r, delay=%r)' % (self.images, self.delay) diff --git a/win2xcur/main.py b/win2xcur/main.py new file mode 100644 index 0000000..23174e1 --- /dev/null +++ b/win2xcur/main.py @@ -0,0 +1,37 @@ +import argparse +import os +import sys +import traceback + +from win2xcur.parser import open_blob +from win2xcur.writer.x11 import check_xcursorgen, to_x11 + + +def main() -> None: + parser = argparse.ArgumentParser(description='Converts Windows cursors to X11 cursors.') + parser.add_argument('files', type=argparse.FileType('rb'), nargs='+', + help='Windows cursor files to convert (*.cur, *.ani)') + parser.add_argument('-o', '--output', '--output-dir', default=os.curdir, + help='Directory to store converted cursor files.') + + args = parser.parse_args() + + check_xcursorgen() + + for file in args.files: + name = file.name + blob = file.read() + try: + cursor = open_blob(blob) + except Exception: + print('Error occurred while processing %s:' % (name,), file=sys.stderr) + traceback.print_exc() + else: + result = to_x11(cursor.frames) + output = os.path.join(args.output, os.path.splitext(os.path.basename(name))[0]) + with open(output, 'wb') as f: + f.write(result) + + +if __name__ == '__main__': + main() diff --git a/win2xcur/parser/__init__.py b/win2xcur/parser/__init__.py new file mode 100644 index 0000000..7071064 --- /dev/null +++ b/win2xcur/parser/__init__.py @@ -0,0 +1,11 @@ +from win2xcur.parser.ani import ANIParser +from win2xcur.parser.cur import CURParser + +PARSERS = [CURParser, ANIParser] + + +def open_blob(blob): + for parser in PARSERS: + if parser.can_parse(blob): + return parser(blob) + raise ValueError('Unsupported file format') diff --git a/win2xcur/parser/ani.py b/win2xcur/parser/ani.py new file mode 100644 index 0000000..2abf698 --- /dev/null +++ b/win2xcur/parser/ani.py @@ -0,0 +1,86 @@ +import struct +from copy import copy + +from win2xcur.parser.cur import CURParser + + +class ANIParser: + SIGNATURE = b'RIFF' + ANI_TYPE = b'ACON' + FRAME_TYPE = b'fram' + RIFF_HEADER = struct.Struct('<4sI4s') + CHUNK_HEADER = struct.Struct('<4sI') + ANIH_HEADER = struct.Struct(' None: + global xcursorgen_checked + if xcursorgen_checked: + return + + try: + subprocess.check_call(['xcursorgen', '--version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + raise RuntimeError('xcursorgen must be installed to create X11 cursors!') + else: + xcursorgen_checked = True + + +def to_x11(frames: List[CursorFrame]) -> bytes: + check_xcursorgen() + + counter = 0 + configs = [] + with TemporaryDirectory() as png_dir: + for frame in frames: + for cursor in frame: + name = '%d.png' % (counter,) + hx, hy = cursor.hotspot + configs.append('%d %d %d %s %d' % (cursor.image.width, hx, hy, name, int(frame.delay * 1000))) + + image = Image(image=cursor.image) + image.save(filename=os.path.join(png_dir, name)) + counter += 1 + + process = subprocess.Popen(['xcursorgen', '-p', png_dir], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + result, error = process.communicate('\n'.join(configs).encode(sys.getfilesystemencoding())) + + if error: + raise RuntimeError('xcursorgen failed: %r' % error) + + return result