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
strategy:
matrix:
python-version: [ 3.6, 3.7, 3.8, 3.9 ]
python-version: [ 3.7, 3.8, 3.9, '3.10' ]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
@ -32,6 +32,7 @@ jobs:
- name: Lint with flake8
run: flake8 .
- name: Typecheck with mypy
if: matrix.python-version != 3.7
run: mypy .
- name: Test packages
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`,
`*.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/
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]
ignore_missing_imports = 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(
name='win2xcur',
version='0.1.0',
version='0.1.2',
packages=find_packages(),
install_requires=['numpy', 'Wand'],

View file

@ -7,7 +7,7 @@ from multiprocessing.pool import ThreadPool
from threading import Lock
from typing import BinaryIO
from win2xcur import shadow
from win2xcur import scale, shadow
from win2xcur.parser import open_blob
from win2xcur.writer import to_x11
@ -29,9 +29,11 @@ def main() -> None:
parser.add_argument('-x', '--shadow-x', type=float, default=0.05,
help='x-offset of shadow (as fraction of width)')
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',
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()
print_lock = Lock()
@ -46,6 +48,8 @@ def main() -> None:
print(f'Error occurred while processing {name}:', file=sys.stderr)
traceback.print_exc()
else:
if args.scale:
scale.apply_to_frames(cursor.frames, scale=args.scale)
if args.shadow:
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)

View file

@ -7,6 +7,7 @@ from multiprocessing.pool import ThreadPool
from threading import Lock
from typing import BinaryIO
from win2xcur import scale
from win2xcur.parser import open_blob
from win2xcur.writer import to_smart
@ -17,6 +18,8 @@ def main() -> None:
help='X11 cursor files to convert (no extension)')
parser.add_argument('-o', '--output', '--output-dir', default=os.curdir,
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()
print_lock = Lock()
@ -31,6 +34,8 @@ def main() -> None:
print(f'Error occurred while processing {name}:', file=sys.stderr)
traceback.print_exc()
else:
if args.scale:
scale.apply_to_frames(cursor.frames, scale=args.scale)
ext, result = to_smart(cursor.frames)
output = os.path.join(args.output, os.path.basename(name) + ext)
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 = \
self._unpack(self.IMAGE_HEADER, position)
delay /= 1000
if size != 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 wand.color import Color
from wand.image import BaseImage, Image
from wand.image import BaseImage, COMPOSITE_OPERATORS, Image
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,
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_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.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.negate()
opacity.modulate(50)
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.composite(shadow)
result.composite(image)
result.composite(shadow, operator='difference')
trimmed = result.clone()
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:
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
buffer[0::4] *= alpha
buffer[1::4] *= alpha
buffer[2::4] *= alpha
return cast(bytes, buffer.astype(numpy.uint8).tobytes())
return buffer.astype(np.uint8).tobytes()