Skip to content

Commit f323692

Browse files
committed
feat: add prek and update typing
1 parent 294971f commit f323692

15 files changed

Lines changed: 384 additions & 282 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
repos:
2+
- repo: local
3+
hooks:
4+
- id: ruff
5+
name: ruff
6+
language: system
7+
entry: uv --project packages/python run ruff check --fix --config=packages/python/pyproject.toml
8+
types: [python]
9+
- id: ruff-format
10+
name: ruff-format
11+
language: system
12+
entry: uv --project packages/python run ruff format --config=packages/python/pyproject.toml
13+
types: [python]
14+
- id: mypy
15+
name: mypy
16+
language: system
17+
entry: uv --project packages/python run mypy --config-file=packages/python/pyproject.toml packages/python/src/
18+
pass_filenames: false
19+
- id: pylint
20+
name: pylint
21+
language: system
22+
entry: uv --project packages/python run pylint --rcfile=packages/python/pyproject.toml packages/python/src/
23+
pass_filenames: false

packages/python/pyproject.toml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,35 @@ strict = true
6262
warn_return_any = true
6363
warn_unused_configs = true
6464

65+
[tool.pylint.format]
66+
max-line-length = 120
67+
68+
[tool.pylint.basic]
69+
# Allow single-letter and short uppercase names common in color science (L, C, Y, dL, dC, dH)
70+
good-names = ["i", "j", "k", "x", "y", "z", "L", "C", "Y"]
71+
variable-rgx = "^[a-zA-Z_][a-zA-Z0-9_]*$"
72+
argument-rgx = "^[a-zA-Z_][a-zA-Z0-9_]*$"
73+
74+
[tool.pylint.design]
75+
max-args = 8
76+
max-positional-arguments = 8
77+
max-locals = 40
78+
max-returns = 10
79+
max-branches = 15
80+
81+
[tool.pylint.messages_control]
82+
disable = [
83+
"fixme", # TODO comments are fine
84+
"duplicate-code", # algorithmic variants share structure by design
85+
"consider-using-enumerate", # numpy indexing patterns don't benefit from enumerate
86+
"consider-using-max-builtin", # explicit comparison is clearer for float edge cases
87+
]
88+
6589
[tool.pytest.ini_options]
66-
testpaths = ["tests"]
90+
testpaths = ["tests"]
91+
92+
[dependency-groups]
93+
dev = [
94+
"prek>=0.3.4",
95+
"pylint>=4.0.5",
96+
]

packages/python/scripts/generate_patches.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@
1212
import math
1313
import sys
1414

15-
from PIL import Image, ImageDraw, ImageFont
16-
1715
from epaper_dithering import ColorScheme
16+
from PIL import Image, ImageDraw, ImageFont
1817

1918

2019
def generate_patches(scheme: ColorScheme, width: int, height: int) -> Image.Image:
@@ -47,8 +46,10 @@ def generate_patches(scheme: ColorScheme, width: int, height: int) -> Image.Imag
4746
bright = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) > 128
4847
draw.text(
4948
(x + cell_w // 2, y + cell_h - font.size),
50-
name.upper(), fill=(0, 0, 0) if bright else (255, 255, 255),
51-
font=font, anchor="mm",
49+
name.upper(),
50+
fill=(0, 0, 0) if bright else (255, 255, 255),
51+
font=font,
52+
anchor="mm",
5253
)
5354

5455
return img

packages/python/src/epaper_dithering/algorithms.py

Lines changed: 94 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from typing import cast
76

87
import numpy as np
98
from PIL import Image
@@ -47,48 +46,63 @@ class ErrorDiffusionKernel:
4746
name="Floyd-Steinberg",
4847
divisor=16,
4948
offsets=[
50-
(1, 0, 7), # Right: 7/16
49+
(1, 0, 7), # Right: 7/16
5150
(-1, 1, 3), # Down-left: 3/16
52-
(0, 1, 5), # Down: 5/16
53-
(1, 1, 1), # Down-right: 1/16
51+
(0, 1, 5), # Down: 5/16
52+
(1, 1, 1), # Down-right: 1/16
5453
],
5554
)
5655

5756
BURKES = ErrorDiffusionKernel(
5857
name="Burkes",
5958
divisor=32,
6059
offsets=[
61-
(1, 0, 8), (2, 0, 4), # Current row
62-
(-2, 1, 2), (-1, 1, 4), (0, 1, 8), (1, 1, 4), (2, 1, 2), # Next row
60+
(1, 0, 8),
61+
(2, 0, 4), # Current row
62+
(-2, 1, 2),
63+
(-1, 1, 4),
64+
(0, 1, 8),
65+
(1, 1, 4),
66+
(2, 1, 2), # Next row
6367
],
6468
)
6569

6670
SIERRA = ErrorDiffusionKernel(
6771
name="Sierra",
6872
divisor=32,
6973
offsets=[
70-
(1, 0, 5), (2, 0, 3), # Current row
71-
(-2, 1, 2), (-1, 1, 4), (0, 1, 5), (1, 1, 4), (2, 1, 2), # Row +1
72-
(-1, 2, 2), (0, 2, 3), (1, 2, 2), # Row +2
74+
(1, 0, 5),
75+
(2, 0, 3), # Current row
76+
(-2, 1, 2),
77+
(-1, 1, 4),
78+
(0, 1, 5),
79+
(1, 1, 4),
80+
(2, 1, 2), # Row +1
81+
(-1, 2, 2),
82+
(0, 2, 3),
83+
(1, 2, 2), # Row +2
7384
],
7485
)
7586

7687
SIERRA_LITE = ErrorDiffusionKernel(
7788
name="Sierra Lite",
7889
divisor=4,
7990
offsets=[
80-
(1, 0, 2), # Right: 2/4
91+
(1, 0, 2), # Right: 2/4
8192
(-1, 1, 1), # Down-left: 1/4
82-
(0, 1, 1), # Down: 1/4
93+
(0, 1, 1), # Down: 1/4
8394
],
8495
)
8596

8697
ATKINSON = ErrorDiffusionKernel(
8798
name="Atkinson",
8899
divisor=8,
89100
offsets=[
90-
(1, 0, 1), (2, 0, 1), # Current row
91-
(-1, 1, 1), (0, 1, 1), (1, 1, 1), # Row +1
101+
(1, 0, 1),
102+
(2, 0, 1), # Current row
103+
(-1, 1, 1),
104+
(0, 1, 1),
105+
(1, 1, 1), # Row +1
92106
(0, 2, 1), # Row +2
93107
],
94108
)
@@ -97,29 +111,42 @@ class ErrorDiffusionKernel:
97111
name="Stucki",
98112
divisor=42,
99113
offsets=[
100-
(1, 0, 8), (2, 0, 4), # Current row
101-
(-2, 1, 2), (-1, 1, 4), (0, 1, 8), (1, 1, 4), (2, 1, 2), # Row +1
102-
(-2, 2, 1), (-1, 2, 2), (0, 2, 4), (1, 2, 2), (2, 2, 1), # Row +2
114+
(1, 0, 8),
115+
(2, 0, 4), # Current row
116+
(-2, 1, 2),
117+
(-1, 1, 4),
118+
(0, 1, 8),
119+
(1, 1, 4),
120+
(2, 1, 2), # Row +1
121+
(-2, 2, 1),
122+
(-1, 2, 2),
123+
(0, 2, 4),
124+
(1, 2, 2),
125+
(2, 2, 1), # Row +2
103126
],
104127
)
105128

106129
JARVIS_JUDICE_NINKE = ErrorDiffusionKernel(
107130
name="Jarvis-Judice-Ninke",
108131
divisor=48,
109132
offsets=[
110-
(1, 0, 7), (2, 0, 5), # Current row
111-
(-2, 1, 3), (-1, 1, 5), (0, 1, 7), (1, 1, 5), (2, 1, 3), # Row +1
112-
(-2, 2, 1), (-1, 2, 3), (0, 2, 5), (1, 2, 3), (2, 2, 1), # Row +2
133+
(1, 0, 7),
134+
(2, 0, 5), # Current row
135+
(-2, 1, 3),
136+
(-1, 1, 5),
137+
(0, 1, 7),
138+
(1, 1, 5),
139+
(2, 1, 3), # Row +1
140+
(-2, 2, 1),
141+
(-1, 2, 3),
142+
(0, 2, 5),
143+
(1, 2, 3),
144+
(2, 2, 1), # Row +2
113145
],
114146
)
115147

116148
# Bayer 4x4 matrix normalized to [-0.5, 0.5] (centered around 0)
117-
_BAYER_4X4 = (
118-
np.array([[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]],
119-
dtype=np.float32)
120-
/ 16.0
121-
- 0.5
122-
)
149+
_BAYER_4X4 = np.array([[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]], dtype=np.float32) / 16.0 - 0.5
123150

124151

125152
def get_palette_colors(color_scheme: ColorScheme | ColorPalette) -> list[tuple[int, int, int]]:
@@ -195,8 +222,8 @@ def error_diffusion_dither(
195222
if isinstance(color_scheme, ColorPalette) and tone_compression != 0:
196223
if tone_compression == "auto":
197224
pixels_linear = auto_compress_dynamic_range(pixels_linear, palette_linear)
198-
else:
199-
pixels_linear = compress_dynamic_range(pixels_linear, palette_linear, cast(float, tone_compression))
225+
elif isinstance(tone_compression, float):
226+
pixels_linear = compress_dynamic_range(pixels_linear, palette_linear, tone_compression)
200227

201228
# Pre-compute palette LAB components for scalar per-pixel matching
202229
palette_L, palette_a, palette_b, palette_C = precompute_palette_lab(palette_linear)
@@ -274,8 +301,10 @@ def error_diffusion_dither(
274301

275302

276303
def floyd_steinberg_dither(
277-
image: Image.Image, color_scheme: ColorScheme | ColorPalette,
278-
serpentine: bool = True, tone_compression: float | str = "auto",
304+
image: Image.Image,
305+
color_scheme: ColorScheme | ColorPalette,
306+
serpentine: bool = True,
307+
tone_compression: float | str = "auto",
279308
) -> Image.Image:
280309
"""Apply Floyd-Steinberg error diffusion dithering.
281310
@@ -298,8 +327,10 @@ def floyd_steinberg_dither(
298327

299328

300329
def burkes_dither(
301-
image: Image.Image, color_scheme: ColorScheme | ColorPalette,
302-
serpentine: bool = True, tone_compression: float | str = "auto",
330+
image: Image.Image,
331+
color_scheme: ColorScheme | ColorPalette,
332+
serpentine: bool = True,
333+
tone_compression: float | str = "auto",
303334
) -> Image.Image:
304335
"""Apply Burkes error diffusion dithering.
305336
@@ -320,8 +351,10 @@ def burkes_dither(
320351

321352

322353
def sierra_dither(
323-
image: Image.Image, color_scheme: ColorScheme | ColorPalette,
324-
serpentine: bool = True, tone_compression: float | str = "auto",
354+
image: Image.Image,
355+
color_scheme: ColorScheme | ColorPalette,
356+
serpentine: bool = True,
357+
tone_compression: float | str = "auto",
325358
) -> Image.Image:
326359
"""Apply Sierra error diffusion dithering.
327360
@@ -345,8 +378,10 @@ def sierra_dither(
345378

346379

347380
def sierra_lite_dither(
348-
image: Image.Image, color_scheme: ColorScheme | ColorPalette,
349-
serpentine: bool = True, tone_compression: float | str = "auto",
381+
image: Image.Image,
382+
color_scheme: ColorScheme | ColorPalette,
383+
serpentine: bool = True,
384+
tone_compression: float | str = "auto",
350385
) -> Image.Image:
351386
"""Apply Sierra Lite error diffusion dithering.
352387
@@ -369,8 +404,10 @@ def sierra_lite_dither(
369404

370405

371406
def atkinson_dither(
372-
image: Image.Image, color_scheme: ColorScheme | ColorPalette,
373-
serpentine: bool = True, tone_compression: float | str = "auto",
407+
image: Image.Image,
408+
color_scheme: ColorScheme | ColorPalette,
409+
serpentine: bool = True,
410+
tone_compression: float | str = "auto",
374411
) -> Image.Image:
375412
"""Apply Atkinson error diffusion dithering.
376413
@@ -394,8 +431,10 @@ def atkinson_dither(
394431

395432

396433
def stucki_dither(
397-
image: Image.Image, color_scheme: ColorScheme | ColorPalette,
398-
serpentine: bool = True, tone_compression: float | str = "auto",
434+
image: Image.Image,
435+
color_scheme: ColorScheme | ColorPalette,
436+
serpentine: bool = True,
437+
tone_compression: float | str = "auto",
399438
) -> Image.Image:
400439
"""Apply Stucki error diffusion dithering.
401440
@@ -419,8 +458,10 @@ def stucki_dither(
419458

420459

421460
def jarvis_judice_ninke_dither(
422-
image: Image.Image, color_scheme: ColorScheme | ColorPalette,
423-
serpentine: bool = True, tone_compression: float | str = "auto",
461+
image: Image.Image,
462+
color_scheme: ColorScheme | ColorPalette,
463+
serpentine: bool = True,
464+
tone_compression: float | str = "auto",
424465
) -> Image.Image:
425466
"""Apply Jarvis-Judice-Ninke error diffusion dithering.
426467
@@ -449,7 +490,9 @@ def jarvis_judice_ninke_dither(
449490

450491

451492
def direct_palette_map(
452-
image: Image.Image, color_scheme: ColorScheme | ColorPalette, tone_compression: float | str = "auto",
493+
image: Image.Image,
494+
color_scheme: ColorScheme | ColorPalette,
495+
tone_compression: float | str = "auto",
453496
) -> Image.Image:
454497
"""Map image colors directly to palette without dithering.
455498
@@ -485,8 +528,8 @@ def direct_palette_map(
485528
if isinstance(color_scheme, ColorPalette) and tone_compression != 0:
486529
if tone_compression == "auto":
487530
pixels_linear = auto_compress_dynamic_range(pixels_linear, palette_linear)
488-
else:
489-
pixels_linear = compress_dynamic_range(pixels_linear, palette_linear, cast(float, tone_compression))
531+
elif isinstance(tone_compression, float):
532+
pixels_linear = compress_dynamic_range(pixels_linear, palette_linear, tone_compression)
490533

491534
# Find closest palette color for ALL pixels at once using LAB
492535
output_pixels = find_closest_palette_color_lab(pixels_linear, palette_linear)
@@ -501,7 +544,9 @@ def direct_palette_map(
501544

502545

503546
def ordered_dither(
504-
image: Image.Image, color_scheme: ColorScheme | ColorPalette, tone_compression: float | str = "auto",
547+
image: Image.Image,
548+
color_scheme: ColorScheme | ColorPalette,
549+
tone_compression: float | str = "auto",
505550
) -> Image.Image:
506551
"""Apply ordered (Bayer) dithering with full vectorization.
507552
@@ -528,10 +573,7 @@ def ordered_dither(
528573
"""
529574
# Bayer 4x4 matrix normalized to [-0.5, 0.5] (centered around 0)
530575
bayer_matrix = (
531-
np.array([[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]],
532-
dtype=np.float32)
533-
/ 16.0
534-
- 0.5
576+
np.array([[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]], dtype=np.float32) / 16.0 - 0.5
535577
)
536578

537579
# ===== Image Preprocessing =====
@@ -555,14 +597,14 @@ def ordered_dither(
555597
if isinstance(color_scheme, ColorPalette) and tone_compression != 0:
556598
if tone_compression == "auto":
557599
pixels_linear = auto_compress_dynamic_range(pixels_linear, palette_linear)
558-
else:
559-
pixels_linear = compress_dynamic_range(pixels_linear, palette_linear, cast(float, tone_compression))
600+
elif isinstance(tone_compression, float):
601+
pixels_linear = compress_dynamic_range(pixels_linear, palette_linear, tone_compression)
560602

561603
# ===== VECTORIZED ORDERED DITHERING =====
562604

563605
# Create threshold matrix for entire image using broadcasting
564606
y_indices = np.arange(height)[:, np.newaxis] % 4 # Shape: (height, 1)
565-
x_indices = np.arange(width)[np.newaxis, :] % 4 # Shape: (1, width)
607+
x_indices = np.arange(width)[np.newaxis, :] % 4 # Shape: (1, width)
566608
threshold_matrix = bayer_matrix[y_indices, x_indices] # Shape: (height, width)
567609

568610
# Add threshold to all pixels at once

0 commit comments

Comments
 (0)