arthash 0.1.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.
- arthash-0.1.0/PKG-INFO +66 -0
- arthash-0.1.0/README.md +45 -0
- arthash-0.1.0/arthash-py/Cargo.lock +237 -0
- arthash-0.1.0/arthash-py/Cargo.toml +21 -0
- arthash-0.1.0/arthash-py/README.md +45 -0
- arthash-0.1.0/arthash-py/bench_binding.py +126 -0
- arthash-0.1.0/arthash-py/src/lib.rs +235 -0
- arthash-0.1.0/arthash-py/tests/__init__.py +0 -0
- arthash-0.1.0/arthash-py/tests/conftest.py +36 -0
- arthash-0.1.0/arthash-py/tests/generate_vectors.py +205 -0
- arthash-0.1.0/arthash-py/tests/test_codec.py +143 -0
- arthash-0.1.0/arthash-py/tests/test_search_options.py +105 -0
- arthash-0.1.0/arthash-py/tests/test_shape_roundtrip.py +113 -0
- arthash-0.1.0/arthash-py/tests/test_svg.py +164 -0
- arthash-0.1.0/arthash-py/tests/test_v4_roundtrip.py +73 -0
- arthash-0.1.0/arthash-py/tests/test_vectors.py +93 -0
- arthash-0.1.0/arthash-py/uv.lock +8 -0
- arthash-0.1.0/arthash-rs/Cargo.lock +271 -0
- arthash-0.1.0/arthash-rs/Cargo.toml +38 -0
- arthash-0.1.0/arthash-rs/README.md +81 -0
- arthash-0.1.0/arthash-rs/examples/bench.rs +156 -0
- arthash-0.1.0/arthash-rs/examples/bench_hillclimb.rs +280 -0
- arthash-0.1.0/arthash-rs/src/api.rs +240 -0
- arthash-0.1.0/arthash-rs/src/bitio.rs +86 -0
- arthash-0.1.0/arthash-rs/src/codec.rs +158 -0
- arthash-0.1.0/arthash-rs/src/colorspace.rs +27 -0
- arthash-0.1.0/arthash-rs/src/dct/colorspace.rs +115 -0
- arthash-0.1.0/arthash-rs/src/dct/decode.rs +276 -0
- arthash-0.1.0/arthash-rs/src/dct/encode.rs +367 -0
- arthash-0.1.0/arthash-rs/src/dct/mod.rs +8 -0
- arthash-0.1.0/arthash-rs/src/lib.rs +44 -0
- arthash-0.1.0/arthash-rs/src/shape/circle.rs +423 -0
- arthash-0.1.0/arthash-rs/src/shape/integral.rs +611 -0
- arthash-0.1.0/arthash-rs/src/shape/integral2d.rs +280 -0
- arthash-0.1.0/arthash-rs/src/shape/mod.rs +25 -0
- arthash-0.1.0/arthash-rs/src/shape/options.rs +67 -0
- arthash-0.1.0/arthash-rs/src/shape/pixel.rs +223 -0
- arthash-0.1.0/arthash-rs/src/shape/quant.rs +191 -0
- arthash-0.1.0/arthash-rs/src/shape/raster.rs +536 -0
- arthash-0.1.0/arthash-rs/src/shape/rect.rs +288 -0
- arthash-0.1.0/arthash-rs/src/shape/residual.rs +140 -0
- arthash-0.1.0/arthash-rs/src/shape/rng.rs +111 -0
- arthash-0.1.0/arthash-rs/src/shape/rotrect.rs +313 -0
- arthash-0.1.0/arthash-rs/src/shape/square.rs +271 -0
- arthash-0.1.0/arthash-rs/src/shape/svg.rs +505 -0
- arthash-0.1.0/arthash-rs/src/shape/triangle.rs +507 -0
- arthash-0.1.0/arthash-rs/tests/vectors.rs +337 -0
- arthash-0.1.0/pyproject.toml +35 -0
- arthash-0.1.0/python/arthash/__about__.py +1 -0
- arthash-0.1.0/python/arthash/__init__.py +43 -0
- arthash-0.1.0/python/arthash/_api.py +153 -0
- arthash-0.1.0/python/arthash/_codec.py +168 -0
- arthash-0.1.0/python/arthash/_colorspace.py +18 -0
- arthash-0.1.0/python/arthash/_search.py +38 -0
- arthash-0.1.0/python/arthash/palettes.py +131 -0
arthash-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arthash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Classifier: Programming Language :: Rust
|
|
10
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
11
|
+
Requires-Dist: numpy>=2.0
|
|
12
|
+
Requires-Dist: pillow>=11.0
|
|
13
|
+
Summary: Placeholder-image hash family — DCT / CIRCLE / TRIANGLE / PIXEL modes share a unified Codec API. PyO3 binding to arthash-rs.
|
|
14
|
+
Keywords: thumbhash,placeholder,image,hash,perceptual,sqip,primitive
|
|
15
|
+
Author-email: Jianqi Pan <jannchie@gmail.com>
|
|
16
|
+
License: MIT
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
19
|
+
Project-URL: Source, https://github.com/Jannchie/arthash
|
|
20
|
+
|
|
21
|
+
# arthash
|
|
22
|
+
|
|
23
|
+
Placeholder-image hash family. Four modes share a unified Codec API:
|
|
24
|
+
|
|
25
|
+
| Shape | Bytes (typical) | Notes |
|
|
26
|
+
|----------|-----------------|-------|
|
|
27
|
+
| DCT | ~21 B | ThumbHash V4 derivative. Default. |
|
|
28
|
+
| CIRCLE | varies | SQIP-style overlapping circles. |
|
|
29
|
+
| TRIANGLE | varies | fogleman/primitive-style triangle mosaic. |
|
|
30
|
+
| PIXEL | varies | Retro-palette pixel mosaic. |
|
|
31
|
+
|
|
32
|
+
The implementation is a thin Python wrapper around the `arthash-rs` Rust crate
|
|
33
|
+
exposed via PyO3 — encode/decode/SVG all run in native code.
|
|
34
|
+
|
|
35
|
+
## Install (from source)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
maturin develop --uv -m packages/arthash-py/Cargo.toml
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from arthash import encode, decode, to_svg, Codec, ShapeType
|
|
45
|
+
|
|
46
|
+
# DCT (default)
|
|
47
|
+
h = encode("photo.jpg")
|
|
48
|
+
w, hh, rgba = decode(h, base_size=256)
|
|
49
|
+
|
|
50
|
+
# Shape modes
|
|
51
|
+
codec = Codec(shape=ShapeType.CIRCLE, n_shapes=12)
|
|
52
|
+
h = encode("photo.jpg", codec, seed=0)
|
|
53
|
+
w, hh, rgb = decode(h, codec, base_size=256) # (h, w, 3) RGB ndarray
|
|
54
|
+
svg = to_svg(h, codec, base_size=256, blur=8.0) # circle/triangle only
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Layout
|
|
58
|
+
|
|
59
|
+
- `python/arthash/` — public Python API (`Codec`, `ShapeType`, `SearchOptions`,
|
|
60
|
+
`palettes`, `encode`, `decode`, `to_svg`).
|
|
61
|
+
- `src/lib.rs` — PyO3 binding to the `arthash` Rust crate. Compiled into
|
|
62
|
+
`arthash._native`.
|
|
63
|
+
- `tests/` — pytest suite covering codec validation, V4 round-trip, shape
|
|
64
|
+
round-trip, SVG generation, search-options, and the cross-language test
|
|
65
|
+
vectors at `docs/test-vectors/vectors.json`.
|
|
66
|
+
|
arthash-0.1.0/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# arthash
|
|
2
|
+
|
|
3
|
+
Placeholder-image hash family. Four modes share a unified Codec API:
|
|
4
|
+
|
|
5
|
+
| Shape | Bytes (typical) | Notes |
|
|
6
|
+
|----------|-----------------|-------|
|
|
7
|
+
| DCT | ~21 B | ThumbHash V4 derivative. Default. |
|
|
8
|
+
| CIRCLE | varies | SQIP-style overlapping circles. |
|
|
9
|
+
| TRIANGLE | varies | fogleman/primitive-style triangle mosaic. |
|
|
10
|
+
| PIXEL | varies | Retro-palette pixel mosaic. |
|
|
11
|
+
|
|
12
|
+
The implementation is a thin Python wrapper around the `arthash-rs` Rust crate
|
|
13
|
+
exposed via PyO3 — encode/decode/SVG all run in native code.
|
|
14
|
+
|
|
15
|
+
## Install (from source)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
maturin develop --uv -m packages/arthash-py/Cargo.toml
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from arthash import encode, decode, to_svg, Codec, ShapeType
|
|
25
|
+
|
|
26
|
+
# DCT (default)
|
|
27
|
+
h = encode("photo.jpg")
|
|
28
|
+
w, hh, rgba = decode(h, base_size=256)
|
|
29
|
+
|
|
30
|
+
# Shape modes
|
|
31
|
+
codec = Codec(shape=ShapeType.CIRCLE, n_shapes=12)
|
|
32
|
+
h = encode("photo.jpg", codec, seed=0)
|
|
33
|
+
w, hh, rgb = decode(h, codec, base_size=256) # (h, w, 3) RGB ndarray
|
|
34
|
+
svg = to_svg(h, codec, base_size=256, blur=8.0) # circle/triangle only
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Layout
|
|
38
|
+
|
|
39
|
+
- `python/arthash/` — public Python API (`Codec`, `ShapeType`, `SearchOptions`,
|
|
40
|
+
`palettes`, `encode`, `decode`, `to_svg`).
|
|
41
|
+
- `src/lib.rs` — PyO3 binding to the `arthash` Rust crate. Compiled into
|
|
42
|
+
`arthash._native`.
|
|
43
|
+
- `tests/` — pytest suite covering codec validation, V4 round-trip, shape
|
|
44
|
+
round-trip, SVG generation, search-options, and the cross-language test
|
|
45
|
+
vectors at `docs/test-vectors/vectors.json`.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "arthash"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"matrixmultiply",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[[package]]
|
|
13
|
+
name = "arthash-py"
|
|
14
|
+
version = "0.1.0"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"arthash",
|
|
17
|
+
"numpy",
|
|
18
|
+
"pyo3",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[[package]]
|
|
22
|
+
name = "autocfg"
|
|
23
|
+
version = "1.5.0"
|
|
24
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
25
|
+
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|
26
|
+
|
|
27
|
+
[[package]]
|
|
28
|
+
name = "heck"
|
|
29
|
+
version = "0.5.0"
|
|
30
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
31
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
32
|
+
|
|
33
|
+
[[package]]
|
|
34
|
+
name = "libc"
|
|
35
|
+
version = "0.2.186"
|
|
36
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
37
|
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
38
|
+
|
|
39
|
+
[[package]]
|
|
40
|
+
name = "matrixmultiply"
|
|
41
|
+
version = "0.3.10"
|
|
42
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
43
|
+
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
|
|
44
|
+
dependencies = [
|
|
45
|
+
"autocfg",
|
|
46
|
+
"rawpointer",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[[package]]
|
|
50
|
+
name = "ndarray"
|
|
51
|
+
version = "0.17.2"
|
|
52
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
53
|
+
checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
|
|
54
|
+
dependencies = [
|
|
55
|
+
"matrixmultiply",
|
|
56
|
+
"num-complex",
|
|
57
|
+
"num-integer",
|
|
58
|
+
"num-traits",
|
|
59
|
+
"portable-atomic",
|
|
60
|
+
"portable-atomic-util",
|
|
61
|
+
"rawpointer",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[[package]]
|
|
65
|
+
name = "num-complex"
|
|
66
|
+
version = "0.4.6"
|
|
67
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
68
|
+
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
|
69
|
+
dependencies = [
|
|
70
|
+
"num-traits",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[[package]]
|
|
74
|
+
name = "num-integer"
|
|
75
|
+
version = "0.1.46"
|
|
76
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
77
|
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
|
78
|
+
dependencies = [
|
|
79
|
+
"num-traits",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
[[package]]
|
|
83
|
+
name = "num-traits"
|
|
84
|
+
version = "0.2.19"
|
|
85
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
86
|
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
87
|
+
dependencies = [
|
|
88
|
+
"autocfg",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
[[package]]
|
|
92
|
+
name = "numpy"
|
|
93
|
+
version = "0.28.0"
|
|
94
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
95
|
+
checksum = "778da78c64ddc928ebf5ad9df5edf0789410ff3bdbf3619aed51cd789a6af1e2"
|
|
96
|
+
dependencies = [
|
|
97
|
+
"libc",
|
|
98
|
+
"ndarray",
|
|
99
|
+
"num-complex",
|
|
100
|
+
"num-integer",
|
|
101
|
+
"num-traits",
|
|
102
|
+
"pyo3",
|
|
103
|
+
"pyo3-build-config",
|
|
104
|
+
"rustc-hash",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
[[package]]
|
|
108
|
+
name = "once_cell"
|
|
109
|
+
version = "1.21.4"
|
|
110
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
111
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
112
|
+
|
|
113
|
+
[[package]]
|
|
114
|
+
name = "portable-atomic"
|
|
115
|
+
version = "1.13.1"
|
|
116
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
117
|
+
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|
118
|
+
|
|
119
|
+
[[package]]
|
|
120
|
+
name = "portable-atomic-util"
|
|
121
|
+
version = "0.2.7"
|
|
122
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
123
|
+
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
|
124
|
+
dependencies = [
|
|
125
|
+
"portable-atomic",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
[[package]]
|
|
129
|
+
name = "proc-macro2"
|
|
130
|
+
version = "1.0.106"
|
|
131
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
132
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
133
|
+
dependencies = [
|
|
134
|
+
"unicode-ident",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
[[package]]
|
|
138
|
+
name = "pyo3"
|
|
139
|
+
version = "0.28.3"
|
|
140
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
141
|
+
checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
|
|
142
|
+
dependencies = [
|
|
143
|
+
"libc",
|
|
144
|
+
"once_cell",
|
|
145
|
+
"portable-atomic",
|
|
146
|
+
"pyo3-build-config",
|
|
147
|
+
"pyo3-ffi",
|
|
148
|
+
"pyo3-macros",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
[[package]]
|
|
152
|
+
name = "pyo3-build-config"
|
|
153
|
+
version = "0.28.3"
|
|
154
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
155
|
+
checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
|
|
156
|
+
dependencies = [
|
|
157
|
+
"target-lexicon",
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
[[package]]
|
|
161
|
+
name = "pyo3-ffi"
|
|
162
|
+
version = "0.28.3"
|
|
163
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
164
|
+
checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
|
|
165
|
+
dependencies = [
|
|
166
|
+
"libc",
|
|
167
|
+
"pyo3-build-config",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
[[package]]
|
|
171
|
+
name = "pyo3-macros"
|
|
172
|
+
version = "0.28.3"
|
|
173
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
174
|
+
checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
|
|
175
|
+
dependencies = [
|
|
176
|
+
"proc-macro2",
|
|
177
|
+
"pyo3-macros-backend",
|
|
178
|
+
"quote",
|
|
179
|
+
"syn",
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
[[package]]
|
|
183
|
+
name = "pyo3-macros-backend"
|
|
184
|
+
version = "0.28.3"
|
|
185
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
186
|
+
checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
|
|
187
|
+
dependencies = [
|
|
188
|
+
"heck",
|
|
189
|
+
"proc-macro2",
|
|
190
|
+
"pyo3-build-config",
|
|
191
|
+
"quote",
|
|
192
|
+
"syn",
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
[[package]]
|
|
196
|
+
name = "quote"
|
|
197
|
+
version = "1.0.45"
|
|
198
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
199
|
+
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
|
200
|
+
dependencies = [
|
|
201
|
+
"proc-macro2",
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
[[package]]
|
|
205
|
+
name = "rawpointer"
|
|
206
|
+
version = "0.2.1"
|
|
207
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
208
|
+
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
|
209
|
+
|
|
210
|
+
[[package]]
|
|
211
|
+
name = "rustc-hash"
|
|
212
|
+
version = "2.1.2"
|
|
213
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
214
|
+
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
|
215
|
+
|
|
216
|
+
[[package]]
|
|
217
|
+
name = "syn"
|
|
218
|
+
version = "2.0.117"
|
|
219
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
220
|
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
221
|
+
dependencies = [
|
|
222
|
+
"proc-macro2",
|
|
223
|
+
"quote",
|
|
224
|
+
"unicode-ident",
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
[[package]]
|
|
228
|
+
name = "target-lexicon"
|
|
229
|
+
version = "0.13.5"
|
|
230
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
231
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
232
|
+
|
|
233
|
+
[[package]]
|
|
234
|
+
name = "unicode-ident"
|
|
235
|
+
version = "1.0.24"
|
|
236
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
237
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "arthash-py"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
description = "Python bindings (PyO3) for arthash-rs."
|
|
7
|
+
publish = false
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
|
|
10
|
+
[lib]
|
|
11
|
+
name = "_native"
|
|
12
|
+
crate-type = ["cdylib"]
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
arthash = { path = "../arthash-rs" }
|
|
16
|
+
pyo3 = { version = "0.28", features = ["extension-module", "abi3-py39"] }
|
|
17
|
+
numpy = "0.28"
|
|
18
|
+
|
|
19
|
+
[profile.release]
|
|
20
|
+
lto = "thin"
|
|
21
|
+
codegen-units = 1
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# arthash
|
|
2
|
+
|
|
3
|
+
Placeholder-image hash family. Four modes share a unified Codec API:
|
|
4
|
+
|
|
5
|
+
| Shape | Bytes (typical) | Notes |
|
|
6
|
+
|----------|-----------------|-------|
|
|
7
|
+
| DCT | ~21 B | ThumbHash V4 derivative. Default. |
|
|
8
|
+
| CIRCLE | varies | SQIP-style overlapping circles. |
|
|
9
|
+
| TRIANGLE | varies | fogleman/primitive-style triangle mosaic. |
|
|
10
|
+
| PIXEL | varies | Retro-palette pixel mosaic. |
|
|
11
|
+
|
|
12
|
+
The implementation is a thin Python wrapper around the `arthash-rs` Rust crate
|
|
13
|
+
exposed via PyO3 — encode/decode/SVG all run in native code.
|
|
14
|
+
|
|
15
|
+
## Install (from source)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
maturin develop --uv -m packages/arthash-py/Cargo.toml
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from arthash import encode, decode, to_svg, Codec, ShapeType
|
|
25
|
+
|
|
26
|
+
# DCT (default)
|
|
27
|
+
h = encode("photo.jpg")
|
|
28
|
+
w, hh, rgba = decode(h, base_size=256)
|
|
29
|
+
|
|
30
|
+
# Shape modes
|
|
31
|
+
codec = Codec(shape=ShapeType.CIRCLE, n_shapes=12)
|
|
32
|
+
h = encode("photo.jpg", codec, seed=0)
|
|
33
|
+
w, hh, rgb = decode(h, codec, base_size=256) # (h, w, 3) RGB ndarray
|
|
34
|
+
svg = to_svg(h, codec, base_size=256, blur=8.0) # circle/triangle only
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Layout
|
|
38
|
+
|
|
39
|
+
- `python/arthash/` — public Python API (`Codec`, `ShapeType`, `SearchOptions`,
|
|
40
|
+
`palettes`, `encode`, `decode`, `to_svg`).
|
|
41
|
+
- `src/lib.rs` — PyO3 binding to the `arthash` Rust crate. Compiled into
|
|
42
|
+
`arthash._native`.
|
|
43
|
+
- `tests/` — pytest suite covering codec validation, V4 round-trip, shape
|
|
44
|
+
round-trip, SVG generation, search-options, and the cross-language test
|
|
45
|
+
vectors at `docs/test-vectors/vectors.json`.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Benchmark the raw `arthash._native` extension surface — same workload as
|
|
2
|
+
scripts/bench_py.py but calling the FFI directly (no PIL, no Codec dataclass)
|
|
3
|
+
so we can isolate FFI overhead from the Python wrapper's preprocessing.
|
|
4
|
+
|
|
5
|
+
Run (from repo root):
|
|
6
|
+
|
|
7
|
+
uv run python packages/arthash-py/bench_binding.py > docs/benchmarks/binding.ndjson
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import statistics
|
|
13
|
+
import time
|
|
14
|
+
from typing import Callable
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from arthash import _native as m
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def gradient_rgb(w: int, h: int) -> np.ndarray:
|
|
21
|
+
xs = np.arange(w, dtype=np.float32)
|
|
22
|
+
ys = np.arange(h, dtype=np.float32)
|
|
23
|
+
rx = np.round(xs * 255.0 / max(w - 1, 1)).astype(np.uint8)
|
|
24
|
+
gy = np.round(ys * 255.0 / max(h - 1, 1)).astype(np.uint8)
|
|
25
|
+
img = np.zeros((h, w, 3), dtype=np.uint8)
|
|
26
|
+
img[..., 0] = np.broadcast_to(rx, (h, w))
|
|
27
|
+
img[..., 1] = np.broadcast_to(gy[:, None], (h, w))
|
|
28
|
+
xy = (xs[None, :] + ys[:, None]) * 0.3
|
|
29
|
+
img[..., 2] = np.clip(xy, 0, 255).astype(np.uint8)
|
|
30
|
+
return img
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def measure(fn: Callable[[], None], warmup: int, iters: int) -> dict:
|
|
34
|
+
for _ in range(warmup):
|
|
35
|
+
fn()
|
|
36
|
+
samples = []
|
|
37
|
+
for _ in range(iters):
|
|
38
|
+
t0 = time.perf_counter_ns()
|
|
39
|
+
fn()
|
|
40
|
+
samples.append((time.perf_counter_ns() - t0) / 1000.0)
|
|
41
|
+
samples.sort()
|
|
42
|
+
return {
|
|
43
|
+
"median_us": statistics.median(samples),
|
|
44
|
+
"p95_us": samples[int(len(samples) * 0.95)],
|
|
45
|
+
"min_us": samples[0],
|
|
46
|
+
"iters": iters,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def report(mode: str, op: str, w: int, h: int, s: dict, extra: dict | None = None) -> None:
|
|
51
|
+
mpix_s = (w * h) / s["median_us"]
|
|
52
|
+
out = {
|
|
53
|
+
"impl": "pyo3",
|
|
54
|
+
"mode": mode,
|
|
55
|
+
"op": op,
|
|
56
|
+
"w": w,
|
|
57
|
+
"h": h,
|
|
58
|
+
"median_us": round(s["median_us"], 2),
|
|
59
|
+
"p95_us": round(s["p95_us"], 2),
|
|
60
|
+
"min_us": round(s["min_us"], 2),
|
|
61
|
+
"iters": s["iters"],
|
|
62
|
+
"mpix_per_s": round(mpix_s, 3),
|
|
63
|
+
}
|
|
64
|
+
if extra:
|
|
65
|
+
out.update(extra)
|
|
66
|
+
print(json.dumps(out), flush=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main() -> None:
|
|
70
|
+
# DCT
|
|
71
|
+
w, h = 100, 100
|
|
72
|
+
img = gradient_rgb(w, h)
|
|
73
|
+
rgb_bytes = img.tobytes()
|
|
74
|
+
|
|
75
|
+
hash_holder = {"b": b""}
|
|
76
|
+
|
|
77
|
+
def enc():
|
|
78
|
+
hash_holder["b"] = m.encode_rgb(rgb_bytes, w, h)
|
|
79
|
+
|
|
80
|
+
s = measure(enc, 30, 200)
|
|
81
|
+
report("dct", "encode", w, h, s, {"hash_bytes": len(hash_holder["b"])})
|
|
82
|
+
|
|
83
|
+
hb = hash_holder["b"]
|
|
84
|
+
|
|
85
|
+
def dec():
|
|
86
|
+
m.decode(hb, base_size=256)
|
|
87
|
+
|
|
88
|
+
s = measure(dec, 10, 50)
|
|
89
|
+
report("dct", "decode", w, h, s)
|
|
90
|
+
|
|
91
|
+
# Shape modes
|
|
92
|
+
shapes = [
|
|
93
|
+
("circle", "circle", 12),
|
|
94
|
+
("triangle", "triangle", 12),
|
|
95
|
+
("pixel", "pixel", 12),
|
|
96
|
+
]
|
|
97
|
+
for name, shape, n_shapes in shapes:
|
|
98
|
+
w, h = 48, 48
|
|
99
|
+
img = gradient_rgb(w, h)
|
|
100
|
+
rgb_bytes = img.tobytes()
|
|
101
|
+
codec = {"shape": shape, "n_shapes": n_shapes}
|
|
102
|
+
|
|
103
|
+
if shape == "pixel":
|
|
104
|
+
warmup, iters = 10, 100
|
|
105
|
+
else:
|
|
106
|
+
warmup, iters = 3, 15
|
|
107
|
+
|
|
108
|
+
hash_holder = {"b": b""}
|
|
109
|
+
|
|
110
|
+
def enc():
|
|
111
|
+
hash_holder["b"] = m.encode_rgb(rgb_bytes, w, h, codec)
|
|
112
|
+
|
|
113
|
+
s = measure(enc, warmup, iters)
|
|
114
|
+
report(name, "encode", w, h, s, {"hash_bytes": len(hash_holder["b"])})
|
|
115
|
+
|
|
116
|
+
hb = hash_holder["b"]
|
|
117
|
+
|
|
118
|
+
def dec():
|
|
119
|
+
m.decode(hb, codec, base_size=256)
|
|
120
|
+
|
|
121
|
+
s = measure(dec, 5, 30)
|
|
122
|
+
report(name, "decode", w, h, s)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
main()
|