dynimg 0.1.9__tar.gz → 0.1.10__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.
- {dynimg-0.1.9 → dynimg-0.1.10}/Cargo.lock +1 -1
- {dynimg-0.1.9 → dynimg-0.1.10}/Cargo.toml +2 -2
- dynimg-0.1.10/Makefile +34 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/PKG-INFO +1 -1
- {dynimg-0.1.9 → dynimg-0.1.10}/pyproject.toml +1 -1
- {dynimg-0.1.9 → dynimg-0.1.10}/python/dynimg/__init__.py +3 -1
- {dynimg-0.1.9 → dynimg-0.1.10}/src/lib.rs +5 -1
- {dynimg-0.1.9 → dynimg-0.1.10}/test_wheels/.gitignore +3 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/test_wheels/test_dynimg.py +66 -19
- {dynimg-0.1.9 → dynimg-0.1.10}/.claude/settings.local.json +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/.github/workflows/build-wheels.yml +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/.github/workflows/ci.yml +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/.github/workflows/release.yml +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/.gitignore +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/.pearls/CLAUDE.md +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/.pearls/issues.jsonl +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/CLAUDE.md +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/LICENSE +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/README.md +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/codebook.toml +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/assets/PlaywriteINGuides-Regular.ttf +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/assets/RobotoMono-Bold.ttf +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/assets/RobotoMono-Bold.woff2 +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/assets/logo.svg +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/assets/servo.css +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/assets/style.css +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/google-fonts.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/inline-only.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/local-assets.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/local-font-woff2.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/local-font.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/mixed-assets.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/og-image.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/quote.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/remote-image.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/servo.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/examples/social-card.html +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/python/dynimg/__init__.pyi +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/python/dynimg/py.typed +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/scripts/render-examples.sh +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/src/main.rs +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/src/python.rs +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/test_wheels/README.md +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/test_wheels/debug_parser.py +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/test_wheels/debug_verbose.py +0 -0
- {dynimg-0.1.9 → dynimg-0.1.10}/test_wheels/test_from_ci.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "dynimg"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.10"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "A fast library and CLI for rendering HTML/CSS to images"
|
|
6
6
|
license = "MIT"
|
|
@@ -65,4 +65,4 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
|
65
65
|
bytes = "1"
|
|
66
66
|
|
|
67
67
|
[profile.release]
|
|
68
|
-
lto =
|
|
68
|
+
lto = "thin"
|
dynimg-0.1.10/Makefile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.PHONY: install dev test lint fmt clean build
|
|
2
|
+
|
|
3
|
+
# Install Python package into local venv
|
|
4
|
+
install:
|
|
5
|
+
maturin develop --release
|
|
6
|
+
|
|
7
|
+
# Install in development mode (faster builds, debug symbols)
|
|
8
|
+
dev:
|
|
9
|
+
maturin develop
|
|
10
|
+
|
|
11
|
+
# Run tests
|
|
12
|
+
test: install
|
|
13
|
+
cargo test
|
|
14
|
+
python test_wheels/test_dynimg.py
|
|
15
|
+
|
|
16
|
+
# Run lints
|
|
17
|
+
lint:
|
|
18
|
+
cargo clippy -- -D warnings
|
|
19
|
+
cargo fmt -- --check
|
|
20
|
+
|
|
21
|
+
# Format code
|
|
22
|
+
fmt:
|
|
23
|
+
cargo fmt
|
|
24
|
+
|
|
25
|
+
# Clean build artifacts
|
|
26
|
+
clean:
|
|
27
|
+
cargo clean
|
|
28
|
+
rm -rf dist/
|
|
29
|
+
rm -rf target/
|
|
30
|
+
rm -rf *.egg-info/
|
|
31
|
+
|
|
32
|
+
# Build release wheels
|
|
33
|
+
build:
|
|
34
|
+
maturin build --release
|
|
@@ -574,6 +574,10 @@ fn write_webp_lossless(path: &Path, buffer: &[u8], width: u32, height: u32) -> R
|
|
|
574
574
|
|
|
575
575
|
fn encode_webp_lossless(buffer: &[u8], width: u32, height: u32) -> Vec<u8> {
|
|
576
576
|
let encoder = webp::Encoder::from_rgba(buffer, width, height);
|
|
577
|
-
let
|
|
577
|
+
let mut config = webp::WebPConfig::new().unwrap();
|
|
578
|
+
config.lossless = 1;
|
|
579
|
+
config.quality = 75.0;
|
|
580
|
+
config.method = 0; // 0=fastest, 6=slowest (default)
|
|
581
|
+
let webp_data = encoder.encode_advanced(&config).unwrap();
|
|
578
582
|
webp_data.to_vec()
|
|
579
583
|
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
"""Test script for dynimg wheels."""
|
|
3
3
|
|
|
4
4
|
import sys
|
|
5
|
+
import time
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RunTimer:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.start = time.perf_counter()
|
|
12
|
+
|
|
13
|
+
@contextmanager
|
|
14
|
+
def measure(self, name):
|
|
15
|
+
t0 = time.perf_counter()
|
|
16
|
+
yield
|
|
17
|
+
t1 = time.perf_counter()
|
|
18
|
+
print(f"[{t1 - self.start:.3f}s] {name} took {(t1 - t0) * 1000:.2f}ms")
|
|
5
19
|
|
|
6
20
|
|
|
7
21
|
def test_import():
|
|
@@ -17,11 +31,26 @@ def test_render_basic():
|
|
|
17
31
|
"""Test basic rendering."""
|
|
18
32
|
import dynimg
|
|
19
33
|
|
|
34
|
+
timer = RunTimer()
|
|
20
35
|
html = '<html><body style="background:blue;"><h1>Test</h1></body></html>'
|
|
21
|
-
|
|
36
|
+
|
|
37
|
+
with timer.measure("Render"):
|
|
38
|
+
img = dynimg.render(
|
|
39
|
+
html, dynimg.RenderOptions(width=100, height=100, scale=1.0)
|
|
40
|
+
)
|
|
22
41
|
|
|
23
42
|
assert img.width == 100, f"Expected width 100, got {img.width}"
|
|
24
43
|
assert img.height == 100, f"Expected height 100, got {img.height}"
|
|
44
|
+
|
|
45
|
+
with timer.measure("Save PNG"):
|
|
46
|
+
img.save_png("test_basic.png")
|
|
47
|
+
|
|
48
|
+
with timer.measure("Save JPEG"):
|
|
49
|
+
img.save_jpeg("test_basic.jpg")
|
|
50
|
+
|
|
51
|
+
with timer.measure("Save WebP"):
|
|
52
|
+
img.save_webp("test_basic.webp")
|
|
53
|
+
|
|
25
54
|
print(f"Basic render: {img.width}x{img.height}")
|
|
26
55
|
return True
|
|
27
56
|
|
|
@@ -30,6 +59,7 @@ def test_render_gradient():
|
|
|
30
59
|
"""Test gradient rendering."""
|
|
31
60
|
import dynimg
|
|
32
61
|
|
|
62
|
+
timer = RunTimer()
|
|
33
63
|
html = """
|
|
34
64
|
<html>
|
|
35
65
|
<body style="background: linear-gradient(135deg, #667eea, #764ba2);
|
|
@@ -42,10 +72,21 @@ def test_render_gradient():
|
|
|
42
72
|
</html>
|
|
43
73
|
"""
|
|
44
74
|
options = dynimg.RenderOptions(width=1200, height=630, scale=2.0)
|
|
45
|
-
|
|
75
|
+
|
|
76
|
+
with timer.measure("Render"):
|
|
77
|
+
img = dynimg.render(html, options)
|
|
46
78
|
|
|
47
79
|
assert img.width == 2400, f"Expected width 2400, got {img.width}"
|
|
48
80
|
assert img.height == 1260, f"Expected height 1260, got {img.height}"
|
|
81
|
+
with timer.measure("Save WebP"):
|
|
82
|
+
img.save_webp("test_gradient.webp")
|
|
83
|
+
|
|
84
|
+
with timer.measure("Save PNG"):
|
|
85
|
+
img.save_png("test_gradient.png")
|
|
86
|
+
|
|
87
|
+
with timer.measure("Save JPEG"):
|
|
88
|
+
img.save_jpeg("test_gradient.jpg")
|
|
89
|
+
|
|
49
90
|
print(f"Gradient render: {img.width}x{img.height}")
|
|
50
91
|
return True
|
|
51
92
|
|
|
@@ -56,32 +97,32 @@ def test_save_formats():
|
|
|
56
97
|
|
|
57
98
|
import dynimg
|
|
58
99
|
|
|
100
|
+
timer = RunTimer()
|
|
59
101
|
html = '<html><body style="background:red; width:50px; height:50px;"></body></html>'
|
|
60
|
-
|
|
102
|
+
with timer.measure("Render"):
|
|
103
|
+
img = dynimg.render(html, dynimg.RenderOptions(width=50, height=50, scale=1.0))
|
|
61
104
|
|
|
62
105
|
# Test PNG
|
|
63
|
-
|
|
106
|
+
with timer.measure("Save PNG"):
|
|
107
|
+
img.save_png("test_output.png")
|
|
64
108
|
assert os.path.exists("test_output.png"), "PNG file not created"
|
|
65
109
|
png_size = os.path.getsize("test_output.png")
|
|
66
110
|
print(f"PNG saved: {png_size} bytes")
|
|
67
111
|
|
|
68
112
|
# Test WebP
|
|
69
|
-
|
|
113
|
+
with timer.measure("Save WebP"):
|
|
114
|
+
img.save_webp("test_output.webp")
|
|
70
115
|
assert os.path.exists("test_output.webp"), "WebP file not created"
|
|
71
116
|
webp_size = os.path.getsize("test_output.webp")
|
|
72
117
|
print(f"WebP saved: {webp_size} bytes")
|
|
73
118
|
|
|
74
119
|
# Test JPEG
|
|
75
|
-
|
|
120
|
+
with timer.measure("Save JPEG"):
|
|
121
|
+
img.save_jpeg("test_output.jpg", quality=90)
|
|
76
122
|
assert os.path.exists("test_output.jpg"), "JPEG file not created"
|
|
77
123
|
jpeg_size = os.path.getsize("test_output.jpg")
|
|
78
124
|
print(f"JPEG saved: {jpeg_size} bytes")
|
|
79
125
|
|
|
80
|
-
# Cleanup
|
|
81
|
-
os.remove("test_output.png")
|
|
82
|
-
os.remove("test_output.webp")
|
|
83
|
-
os.remove("test_output.jpg")
|
|
84
|
-
|
|
85
126
|
return True
|
|
86
127
|
|
|
87
128
|
|
|
@@ -89,10 +130,13 @@ def test_to_bytes():
|
|
|
89
130
|
"""Test encoding to bytes."""
|
|
90
131
|
import dynimg
|
|
91
132
|
|
|
133
|
+
timer = RunTimer()
|
|
92
134
|
html = (
|
|
93
135
|
'<html><body style="background:green; width:50px; height:50px;"></body></html>'
|
|
94
136
|
)
|
|
95
|
-
|
|
137
|
+
|
|
138
|
+
with timer.measure("Render"):
|
|
139
|
+
img = dynimg.render(html, dynimg.RenderOptions(width=50, height=50, scale=1.0))
|
|
96
140
|
|
|
97
141
|
png_bytes = img.to_png()
|
|
98
142
|
assert len(png_bytes) > 0, "PNG bytes empty"
|
|
@@ -118,21 +162,24 @@ def test_render_to_file():
|
|
|
118
162
|
|
|
119
163
|
import dynimg
|
|
120
164
|
|
|
165
|
+
timer = RunTimer()
|
|
121
166
|
html = (
|
|
122
167
|
'<html><body style="background:yellow; width:50px; height:50px;"></body></html>'
|
|
123
168
|
)
|
|
124
169
|
|
|
125
|
-
|
|
170
|
+
with timer.measure("render_to_file PNG"):
|
|
171
|
+
dynimg.render_to_file(html, "test_direct.png")
|
|
126
172
|
assert os.path.exists("test_direct.png"), "Direct PNG not created"
|
|
127
173
|
print(f"render_to_file PNG: {os.path.getsize('test_direct.png')} bytes")
|
|
128
174
|
os.remove("test_direct.png")
|
|
129
175
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
176
|
+
with timer.measure("render_to_file WebP"):
|
|
177
|
+
dynimg.render_to_file(
|
|
178
|
+
html,
|
|
179
|
+
"test_direct.webp",
|
|
180
|
+
options=dynimg.RenderOptions(width=100, height=100, scale=1.0),
|
|
181
|
+
quality=85,
|
|
182
|
+
)
|
|
136
183
|
assert os.path.exists("test_direct.webp"), "Direct WebP not created"
|
|
137
184
|
print(f"render_to_file WebP: {os.path.getsize('test_direct.webp')} bytes")
|
|
138
185
|
os.remove("test_direct.webp")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|