Skip to content

Commit

Permalink
Merge pull request #62 from ful1e5/feat/custom-canvas-sizes
Browse files Browse the repository at this point in the history
feat: custom canvas sizes for improved Windows cursor resizing
  • Loading branch information
ful1e5 authored May 23, 2024
2 parents 5bc1a49 + 16d4f24 commit 30a8d5a
Show file tree
Hide file tree
Showing 22 changed files with 123 additions and 62 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

### What's New?

- Clickgen now allows cursor bitmap re-canvasing by specifying size using the `cursor_size:canvas_size` format. See [changelog-05212024](https://github.com/ful1e5/clickgen/discussions/59#discussioncomment-9511166)

## [v2.2.2] - 24 April 2024

### Important Changes
Expand Down
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,22 @@ Clickgen's core functionality is heavily inspired by [quantum5/win2xcur](https:/
> **Note**
> The project's success depends on sponsorships. Meeting sponsorship goals for [ful1e5](https://github.com/ful1e5) GitHub Account will drive new releases and ongoing development.
- **2023-08-23:** `ctgen` CLI supports `.json` and `.yaml`/`.yml` as configuration file.
- **2023-08-17:** Cursor size settings moved to `[cursors.fallback_settings]` in config. See [changelog-08172023](https://github.com/ful1e5/clickgen/discussions/59#discussioncomment-6747666)
- **2022-06-15:** Docker Image support deprecated due to cross-platform compatibility.
- **2022-07-09:** :warning: All the **functionality and modules are removed from older versions in `v2.0.0`**.
I will be restricting any updates to the `>=v1.2.0` versions to security updates and hotfixes.
Check updated documentations for [building cursors from API](#api-examples) and [CLIs](#usage) usage.
- **2024-05-24:** Clickgen now allows cursor bitmap re-canvasing by specifying size using the `cursor_size:canvas_size` format. See [changelog-05212024](https://github.com/ful1e5/clickgen/discussions/59#discussioncomment-9511166)
- **2023-08-23:** `ctgen` CLI supports `.json` and `.yaml`/`.yml` as configuration file.
- **2023-08-17:** Cursor size settings moved to `[cursors.fallback_settings]` in config. See [changelog-08172023](https://github.com/ful1e5/clickgen/discussions/59#discussioncomment-6747666)
- **2022-06-15:** Docker Image support deprecated due to cross-platform compatibility.
- **2022-07-09:** :warning: All the **functionality and modules are removed from older versions in `v2.0.0`**.
I will be restricting any updates to the `>=v1.2.0` versions to security updates and hotfixes.
Check updated documentations for [building cursors from API](#api-examples) and [CLIs](#usage) usage.

## Requirements

- Python version 3.7.5 or higher
- [Pillow](https://pypi.org/project/Pillow) >= 8.1.1
- [PyYaml](https://pypi.org/project/PyYaml) >= 6.0.1
- [attrs](https://pypi.org/project/attrs) >= 15.0.0
- [numpy](https://pypi.org/project/numpy) >= 1.21.6
- [toml](https://pypi.org/project/toml) >= 0.10.2
- Python version 3.7.5 or higher
- [Pillow](https://pypi.org/project/Pillow) >= 8.1.1
- [PyYaml](https://pypi.org/project/PyYaml) >= 6.0.1
- [attrs](https://pypi.org/project/attrs) >= 15.0.0
- [numpy](https://pypi.org/project/numpy) >= 1.21.6
- [toml](https://pypi.org/project/toml) >= 0.10.2

## Install

Expand All @@ -45,7 +46,7 @@ pip3 install clickgen
### Arch Linux

- [AUR](https://aur.archlinux.org/packages/python-clickgen)
- [AUR](https://aur.archlinux.org/packages/python-clickgen)

## Usage

Expand Down
2 changes: 1 addition & 1 deletion samples/custom_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
pngs.append(p.read())

try:
ani = open_blob(pngs, hotspot=(100, 100))
ani = open_blob(pngs, hotspot=(100, 100), sizes=["24:32", "32"])

# save Windows animated cursor
aext, aresult = to_win(ani.frames)
Expand Down
2 changes: 1 addition & 1 deletion samples/sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"cursors": {
"fallback_settings": {
"x11_sizes": [16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 88, 96],
"win_sizes": [24, 32, 48, 64, 96],
"win_sizes": ["24:32", "32", "48", "64", "96"],
"x_hotspot": 53,
"y_hotspot": 36,
"x11_delay": 30,
Expand Down
6 changes: 3 additions & 3 deletions samples/sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ platforms = ['x11', 'windows']

[cursors]
[cursors.fallback_settings]
x11_sizes = [16,20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 88, 96]
win_sizes = [24, 32, 48, 64, 96]
x11_sizes = [16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 88, 96]
win_sizes = ['24:32', '32', '48', '64', '96']
x_hotspot = 53
y_hotspot = 36
x11_delay = 30
Expand All @@ -27,7 +27,7 @@ x11_symlinks = ['link1']
png = 'pointer.png'
win_name = 'Alternate'
x11_name = 'pointer2'
x11_symlinks = ['link2','link3', 'link4', 'link5', 'link6','link7', 'link8']
x11_symlinks = ['link2', 'link3', 'link4', 'link5', 'link6', 'link7', 'link8']


[cursors.test_pointer3]
Expand Down
2 changes: 1 addition & 1 deletion samples/sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ config:
cursors:
fallback_settings:
x11_sizes: [16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 88, 96]
win_sizes: [24, 32, 48, 64, 96]
win_sizes: ["24", "32", "48", "64", "96"]
x_hotspot: 53
y_hotspot: 36
x11_delay: 30
Expand Down
14 changes: 7 additions & 7 deletions src/clickgen/configparser.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ from clickgen.parser.png import DELAY as DELAY, SIZES as SIZES
from clickgen.writer.windows import to_win as to_win
from clickgen.writer.x11 import to_x11 as to_x11
from pathlib import Path
from typing import Any, Dict, List, TypeVar
from typing import Any, TypeVar

class ThemeSection:
name: str
Expand All @@ -16,25 +16,25 @@ class ThemeSection:
def __gt__(self, other): ...
def __ge__(self, other): ...

def parse_theme_section(d: Dict[str, Any], **kwargs) -> ThemeSection: ...
def parse_theme_section(d: dict[str, Any], **kwargs) -> ThemeSection: ...

class ConfigSection:
bitmaps_dir: Path
out_dir: Path
platforms: List[str]
platforms: list[str]
def __init__(self, bitmaps_dir, out_dir, platforms) -> None: ...
def __lt__(self, other): ...
def __le__(self, other): ...
def __gt__(self, other): ...
def __ge__(self, other): ...

def parse_config_section(fp: Path, d: Dict[str, Any], **kwargs) -> ConfigSection: ...
def parse_config_section(fp: Path, d: dict[str, Any], **kwargs) -> ConfigSection: ...
T = TypeVar('T')

class CursorSection:
x11_cursor_name: str | None
x11_cursor: bytes | None
x11_symlinks: List[str]
x11_symlinks: list[str]
win_cursor_name: str | None
win_cursor: bytes | None
def __init__(self, x11_cursor_name, x11_cursor, x11_symlinks, win_cursor_name, win_cursor) -> None: ...
Expand All @@ -43,12 +43,12 @@ class CursorSection:
def __gt__(self, other): ...
def __ge__(self, other): ...

def parse_cursors_section(d: Dict[str, Any], config: ConfigSection, **kwargs) -> List[CursorSection]: ...
def parse_cursors_section(d: dict[str, Any], config: ConfigSection, **kwargs) -> list[CursorSection]: ...

class ClickgenConfig:
theme: ThemeSection
config: ConfigSection
cursors: List[CursorSection]
cursors: list[CursorSection]
def __init__(self, theme, config, cursors) -> None: ...
def __lt__(self, other): ...
def __le__(self, other): ...
Expand Down
10 changes: 5 additions & 5 deletions src/clickgen/cursors.pyi
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from PIL.Image import Image as Image
from typing import Iterator, List, Tuple
from typing import Iterator

class CursorImage:
image: Image
hotspot: Tuple[int, int]
hotspot: tuple[int, int]
nominal: int
def __init__(self, image: Image, hotspot: Tuple[int, int], nominal: int) -> None: ...
def __init__(self, image: Image, hotspot: tuple[int, int], nominal: int) -> None: ...

class CursorFrame:
images: List[CursorImage]
images: list[CursorImage]
delay: int
def __init__(self, images: List[CursorImage], delay: int = 0) -> None: ...
def __init__(self, images: list[CursorImage], delay: int = 0) -> None: ...
def __getitem__(self, item: int) -> CursorImage: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[CursorImage]: ...
3 changes: 1 addition & 2 deletions src/clickgen/packer/windows.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from _typeshed import Incomplete
from pathlib import Path
from string import Template
from typing import Dict

FILE_TEMPLETES: Dict[str, Template]
FILE_TEMPLETES: dict[str, Template]
all_wreg: Incomplete

def pack_win(dir: Path, theme_name: str, comment: str, website: str | None = None) -> None: ...
3 changes: 1 addition & 2 deletions src/clickgen/packer/x11.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from pathlib import Path
from string import Template
from typing import Dict

FILE_TEMPLATES: Dict[str, Template]
FILE_TEMPLATES: dict[str, Template]

def pack_x11(dir: Path, theme_name: str, comment: str) -> None: ...
3 changes: 1 addition & 2 deletions src/clickgen/parser/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from clickgen.parser.base import BaseParser
from clickgen.parser.png import MultiPNGParser as MultiPNGParser, SinglePNGParser as SinglePNGParser
from typing import List, Tuple

__all__ = ['SinglePNGParser', 'MultiPNGParser', 'open_blob']

def open_blob(blob: bytes | List[bytes], hotspot: Tuple[int, int], sizes: List[int] | None = None, delay: int | None = None) -> BaseParser: ...
def open_blob(blob: bytes | list[bytes], hotspot: tuple[int, int], sizes: list[int] | None = None, delay: int | None = None) -> BaseParser: ...
4 changes: 2 additions & 2 deletions src/clickgen/parser/base.pyi
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from abc import ABCMeta, abstractmethod
from clickgen.cursors import CursorFrame as CursorFrame
from typing import Any, List
from typing import Any

class BaseParser(metaclass=ABCMeta):
blob: bytes
frames: List[CursorFrame]
frames: list[CursorFrame]
@abstractmethod
def __init__(self, blob: bytes): ...
@classmethod
Expand Down
42 changes: 37 additions & 5 deletions src/clickgen/parser/png.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-

import io
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Union

from PIL import Image

Expand All @@ -24,14 +24,14 @@ def __init__(
self,
blob: bytes,
hotspot: Tuple[int, int],
sizes: Optional[List[int]] = None,
sizes: Optional[List[Union[int, str]]] = None,
delay: Optional[int] = None,
) -> None:
super().__init__(blob)
self._image = Image.open(io.BytesIO(self.blob))

# 'set' to prevent value duplication
if not sizes:
# 'set' to prevent value duplication
self.sizes = set(SIZES)
else:
self.sizes = set(sizes)
Expand All @@ -58,9 +58,41 @@ def _dim(i: int) -> int:
def _parse(self) -> List[CursorFrame]:
images: List[CursorImage] = []
for s in sorted(self.sizes):
res_img = self._image.resize((s, s), 1)
size: int = 0
canvas_size: int = 0

if isinstance(s, str):
try:
if ":" in s:
size_str, canvas_size_str = s.split(":")
size = int(size_str)
canvas_size = int(canvas_size_str)
else:
size = int(s)
canvas_size = size
except ValueError:
raise ValueError(
f"'sizes' input '{s}' must be an integer or integers separated by ':'."
)
elif isinstance(s, int):
size = s
canvas_size = s
else:
raise TypeError(
"Input must be 'cursor_size:canvas_size' or an integer."
)

res_img = self._image.resize((size, size), 1)

if size != canvas_size:
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
canvas.paste(res_img, (0, 0))
res_img = canvas

res_hotspot = self._cal_hotspot(res_img)
images.append(CursorImage(image=res_img, hotspot=res_hotspot, nominal=s))
images.append(
CursorImage(image=res_img, hotspot=res_hotspot, nominal=canvas_size)
)

return [CursorFrame(images, delay=self.delay)]

Expand Down
7 changes: 3 additions & 4 deletions src/clickgen/parser/png.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from _typeshed import Incomplete
from clickgen.cursors import CursorFrame as CursorFrame, CursorImage as CursorImage
from clickgen.parser.base import BaseParser as BaseParser
from typing import List, Tuple

SIZES: Incomplete
DELAY: int
Expand All @@ -14,10 +13,10 @@ class SinglePNGParser(BaseParser):
delay: Incomplete
hotspot: Incomplete
frames: Incomplete
def __init__(self, blob: bytes, hotspot: Tuple[int, int], sizes: List[int] | None = None, delay: int | None = None) -> None: ...
def __init__(self, blob: bytes, hotspot: tuple[int, int], sizes: list[int | str] | None = None, delay: int | None = None) -> None: ...

class MultiPNGParser(BaseParser):
@classmethod
def can_parse(cls, blobs: List[bytes]) -> bool: ...
def can_parse(cls, blobs: list[bytes]) -> bool: ...
frames: Incomplete
def __init__(self, blobs: List[bytes], hotspot: Tuple[int, int], sizes: List[int] | None = None, delay: int | None = None) -> None: ...
def __init__(self, blobs: list[bytes], hotspot: tuple[int, int], sizes: list[int] | None = None, delay: int | None = None) -> None: ...
2 changes: 1 addition & 1 deletion src/clickgen/scripts/clickgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def main() -> None:
dest="sizes",
nargs="+",
default=SIZES,
type=int,
type=str,
help="Set pixel-size for cursor.",
)
parser.add_argument(
Expand Down
2 changes: 1 addition & 1 deletion src/clickgen/scripts/ctgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def main() -> None: # noqa: C901
dest="sizes",
nargs="+",
default=None,
type=int,
type=str,
help=""" Change cursor size.
Multiple sizes are assigned to XCursor
while one size will be assigned to Windows.""",
Expand Down
4 changes: 2 additions & 2 deletions src/clickgen/scripts/ctgen.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ from clickgen.configparser import parse_config_file as parse_config_file
from clickgen.libs.colors import blue as blue, bold as bold, cyan as cyan, fail as fail, magenta as magenta, print_done as print_done, print_info as print_info, print_subtext as print_subtext, print_text as print_text
from clickgen.packer.windows import pack_win as pack_win
from clickgen.packer.x11 import pack_x11 as pack_x11
from typing import Any, Dict, Generator
from typing import Any, Generator

def get_kwargs(args) -> Dict[str, Any]: ...
def get_kwargs(args) -> dict[str, Any]: ...
def cwd(path) -> Generator[None, None, None]: ...
def main() -> None: ...
4 changes: 1 addition & 3 deletions src/clickgen/writer/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,8 @@ def re_canvas(size: int, img: Image.Image):
re_canvas(96, clone).save(blob, "PNG")
elif width <= 128 or height <= 128:
re_canvas(128, clone).save(blob, "PNG")
elif width <= 256 or height <= 256:
re_canvas(256, clone).save(blob, "PNG")
else:
raise ValueError(f"Unable to re-canvas windows cursors: {width}x{height}")
re_canvas(256, clone).save(blob, "PNG")

blob.seek(0)
image_data.append(blob.read())
Expand Down
9 changes: 4 additions & 5 deletions src/clickgen/writer/windows.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from _typeshed import Incomplete
from clickgen.cursors import CursorFrame as CursorFrame
from typing import List, Tuple

MAGIC: bytes
ICO_TYPE_CUR: int
Expand All @@ -24,7 +23,7 @@ UNSIGNED: Incomplete
SEQUENCE_FLAG: int
ICON_FLAG: int

def get_ani_cur_list(frames: List[CursorFrame]) -> bytes: ...
def get_ani_rate_chunk(frames: List[CursorFrame]) -> bytes: ...
def to_ani(frames: List[CursorFrame]) -> bytes: ...
def to_win(frames: List[CursorFrame]) -> Tuple[str, bytes]: ...
def get_ani_cur_list(frames: list[CursorFrame]) -> bytes: ...
def get_ani_rate_chunk(frames: list[CursorFrame]) -> bytes: ...
def to_ani(frames: list[CursorFrame]) -> bytes: ...
def to_win(frames: list[CursorFrame]) -> tuple[str, bytes]: ...
3 changes: 1 addition & 2 deletions src/clickgen/writer/x11.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from _typeshed import Incomplete
from clickgen.cursors import CursorFrame as CursorFrame
from typing import List

MAGIC: bytes
VERSION: int
Expand All @@ -10,4 +9,4 @@ CHUNK_IMAGE: int
IMAGE_HEADER: Incomplete

def premultiply_alpha(source: bytes) -> bytes: ...
def to_x11(frames: List[CursorFrame]) -> bytes: ...
def to_x11(frames: list[CursorFrame]) -> bytes: ...
Loading

0 comments on commit 30a8d5a

Please sign in to comment.