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.
- dithering-0.2.0/.github/workflows/CI.yml +256 -0
- dithering-0.2.0/Cargo.lock +218 -0
- dithering-0.2.0/Cargo.toml +25 -0
- {dithering-0.1.26 → dithering-0.2.0}/LICENSE +1 -1
- dithering-0.2.0/PKG-INFO +179 -0
- dithering-0.2.0/README.md +149 -0
- dithering-0.2.0/dithering.pyi +209 -0
- dithering-0.2.0/pyproject.toml +48 -0
- dithering-0.2.0/resources/BackyardML logo 2.png +0 -0
- dithering-0.2.0/resources/BackyardML logo 2.svg +2768 -0
- dithering-0.2.0/resources/BackyardML_logo.png +0 -0
- dithering-0.2.0/resources/examples/atkinson.png +0 -0
- dithering-0.2.0/resources/examples/bayer2x2.png +0 -0
- dithering-0.2.0/resources/examples/bayer8x8.png +0 -0
- dithering-0.2.0/resources/examples/floyd_steinberg.png +0 -0
- dithering-0.2.0/resources/examples/gameboy.png +0 -0
- dithering-0.2.0/resources/examples/levels4.png +0 -0
- dithering-0.2.0/resources/examples/original.png +0 -0
- dithering-0.2.0/resources/examples/original_gray.png +0 -0
- dithering-0.2.0/resources/examples/random.png +0 -0
- dithering-0.2.0/resources/examples/retro16.png +0 -0
- dithering-0.2.0/resources/examples/rgb_fs.png +0 -0
- dithering-0.2.0/resources/examples/stucki_serpentine.png +0 -0
- dithering-0.2.0/src/diffusion.rs +611 -0
- dithering-0.2.0/src/lib.rs +725 -0
- dithering-0.2.0/src/matrices.rs +209 -0
- dithering-0.2.0/src/ordered.rs +320 -0
- dithering-0.2.0/src/palette.rs +104 -0
- dithering-0.2.0/src/space.rs +162 -0
- dithering-0.2.0/tests/test_dithering.py +456 -0
- dithering-0.1.26/.github/workflows/CI.yml +0 -120
- dithering-0.1.26/Cargo.lock +0 -396
- dithering-0.1.26/Cargo.toml +0 -20
- dithering-0.1.26/PKG-INFO +0 -80
- dithering-0.1.26/README.md +0 -64
- dithering-0.1.26/dithering.pyi +0 -19
- dithering-0.1.26/pyproject.toml +0 -16
- dithering-0.1.26/src/lib.rs +0 -10
- dithering-0.1.26/src/methods/mod.rs +0 -3
- dithering-0.1.26/src/methods/ordered.rs +0 -128
- {dithering-0.1.26 → dithering-0.2.0}/.gitignore +0 -0
- {dithering-0.1.26 → dithering-0.2.0}/resources/BackyardML_logo.svg +0 -0
- {dithering-0.1.26 → dithering-0.2.0}/resources/dithered.png +0 -0
- {dithering-0.1.26 → dithering-0.2.0}/resources/dithering.png +0 -0
- {dithering-0.1.26 → dithering-0.2.0}/resources/dithering.svg +0 -0
- {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
|
dithering-0.2.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
37
|
+
[](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
|
+
|  |  |  |
|
|
93
|
+
|
|
94
|
+
| Bayer 2×2 | Bayer 8×8 | Random |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
|  |  |  |
|
|
97
|
+
|
|
98
|
+
| RGB Floyd–Steinberg | Game Boy palette | 16-color palette, Bayer 8×8 |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
|  |  |  |
|
|
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
|
+
|