mirror of
https://github.com/quantum5/win2xcur.git
synced 2025-04-25 02:31:56 -04:00
Compare commits
12 commits
Author | SHA1 | Date | |
---|---|---|---|
|
8e71037f5f | ||
|
0898f35183 | ||
|
54ecf8bf90 | ||
|
7863775921 | ||
|
ffb9c5e24a | ||
|
b5308cbb1e | ||
|
fcf9681371 | ||
|
ee3249c705 | ||
|
99ff1bb1ba | ||
|
a89e52a286 | ||
|
5c03c08a61 | ||
|
b979ed5fc5 |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
|
@ -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
|
||||||
|
|
10
README.md
10
README.md
|
@ -1,4 +1,4 @@
|
||||||
# `win2xcur` and `x2wincur` [](https://github.com/quantum5/win2xcur/actions) [](https://pypi.org/project/win2xcur/) [](https://pypi.org/project/win2xcur/) [](https://pypi.org/project/win2xcur/)
|
# `win2xcur` and `x2wincur` [](https://github.com/quantum5/win2xcur/actions) [](https://pypi.org/project/win2xcur/) [](https://pypi.org/project/win2xcur/) [](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
|
||||||
|
|
1
mypy.ini
1
mypy.ini
|
@ -1,3 +1,4 @@
|
||||||
[mypy]
|
[mypy]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
strict = true
|
strict = true
|
||||||
|
plugins = numpy.typing.mypy_plugin
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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'],
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
12
win2xcur/scale.py
Normal 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),
|
||||||
|
)
|
|
@ -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'))
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue