Compare commits

...

12 commits

Author SHA1 Message Date
Quantum 8e71037f5f Add ability to scale cursor images 2024-01-13 07:23:30 -05:00
Quantum 0898f35183 Fix GitHub Actions badge in README.md 2023-08-02 23:42:43 -04:00
Quantum 54ecf8bf90 Release v0.1.2 2023-05-22 22:39:47 -04:00
Quantum 7863775921 Negate opacity channel on ImageMagick 7
In ImageMagick 7, the semantics of the opacity channel is flipped.
2022-04-24 02:13:45 -04:00
Quantum ffb9c5e24a Support copy_opacity rename in ImageMagick 7 2022-04-09 04:06:26 -04:00
Quantum b5308cbb1e Release version 0.1.1 2022-03-28 00:50:04 -04:00
Quantum fcf9681371 Convert Xcursor delays to seconds 2022-03-28 00:48:30 -04:00
Quantum ee3249c705 Fix spelling in README 2022-03-24 01:37:23 -04:00
Quantum 99ff1bb1ba Deal with Python 3.7 not having the right types 2022-03-17 01:50:30 -04:00
Quantum a89e52a286 Fix mypy typing 2022-03-17 01:46:41 -04:00
ful1e5 5c03c08a61 fixed typo inside win2xcur '--shadow-y' option 2022-03-17 01:29:36 -04:00
Quantum b979ed5fc5 Document wand-related issues 2021-09-08 06:38:36 -04:00
10 changed files with 58 additions and 13 deletions

View file

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [ 3.6, 3.7, 3.8, 3.9 ] python-version: [ 3.7, 3.8, 3.9, '3.10' ]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@ -32,6 +32,7 @@ jobs:
- name: Lint with flake8 - name: Lint with flake8
run: flake8 . run: flake8 .
- name: Typecheck with mypy - name: Typecheck with mypy
if: matrix.python-version != 3.7
run: mypy . run: mypy .
- name: Test packages - name: Test packages
run: python setup.py sdist bdist_wheel run: python setup.py sdist bdist_wheel

View file

@ -1,4 +1,4 @@
# `win2xcur` and `x2wincur` [![Build Status](https://img.shields.io/github/workflow/status/quantum5/win2xcur/Python%20package)](https://github.com/quantum5/win2xcur/actions) [![PyPI](https://img.shields.io/pypi/v/win2xcur.svg)](https://pypi.org/project/win2xcur/) [![PyPI - Format](https://img.shields.io/pypi/format/win2xcur.svg)](https://pypi.org/project/win2xcur/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/win2xcur.svg)](https://pypi.org/project/win2xcur/) # `win2xcur` and `x2wincur` [![Build Status](https://img.shields.io/github/actions/workflow/status/quantum5/win2xcur/build.yml)](https://github.com/quantum5/win2xcur/actions) [![PyPI](https://img.shields.io/pypi/v/win2xcur.svg)](https://pypi.org/project/win2xcur/) [![PyPI - Format](https://img.shields.io/pypi/format/win2xcur.svg)](https://pypi.org/project/win2xcur/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/win2xcur.svg)](https://pypi.org/project/win2xcur/)
`win2xcur` is a tool that converts cursors from Windows format (`*.cur`, `win2xcur` is a tool that converts cursors from Windows format (`*.cur`,
`*.ani`) to Xcursor format. This allows Windows cursor themes to be used on `*.ani`) to Xcursor format. This allows Windows cursor themes to be used on
@ -44,3 +44,11 @@ For example, if you want to convert DMZ-White to Windows:
mkdir dmz-white/ mkdir dmz-white/
x2wincur /usr/share/icons/DMZ-White/cursors/* -o dmz-white/ x2wincur /usr/share/icons/DMZ-White/cursors/* -o dmz-white/
## Troubleshooting
`win2xcur` and `x2wincur` should work out of the box on most systems. If you
are using unconventional distros (e.g. Alpine) and are getting errors related
to `wand`, please see the [Wand documentation on installation][wand-install].
[wand-install]: https://docs.wand-py.org/en/0.6.7/guide/install.html

View file

@ -1,3 +1,4 @@
[mypy] [mypy]
ignore_missing_imports = True ignore_missing_imports = True
strict = true strict = true
plugins = numpy.typing.mypy_plugin

View file

@ -7,7 +7,7 @@ with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f:
setup( setup(
name='win2xcur', name='win2xcur',
version='0.1.0', version='0.1.2',
packages=find_packages(), packages=find_packages(),
install_requires=['numpy', 'Wand'], install_requires=['numpy', 'Wand'],

View file

@ -7,7 +7,7 @@ from multiprocessing.pool import ThreadPool
from threading import Lock from threading import Lock
from typing import BinaryIO from typing import BinaryIO
from win2xcur import shadow from win2xcur import scale, shadow
from win2xcur.parser import open_blob from win2xcur.parser import open_blob
from win2xcur.writer import to_x11 from win2xcur.writer import to_x11
@ -29,9 +29,11 @@ def main() -> None:
parser.add_argument('-x', '--shadow-x', type=float, default=0.05, parser.add_argument('-x', '--shadow-x', type=float, default=0.05,
help='x-offset of shadow (as fraction of width)') help='x-offset of shadow (as fraction of width)')
parser.add_argument('-y', '--shadow-y', type=float, default=0.05, parser.add_argument('-y', '--shadow-y', type=float, default=0.05,
help='x-offset of shadow (as fraction of height)') help='y-offset of shadow (as fraction of height)')
parser.add_argument('-c', '--shadow-color', default='#000000', parser.add_argument('-c', '--shadow-color', default='#000000',
help='color of the shadow') help='color of the shadow')
parser.add_argument('--scale', default=None, type=float,
help='Scale the cursor by the specified factor.')
args = parser.parse_args() args = parser.parse_args()
print_lock = Lock() print_lock = Lock()
@ -46,6 +48,8 @@ def main() -> None:
print(f'Error occurred while processing {name}:', file=sys.stderr) print(f'Error occurred while processing {name}:', file=sys.stderr)
traceback.print_exc() traceback.print_exc()
else: else:
if args.scale:
scale.apply_to_frames(cursor.frames, scale=args.scale)
if args.shadow: if args.shadow:
shadow.apply_to_frames(cursor.frames, color=args.shadow_color, radius=args.shadow_radius, shadow.apply_to_frames(cursor.frames, color=args.shadow_color, radius=args.shadow_radius,
sigma=args.shadow_sigma, xoffset=args.shadow_x, yoffset=args.shadow_y) sigma=args.shadow_sigma, xoffset=args.shadow_x, yoffset=args.shadow_y)

View file

@ -7,6 +7,7 @@ from multiprocessing.pool import ThreadPool
from threading import Lock from threading import Lock
from typing import BinaryIO from typing import BinaryIO
from win2xcur import scale
from win2xcur.parser import open_blob from win2xcur.parser import open_blob
from win2xcur.writer import to_smart from win2xcur.writer import to_smart
@ -17,6 +18,8 @@ def main() -> None:
help='X11 cursor files to convert (no extension)') help='X11 cursor files to convert (no extension)')
parser.add_argument('-o', '--output', '--output-dir', default=os.curdir, parser.add_argument('-o', '--output', '--output-dir', default=os.curdir,
help='Directory to store converted cursor files.') help='Directory to store converted cursor files.')
parser.add_argument('-S', '--scale', default=None, type=float,
help='Scale the cursor by the specified factor.')
args = parser.parse_args() args = parser.parse_args()
print_lock = Lock() print_lock = Lock()
@ -31,6 +34,8 @@ def main() -> None:
print(f'Error occurred while processing {name}:', file=sys.stderr) print(f'Error occurred while processing {name}:', file=sys.stderr)
traceback.print_exc() traceback.print_exc()
else: else:
if args.scale:
scale.apply_to_frames(cursor.frames, scale=args.scale)
ext, result = to_smart(cursor.frames) ext, result = to_smart(cursor.frames)
output = os.path.join(args.output, os.path.basename(name) + ext) output = os.path.join(args.output, os.path.basename(name) + ext)
with open(output, 'wb') as f: with open(output, 'wb') as f:

View file

@ -49,6 +49,7 @@ class XCursorParser(BaseParser):
size, actual_type, nominal_size, version, width, height, x_offset, y_offset, delay = \ size, actual_type, nominal_size, version, width, height, x_offset, y_offset, delay = \
self._unpack(self.IMAGE_HEADER, position) self._unpack(self.IMAGE_HEADER, position)
delay /= 1000
if size != self.IMAGE_HEADER.size: if size != self.IMAGE_HEADER.size:
raise ValueError(f'Unexpected size: {size}, expected {self.IMAGE_HEADER.size}') raise ValueError(f'Unexpected size: {size}, expected {self.IMAGE_HEADER.size}')

12
win2xcur/scale.py Normal file
View file

@ -0,0 +1,12 @@
from typing import List
from win2xcur.cursor import CursorFrame
def apply_to_frames(frames: List[CursorFrame], *, scale: float) -> None:
for frame in frames:
for cursor in frame:
cursor.image.scale(
int(round(cursor.image.width * scale)),
int(round(cursor.image.height) * scale),
)

View file

@ -1,10 +1,17 @@
from typing import List from typing import List
from wand.color import Color from wand.color import Color
from wand.image import BaseImage, Image from wand.image import BaseImage, COMPOSITE_OPERATORS, Image
from win2xcur.cursor import CursorFrame from win2xcur.cursor import CursorFrame
if 'copy_opacity' in COMPOSITE_OPERATORS:
COPY_ALPHA = 'copy_opacity' # ImageMagick 6 name
NEEDS_NEGATE = False
else:
COPY_ALPHA = 'copy_alpha' # ImageMagick 7 name
NEEDS_NEGATE = True
def apply_to_image(image: BaseImage, *, color: str, radius: float, sigma: float, xoffset: float, def apply_to_image(image: BaseImage, *, color: str, radius: float, sigma: float, xoffset: float,
yoffset: float) -> Image: yoffset: float) -> Image:
@ -13,18 +20,24 @@ def apply_to_image(image: BaseImage, *, color: str, radius: float, sigma: float,
new_width = image.width + 3 * xoffset new_width = image.width + 3 * xoffset
new_height = image.height + 3 * yoffset new_height = image.height + 3 * yoffset
if NEEDS_NEGATE:
channel = image.channel_images['opacity'].clone()
channel.negate()
else:
channel = image.channel_images['opacity']
opacity = Image(width=new_width, height=new_height, pseudo='xc:white') opacity = Image(width=new_width, height=new_height, pseudo='xc:white')
opacity.composite(image.channel_images['opacity'], left=xoffset, top=yoffset) opacity.composite(channel, left=xoffset, top=yoffset)
opacity.gaussian_blur(radius * image.width, sigma * image.width) opacity.gaussian_blur(radius * image.width, sigma * image.width)
opacity.negate() opacity.negate()
opacity.modulate(50) opacity.modulate(50)
shadow = Image(width=new_width, height=new_height, pseudo='xc:' + color) shadow = Image(width=new_width, height=new_height, pseudo='xc:' + color)
shadow.composite(opacity, operator='copy_opacity') shadow.composite(opacity, operator=COPY_ALPHA)
result = Image(width=new_width, height=new_height, pseudo='xc:transparent') result = Image(width=new_width, height=new_height, pseudo='xc:transparent')
result.composite(shadow)
result.composite(image) result.composite(image)
result.composite(shadow, operator='difference')
trimmed = result.clone() trimmed = result.clone()
trimmed.trim(color=Color('transparent')) trimmed.trim(color=Color('transparent'))

View file

@ -1,12 +1,12 @@
from typing import cast from typing import Any
import numpy import numpy as np
def premultiply_alpha(source: bytes) -> bytes: def premultiply_alpha(source: bytes) -> bytes:
buffer = numpy.frombuffer(source, dtype=numpy.uint8).astype(numpy.double) buffer: np.ndarray[Any, np.dtype[np.double]] = np.frombuffer(source, dtype=np.uint8).astype(np.double)
alpha = buffer[3::4] / 255.0 alpha = buffer[3::4] / 255.0
buffer[0::4] *= alpha buffer[0::4] *= alpha
buffer[1::4] *= alpha buffer[1::4] *= alpha
buffer[2::4] *= alpha buffer[2::4] *= alpha
return cast(bytes, buffer.astype(numpy.uint8).tobytes()) return buffer.astype(np.uint8).tobytes()