dithering 0.1.26__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. dithering-0.2.0/.github/workflows/CI.yml +256 -0
  2. dithering-0.2.0/Cargo.lock +218 -0
  3. dithering-0.2.0/Cargo.toml +25 -0
  4. {dithering-0.1.26 → dithering-0.2.0}/LICENSE +1 -1
  5. dithering-0.2.0/PKG-INFO +179 -0
  6. dithering-0.2.0/README.md +149 -0
  7. dithering-0.2.0/dithering.pyi +209 -0
  8. dithering-0.2.0/pyproject.toml +48 -0
  9. dithering-0.2.0/resources/BackyardML logo 2.png +0 -0
  10. dithering-0.2.0/resources/BackyardML logo 2.svg +2768 -0
  11. dithering-0.2.0/resources/BackyardML_logo.png +0 -0
  12. dithering-0.2.0/resources/examples/atkinson.png +0 -0
  13. dithering-0.2.0/resources/examples/bayer2x2.png +0 -0
  14. dithering-0.2.0/resources/examples/bayer8x8.png +0 -0
  15. dithering-0.2.0/resources/examples/floyd_steinberg.png +0 -0
  16. dithering-0.2.0/resources/examples/gameboy.png +0 -0
  17. dithering-0.2.0/resources/examples/levels4.png +0 -0
  18. dithering-0.2.0/resources/examples/original.png +0 -0
  19. dithering-0.2.0/resources/examples/original_gray.png +0 -0
  20. dithering-0.2.0/resources/examples/random.png +0 -0
  21. dithering-0.2.0/resources/examples/retro16.png +0 -0
  22. dithering-0.2.0/resources/examples/rgb_fs.png +0 -0
  23. dithering-0.2.0/resources/examples/stucki_serpentine.png +0 -0
  24. dithering-0.2.0/src/diffusion.rs +611 -0
  25. dithering-0.2.0/src/lib.rs +725 -0
  26. dithering-0.2.0/src/matrices.rs +209 -0
  27. dithering-0.2.0/src/ordered.rs +320 -0
  28. dithering-0.2.0/src/palette.rs +104 -0
  29. dithering-0.2.0/src/space.rs +162 -0
  30. dithering-0.2.0/tests/test_dithering.py +456 -0
  31. dithering-0.1.26/.github/workflows/CI.yml +0 -120
  32. dithering-0.1.26/Cargo.lock +0 -396
  33. dithering-0.1.26/Cargo.toml +0 -20
  34. dithering-0.1.26/PKG-INFO +0 -80
  35. dithering-0.1.26/README.md +0 -64
  36. dithering-0.1.26/dithering.pyi +0 -19
  37. dithering-0.1.26/pyproject.toml +0 -16
  38. dithering-0.1.26/src/lib.rs +0 -10
  39. dithering-0.1.26/src/methods/mod.rs +0 -3
  40. dithering-0.1.26/src/methods/ordered.rs +0 -128
  41. {dithering-0.1.26 → dithering-0.2.0}/.gitignore +0 -0
  42. {dithering-0.1.26 → dithering-0.2.0}/resources/BackyardML_logo.svg +0 -0
  43. {dithering-0.1.26 → dithering-0.2.0}/resources/dithered.png +0 -0
  44. {dithering-0.1.26 → dithering-0.2.0}/resources/dithering.png +0 -0
  45. {dithering-0.1.26 → dithering-0.2.0}/resources/dithering.svg +0 -0
  46. {dithering-0.1.26 → dithering-0.2.0}/resources/original.png +0 -0
@@ -0,0 +1,256 @@
1
+ # Based on the template from `maturin generate-ci github` (maturin v1.14.1),
2
+ # with test jobs added. Regenerate with that command when maturin evolves.
3
+ name: CI
4
+
5
+ on:
6
+ push:
7
+ branches:
8
+ - main
9
+ - master
10
+ tags:
11
+ - '*'
12
+ pull_request:
13
+ workflow_dispatch:
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ test-rust:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v6
23
+ - uses: actions/setup-python@v6
24
+ with:
25
+ python-version: "3.13"
26
+ - uses: dtolnay/rust-toolchain@stable
27
+ - name: Run Rust unit tests
28
+ run: cargo test
29
+ - name: Clippy
30
+ run: cargo clippy -- -D warnings
31
+ - name: rustfmt
32
+ run: cargo fmt --check
33
+
34
+ test-python:
35
+ runs-on: ${{ matrix.os }}
36
+ strategy:
37
+ fail-fast: false
38
+ matrix:
39
+ os: [ubuntu-latest, macos-latest, windows-latest]
40
+ python-version: ["3.9", "3.14"]
41
+ steps:
42
+ - uses: actions/checkout@v6
43
+ - uses: actions/setup-python@v6
44
+ with:
45
+ python-version: ${{ matrix.python-version }}
46
+ - uses: dtolnay/rust-toolchain@stable
47
+ - name: Build and install wheel
48
+ run: |
49
+ python -m pip install --upgrade pip maturin numpy pytest pillow
50
+ maturin build --release --out dist
51
+ python -m pip install --no-index --find-links dist dithering
52
+ - name: Run Python tests
53
+ run: pytest tests/ -v
54
+
55
+ linux:
56
+ runs-on: ${{ matrix.platform.runner }}
57
+ needs: [test-rust, test-python]
58
+ strategy:
59
+ matrix:
60
+ platform:
61
+ - runner: ubuntu-22.04
62
+ target: x86_64
63
+ - runner: ubuntu-22.04
64
+ target: x86
65
+ - runner: ubuntu-22.04
66
+ target: aarch64
67
+ - runner: ubuntu-22.04
68
+ target: armv7
69
+ - runner: ubuntu-22.04
70
+ target: s390x
71
+ - runner: ubuntu-22.04
72
+ target: ppc64le
73
+ steps:
74
+ - uses: actions/checkout@v6
75
+ - uses: actions/setup-python@v6
76
+ with:
77
+ python-version: "3.9"
78
+ - name: Build wheels
79
+ uses: PyO3/maturin-action@v1
80
+ with:
81
+ target: ${{ matrix.platform.target }}
82
+ args: --release --out dist
83
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
84
+ manylinux: auto
85
+ - name: Build free-threaded wheels
86
+ uses: PyO3/maturin-action@v1
87
+ with:
88
+ target: ${{ matrix.platform.target }}
89
+ args: --release --out dist -i python3.14t
90
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
91
+ manylinux: auto
92
+ - name: Upload wheels
93
+ uses: actions/upload-artifact@v6
94
+ with:
95
+ name: wheels-linux-${{ matrix.platform.target }}
96
+ path: dist
97
+
98
+ musllinux:
99
+ runs-on: ${{ matrix.platform.runner }}
100
+ needs: [test-rust, test-python]
101
+ strategy:
102
+ matrix:
103
+ platform:
104
+ - runner: ubuntu-22.04
105
+ target: x86_64
106
+ - runner: ubuntu-22.04
107
+ target: x86
108
+ - runner: ubuntu-22.04
109
+ target: aarch64
110
+ - runner: ubuntu-22.04
111
+ target: armv7
112
+ steps:
113
+ - uses: actions/checkout@v6
114
+ - uses: actions/setup-python@v6
115
+ with:
116
+ python-version: "3.9"
117
+ - name: Build wheels
118
+ uses: PyO3/maturin-action@v1
119
+ with:
120
+ target: ${{ matrix.platform.target }}
121
+ args: --release --out dist
122
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
123
+ manylinux: musllinux_1_2
124
+ - name: Build free-threaded wheels
125
+ uses: PyO3/maturin-action@v1
126
+ with:
127
+ target: ${{ matrix.platform.target }}
128
+ args: --release --out dist -i python3.14t
129
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
130
+ manylinux: musllinux_1_2
131
+ - name: Upload wheels
132
+ uses: actions/upload-artifact@v6
133
+ with:
134
+ name: wheels-musllinux-${{ matrix.platform.target }}
135
+ path: dist
136
+
137
+ windows:
138
+ runs-on: ${{ matrix.platform.runner }}
139
+ needs: [test-rust, test-python]
140
+ strategy:
141
+ matrix:
142
+ platform:
143
+ - runner: windows-latest
144
+ target: x64
145
+ python_arch: x64
146
+ - runner: windows-latest
147
+ target: x86
148
+ python_arch: x86
149
+ - runner: windows-11-arm
150
+ target: aarch64
151
+ python_arch: arm64
152
+ steps:
153
+ - uses: actions/checkout@v6
154
+ - uses: actions/setup-python@v6
155
+ with:
156
+ python-version: "3.13"
157
+ architecture: ${{ matrix.platform.python_arch }}
158
+ - name: Build wheels
159
+ uses: PyO3/maturin-action@v1
160
+ with:
161
+ target: ${{ matrix.platform.target }}
162
+ args: --release --out dist
163
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
164
+ - uses: actions/setup-python@v6
165
+ with:
166
+ python-version: "3.14t"
167
+ architecture: ${{ matrix.platform.python_arch }}
168
+ - name: Build free-threaded wheels
169
+ uses: PyO3/maturin-action@v1
170
+ with:
171
+ target: ${{ matrix.platform.target }}
172
+ args: --release --out dist -i python3.14t
173
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
174
+ - name: Upload wheels
175
+ uses: actions/upload-artifact@v6
176
+ with:
177
+ name: wheels-windows-${{ matrix.platform.target }}
178
+ path: dist
179
+
180
+ macos:
181
+ runs-on: ${{ matrix.platform.runner }}
182
+ needs: [test-rust, test-python]
183
+ strategy:
184
+ matrix:
185
+ platform:
186
+ - runner: macos-15-intel
187
+ target: x86_64
188
+ - runner: macos-latest
189
+ target: aarch64
190
+ steps:
191
+ - uses: actions/checkout@v6
192
+ - uses: actions/setup-python@v6
193
+ with:
194
+ python-version: "3.9"
195
+ - name: Build wheels
196
+ uses: PyO3/maturin-action@v1
197
+ with:
198
+ target: ${{ matrix.platform.target }}
199
+ args: --release --out dist
200
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
201
+ - uses: actions/setup-python@v6
202
+ with:
203
+ python-version: "3.14t"
204
+ - name: Build free-threaded wheels
205
+ uses: PyO3/maturin-action@v1
206
+ with:
207
+ target: ${{ matrix.platform.target }}
208
+ args: --release --out dist -i python3.14t
209
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
210
+ - name: Upload wheels
211
+ uses: actions/upload-artifact@v6
212
+ with:
213
+ name: wheels-macos-${{ matrix.platform.target }}
214
+ path: dist
215
+
216
+ sdist:
217
+ runs-on: ubuntu-latest
218
+ steps:
219
+ - uses: actions/checkout@v6
220
+ - name: Build sdist
221
+ uses: PyO3/maturin-action@v1
222
+ with:
223
+ command: sdist
224
+ args: --out dist
225
+ - name: Upload sdist
226
+ uses: actions/upload-artifact@v6
227
+ with:
228
+ name: wheels-sdist
229
+ path: dist
230
+
231
+ release:
232
+ name: Release
233
+ runs-on: ubuntu-latest
234
+ if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
235
+ needs: [linux, musllinux, windows, macos, sdist]
236
+ permissions:
237
+ # Use to sign the release artifacts
238
+ id-token: write
239
+ # Used to upload release artifacts
240
+ contents: write
241
+ # Used to generate artifact attestation
242
+ attestations: write
243
+ steps:
244
+ - uses: actions/download-artifact@v7
245
+ - name: Generate artifact attestation
246
+ uses: actions/attest@v4
247
+ with:
248
+ subject-path: 'wheels-*/*'
249
+ - name: Install uv
250
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
251
+ uses: astral-sh/setup-uv@v7
252
+ - name: Publish to PyPI
253
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
254
+ run: uv publish 'wheels-*/*'
255
+ env:
256
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,218 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.1.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
10
+
11
+ [[package]]
12
+ name = "dithering"
13
+ version = "0.2.0"
14
+ dependencies = [
15
+ "numpy",
16
+ "pyo3",
17
+ ]
18
+
19
+ [[package]]
20
+ name = "heck"
21
+ version = "0.5.0"
22
+ source = "registry+https://github.com/rust-lang/crates.io-index"
23
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
24
+
25
+ [[package]]
26
+ name = "libc"
27
+ version = "0.2.147"
28
+ source = "registry+https://github.com/rust-lang/crates.io-index"
29
+ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
30
+
31
+ [[package]]
32
+ name = "matrixmultiply"
33
+ version = "0.3.7"
34
+ source = "registry+https://github.com/rust-lang/crates.io-index"
35
+ checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77"
36
+ dependencies = [
37
+ "autocfg",
38
+ "rawpointer",
39
+ ]
40
+
41
+ [[package]]
42
+ name = "ndarray"
43
+ version = "0.15.6"
44
+ source = "registry+https://github.com/rust-lang/crates.io-index"
45
+ checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32"
46
+ dependencies = [
47
+ "matrixmultiply",
48
+ "num-complex",
49
+ "num-integer",
50
+ "num-traits",
51
+ "rawpointer",
52
+ ]
53
+
54
+ [[package]]
55
+ name = "num-complex"
56
+ version = "0.4.3"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d"
59
+ dependencies = [
60
+ "num-traits",
61
+ ]
62
+
63
+ [[package]]
64
+ name = "num-integer"
65
+ version = "0.1.45"
66
+ source = "registry+https://github.com/rust-lang/crates.io-index"
67
+ checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
68
+ dependencies = [
69
+ "autocfg",
70
+ "num-traits",
71
+ ]
72
+
73
+ [[package]]
74
+ name = "num-traits"
75
+ version = "0.2.15"
76
+ source = "registry+https://github.com/rust-lang/crates.io-index"
77
+ checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
78
+ dependencies = [
79
+ "autocfg",
80
+ ]
81
+
82
+ [[package]]
83
+ name = "numpy"
84
+ version = "0.29.0"
85
+ source = "registry+https://github.com/rust-lang/crates.io-index"
86
+ checksum = "6a5b15d63a5ff39e378daed0e1340d3a5964703ea9712eb09a0dc66fade996f4"
87
+ dependencies = [
88
+ "libc",
89
+ "ndarray",
90
+ "num-complex",
91
+ "num-integer",
92
+ "num-traits",
93
+ "pyo3",
94
+ "pyo3-build-config",
95
+ "rustc-hash",
96
+ ]
97
+
98
+ [[package]]
99
+ name = "once_cell"
100
+ version = "1.21.4"
101
+ source = "registry+https://github.com/rust-lang/crates.io-index"
102
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
103
+
104
+ [[package]]
105
+ name = "portable-atomic"
106
+ version = "1.13.1"
107
+ source = "registry+https://github.com/rust-lang/crates.io-index"
108
+ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
109
+
110
+ [[package]]
111
+ name = "proc-macro2"
112
+ version = "1.0.106"
113
+ source = "registry+https://github.com/rust-lang/crates.io-index"
114
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
115
+ dependencies = [
116
+ "unicode-ident",
117
+ ]
118
+
119
+ [[package]]
120
+ name = "pyo3"
121
+ version = "0.29.0"
122
+ source = "registry+https://github.com/rust-lang/crates.io-index"
123
+ checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
124
+ dependencies = [
125
+ "libc",
126
+ "once_cell",
127
+ "portable-atomic",
128
+ "pyo3-build-config",
129
+ "pyo3-ffi",
130
+ "pyo3-macros",
131
+ ]
132
+
133
+ [[package]]
134
+ name = "pyo3-build-config"
135
+ version = "0.29.0"
136
+ source = "registry+https://github.com/rust-lang/crates.io-index"
137
+ checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
138
+ dependencies = [
139
+ "target-lexicon",
140
+ ]
141
+
142
+ [[package]]
143
+ name = "pyo3-ffi"
144
+ version = "0.29.0"
145
+ source = "registry+https://github.com/rust-lang/crates.io-index"
146
+ checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
147
+ dependencies = [
148
+ "libc",
149
+ "pyo3-build-config",
150
+ ]
151
+
152
+ [[package]]
153
+ name = "pyo3-macros"
154
+ version = "0.29.0"
155
+ source = "registry+https://github.com/rust-lang/crates.io-index"
156
+ checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
157
+ dependencies = [
158
+ "proc-macro2",
159
+ "pyo3-macros-backend",
160
+ "quote",
161
+ "syn",
162
+ ]
163
+
164
+ [[package]]
165
+ name = "pyo3-macros-backend"
166
+ version = "0.29.0"
167
+ source = "registry+https://github.com/rust-lang/crates.io-index"
168
+ checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
169
+ dependencies = [
170
+ "heck",
171
+ "proc-macro2",
172
+ "quote",
173
+ "syn",
174
+ ]
175
+
176
+ [[package]]
177
+ name = "quote"
178
+ version = "1.0.46"
179
+ source = "registry+https://github.com/rust-lang/crates.io-index"
180
+ checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
181
+ dependencies = [
182
+ "proc-macro2",
183
+ ]
184
+
185
+ [[package]]
186
+ name = "rawpointer"
187
+ version = "0.2.1"
188
+ source = "registry+https://github.com/rust-lang/crates.io-index"
189
+ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
190
+
191
+ [[package]]
192
+ name = "rustc-hash"
193
+ version = "2.1.3"
194
+ source = "registry+https://github.com/rust-lang/crates.io-index"
195
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
196
+
197
+ [[package]]
198
+ name = "syn"
199
+ version = "2.0.118"
200
+ source = "registry+https://github.com/rust-lang/crates.io-index"
201
+ checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
202
+ dependencies = [
203
+ "proc-macro2",
204
+ "quote",
205
+ "unicode-ident",
206
+ ]
207
+
208
+ [[package]]
209
+ name = "target-lexicon"
210
+ version = "0.13.5"
211
+ source = "registry+https://github.com/rust-lang/crates.io-index"
212
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
213
+
214
+ [[package]]
215
+ name = "unicode-ident"
216
+ version = "1.0.10"
217
+ source = "registry+https://github.com/rust-lang/crates.io-index"
218
+ checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
@@ -0,0 +1,25 @@
1
+ [package]
2
+ name = "dithering"
3
+ version = "0.2.0"
4
+ edition = "2021"
5
+ rust-version = "1.83"
6
+ authors = ["Patrik Larsson <patrik@tensorhead.com>"]
7
+ license = "MIT"
8
+ readme = "README.md"
9
+ homepage = "https://github.com/tensorhead/dithering"
10
+ repository = "https://github.com/tensorhead/dithering"
11
+ description = "Fast image dithering for Python: ordered (Bayer), error diffusion (Floyd-Steinberg & friends), random; palettes, linear-light and more."
12
+ keywords = ["image-processing", "dithering", "python", "halftone"]
13
+ categories = ["multimedia::images"]
14
+
15
+ [lib]
16
+ name = "dithering"
17
+ crate-type = ["cdylib", "rlib"]
18
+
19
+ [dependencies]
20
+ numpy = "0.29"
21
+ pyo3 = { version = "0.29", features = ["abi3-py39"] }
22
+
23
+ [profile.release]
24
+ lto = true
25
+ codegen-units = 1
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 BackyardML
3
+ Copyright (c) 2023 Tensorhead
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: dithering
3
+ Version: 0.2.0
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Rust
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: Implementation :: CPython
11
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
12
+ Classifier: Topic :: Multimedia :: Graphics
13
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: numpy>=1.21
16
+ Requires-Dist: pytest ; extra == 'test'
17
+ Requires-Dist: pillow ; extra == 'test'
18
+ Provides-Extra: test
19
+ License-File: LICENSE
20
+ Summary: Fast image dithering: ordered (Bayer), error diffusion (Floyd-Steinberg & friends), random; palettes, linear-light and more. Rust speed, one-call API.
21
+ Keywords: dithering,dither,image-processing,floyd-steinberg,bayer,ordered-dithering,error-diffusion,halftone,pixel-art
22
+ Home-Page: https://github.com/tensorhead/dithering
23
+ Author-email: Patrik Larsson <patrik@tensorhead.com>
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
26
+ Project-URL: Homepage, https://github.com/tensorhead/dithering
27
+ Project-URL: Issues, https://github.com/tensorhead/dithering/issues
28
+ Project-URL: Repository, https://github.com/tensorhead/dithering
29
+
30
+ <p align="center">
31
+ <br>
32
+ <img src="https://raw.githubusercontent.com/tensorhead/dithering/72ea732b8c7372af6f427a5f8244af158d80d208/resources/dithering.svg" width="600"/>
33
+ <br>
34
+ <p>
35
+
36
+ ![CI](https://github.com/tensorhead/dithering/actions/workflows/CI.yml/badge.svg)
37
+ [![PyPI](https://img.shields.io/pypi/v/dithering.svg)](https://pypi.org/project/dithering/)
38
+
39
+ Fast image dithering for Python, written in Rust. One call, sane defaults, every classic algorithm:
40
+
41
+ - **Error diffusion** — Floyd–Steinberg, Jarvis–Judice–Ninke, Stucki, Atkinson, Burkes, Sierra, Two-Row Sierra, Sierra Lite, False Floyd–Steinberg — with optional serpentine scanning.
42
+ - **Ordered (Bayer)** — 2×2 to 16×16 generated matrices, or bring your own threshold matrix.
43
+ - **Random (white noise)** — seedable.
44
+ - **Palettes** — dither straight to any color set (`["#0f380f", "#306230", ...]`), not just black & white.
45
+ - **Multi-level** — quantize to N gray/color levels per channel, not just 2.
46
+ - **Gamma-correct mode** — `linear=True` does the math in linear light and preserves perceived brightness (most libraries get this wrong).
47
+ - **Ergonomics** — numpy in → numpy out, PIL in → PIL out, alpha channels preserved, forgiving names (`"fs"`, `"Floyd-Steinberg"`, `"BAYER8X8"` all work, and `ordered_dither` takes the matrix size as an int), typed stubs, helpful error messages.
48
+ - **Fast** — the hot loops release the GIL; ~550 MP/s ordered and ~120 MP/s Floyd–Steinberg on an M-series laptop.
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install dithering
54
+ ```
55
+
56
+ Wheels are published for Linux (glibc + musl), macOS and Windows — one abi3 wheel per platform covers CPython 3.9 and everything newer. PyPy installs from the source distribution (needs a Rust toolchain).
57
+
58
+ ## Quickstart
59
+
60
+ ```python
61
+ from PIL import Image
62
+ import dithering
63
+
64
+ img = Image.open("photo.jpg")
65
+
66
+ # Classic black & white Floyd-Steinberg (the default):
67
+ bw = dithering.dither(img)
68
+
69
+ # Ordered dithering with an 8x8 Bayer matrix:
70
+ retro = dithering.dither(img, "bayer8x8")
71
+
72
+ # Dither to a Game Boy palette:
73
+ gameboy = dithering.dither(img, "atkinson", palette=["#0f380f", "#306230", "#8bac0f", "#9bbc0f"])
74
+
75
+ gameboy.save("gameboy.png")
76
+ ```
77
+
78
+ Everything also works directly on numpy arrays (2-D grayscale or H×W×C uint8), and returns arrays in that case:
79
+
80
+ ```python
81
+ import numpy as np
82
+ import dithering
83
+
84
+ arr = np.asarray(Image.open("photo.jpg"))
85
+ out = dithering.error_diffusion(arr, "stucki", serpentine=True, levels=4)
86
+ ```
87
+
88
+ ## Gallery
89
+
90
+ | Original | Floyd–Steinberg | Atkinson |
91
+ | --- | --- | --- |
92
+ | ![original](https://raw.githubusercontent.com/tensorhead/dithering/main/resources/examples/original_gray.png) | ![floyd_steinberg](https://raw.githubusercontent.com/tensorhead/dithering/main/resources/examples/floyd_steinberg.png) | ![atkinson](https://raw.githubusercontent.com/tensorhead/dithering/main/resources/examples/atkinson.png) |
93
+
94
+ | Bayer 2×2 | Bayer 8×8 | Random |
95
+ | --- | --- | --- |
96
+ | ![bayer2x2](https://raw.githubusercontent.com/tensorhead/dithering/main/resources/examples/bayer2x2.png) | ![bayer8x8](https://raw.githubusercontent.com/tensorhead/dithering/main/resources/examples/bayer8x8.png) | ![random](https://raw.githubusercontent.com/tensorhead/dithering/main/resources/examples/random.png) |
97
+
98
+ | RGB Floyd–Steinberg | Game Boy palette | 16-color palette, Bayer 8×8 |
99
+ | --- | --- | --- |
100
+ | ![rgb](https://raw.githubusercontent.com/tensorhead/dithering/main/resources/examples/rgb_fs.png) | ![gameboy](https://raw.githubusercontent.com/tensorhead/dithering/main/resources/examples/gameboy.png) | ![retro16](https://raw.githubusercontent.com/tensorhead/dithering/main/resources/examples/retro16.png) |
101
+
102
+ ## API
103
+
104
+ The one-stop entry point dispatches on the method name:
105
+
106
+ ```python
107
+ dithering.dither(image, method="floyd_steinberg", *, levels=2, palette=None,
108
+ serpentine=False, seed=None, strength=1.0, linear=False,
109
+ preserve_alpha=True)
110
+ ```
111
+
112
+ Or call the specific functions — same parameters, minus the ones that don't apply:
113
+
114
+ | Function | Methods |
115
+ | --- | --- |
116
+ | `error_diffusion(image, method, ...)` | `floyd_steinberg` (alias `fs`), `false_floyd_steinberg` (`ffs`), `jarvis_judice_ninke` (`jjn`), `stucki`, `atkinson`, `burkes`, `sierra` (`sierra3`), `sierra_two_row` (`sierra2`), `sierra_lite` |
117
+ | `ordered_dither(image, matrix, ...)` | `bayer2x2` … `bayer16x16`, the size as an int (`8`), or a custom 2-D numpy matrix (integer dtype = Bayer-style indices, float dtype = thresholds in `[0, 1]`) |
118
+ | `random_dither(image, ...)` | white noise; pass `seed=` for reproducible output |
119
+
120
+ All names are case-insensitive and `-`/`_` are interchangeable. `available_methods()` and `available_matrices()` list the valid names at runtime.
121
+
122
+ **Common parameters**
123
+
124
+ | Parameter | Default | Meaning |
125
+ | --- | --- | --- |
126
+ | `levels` | `2` | Output levels per channel. `2` = black & white; `4` gives 0/85/170/255, etc. |
127
+ | `palette` | `None` | Dither to these exact colors instead of gray levels. Hex strings (`"#fff"`, `"#0f380f"`) or `(r, g, b)` tuples. Needs an RGB(A) image. |
128
+ | `serpentine` | `False` | (Error diffusion) alternate the scan direction each row; reduces directional "worm" artifacts. |
129
+ | `strength` | `1.0` | `0.0` = plain quantization, `1.0` = full dithering. |
130
+ | `linear` | `False` | Do the math in linear light (gamma-correct). sRGB 50 % gray is only ~21 % linear light — with `linear=True` the dithered result keeps the *perceived* brightness of the original instead of coming out too bright. |
131
+ | `preserve_alpha` | `True` | For LA/RGBA inputs, pass the alpha channel through untouched instead of dithering it. |
132
+
133
+ **Input/output contract**
134
+
135
+ - Accepts uint8 numpy arrays — 2-D grayscale or 3-D (H, W, C) — and PIL images (any mode; exotic modes are converted to RGB first).
136
+ - Returns a new uint8 array of the same shape, or a PIL image if you passed one. The input is never modified.
137
+ - Float arrays are rejected with a hint (`(img * 255).clip(0, 255).astype(np.uint8)`), invalid names raise `ValueError` listing the valid ones.
138
+
139
+ ## Performance
140
+
141
+ 2048×2048 grayscale (4.2 MP) on an Apple M-series laptop:
142
+
143
+ | Operation | Time | Throughput |
144
+ | --- | --- | --- |
145
+ | `ordered_dither`, Bayer 8×8 | ~8 ms | ~550 MP/s |
146
+ | `random_dither` | ~7 ms | ~570 MP/s |
147
+ | `error_diffusion`, Floyd–Steinberg | ~34 ms | ~120 MP/s |
148
+ | `error_diffusion`, Stucki + serpentine | ~38 ms | ~110 MP/s |
149
+ | Pillow `convert("1")` (Floyd–Steinberg, the only dither it has) | ~18 ms | ~240 MP/s |
150
+
151
+ Pillow's single hard-coded binary-grayscale Floyd–Steinberg loop is still faster than our general engine — but it can't do other kernels, serpentine, levels, palettes, strength or linear light. Pure-Python libraries are typically 100–1000× slower. The hot loops release the GIL, so other Python threads keep running.
152
+
153
+ ## Correctness notes
154
+
155
+ - Bayer thresholds use the mean-preserving `(index + 0.5) / n²` convention: a 50 % gray dithers to exactly 50 % white pixels, solid black stays black, solid white stays white.
156
+ - Error-diffusion kernels are the canonical published coefficients; error falling off the image edge is discarded.
157
+ - Atkinson intentionally diffuses only 6/8 of the error — deep shadows and highlights clip to solid black/white. That's the classic Mac look, not a bug.
158
+ - The test suite (44 Rust + 82 Python tests) asserts mean preservation, palette exactness, alpha preservation and reproducibility for every algorithm.
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ cargo test # Rust unit tests
164
+ maturin develop --release # build into the active venv
165
+ pytest tests/ -v # Python end-to-end tests
166
+ ```
167
+
168
+ ## Contributing
169
+
170
+ Contributions are welcome! Fork the repository, create a branch from `main`, make your changes (please add tests), and open a pull request.
171
+
172
+ ## License
173
+
174
+ MIT — see [LICENSE](https://github.com/tensorhead/dithering/blob/main/LICENSE).
175
+
176
+ ## Contact
177
+
178
+ Questions, suggestions, feedback: open an issue on GitHub or email [patrik@tensorhead.com](mailto:patrik@tensorhead.com).
179
+