webp-convert 1.0.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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include pyproject.toml
3
+ recursive-include webp_convert *.py
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: webp-convert
3
+ Version: 1.0.0
4
+ Summary: Automatically convert PNG/JPG images to WebP with responsive sizes. CLI, file watcher, and Python API.
5
+ License: MIT
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: Pillow>=10.0.0
9
+ Requires-Dist: watchdog>=4.0.0
10
+ Requires-Dist: click>=8.1.0
11
+ Requires-Dist: rich>=13.0.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: pytest-asyncio; extra == "dev"
15
+
16
+ # webp-convert (Python)
17
+
18
+ > Automatically convert PNG/JPG/GIF/TIFF images to WebP with responsive size variants.
19
+ > Zero-config CLI, file watcher, and programmatic Python API.
20
+
21
+ ---
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip3 install webp-convert
27
+ ```
28
+
29
+ Requires Python 3.8+. Uses [Pillow](https://pillow.readthedocs.io/) — no external binaries required.
30
+
31
+ ---
32
+
33
+ ## Quick start
34
+
35
+ ```bash
36
+ # Convert everything in assets/images (default)
37
+ webp-convert
38
+
39
+ # Custom dir + quality
40
+ webp-convert src/img -q 90
41
+
42
+ # Watch mode — auto-converts on file change
43
+ webp-convert src/img --watch
44
+
45
+ # Custom responsive sizes
46
+ webp-convert src/img --sizes 1200,800,400
47
+
48
+ # Write outputs to a separate directory
49
+ webp-convert src/img -o dist/img
50
+ ```
51
+
52
+ ---
53
+
54
+ ## CLI reference
55
+
56
+ ```
57
+ Usage: webp-convert [INPUT_DIR] [OPTIONS]
58
+
59
+ Convert PNG/JPG images to WebP with responsive size variants.
60
+
61
+ Options:
62
+ -o, --output TEXT Output directory (default: same as source)
63
+ -q, --quality INT WebP quality 0-100 [default: 80]
64
+ --lossless Lossless encoding
65
+ --sizes W,W,... Responsive widths e.g. 1200,800,400 (0=full)
66
+ --no-full Skip full-size, only responsive variants
67
+ -r, --recursive Recurse into subdirectories
68
+ --no-overwrite Skip files that already have a .webp counterpart
69
+ -w, --watch Watch mode
70
+ --silent Suppress all output
71
+ -h, --help Show this message and exit
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Programmatic API
77
+
78
+ ```python
79
+ from webp_convert import convert, watch, convert_file, ConvertConfig, SizeSpec
80
+
81
+ # --- Convert a directory (keyword args shortcut) ---
82
+ results = convert(input="src/images", quality=85)
83
+
84
+ # --- Convert with a config object ---
85
+ cfg = ConvertConfig(
86
+ input="src/images",
87
+ output="public/images", # omit to write next to originals
88
+ quality=85,
89
+ lossless=False,
90
+ recursive=True,
91
+ overwrite=True,
92
+ sizes=[
93
+ SizeSpec(width=0, suffix=""), # full size
94
+ SizeSpec(width=1200, suffix="-1200"),
95
+ SizeSpec(width=800, suffix="-800"),
96
+ SizeSpec(width=400, suffix="-400"),
97
+ ],
98
+ on_file=lambda r: print(f"{r.src} → {r.dest} ({r.size_bytes} bytes)"),
99
+ )
100
+ results = convert(cfg)
101
+
102
+ # --- Convert a single file ---
103
+ results = convert_file("hero.png", cfg)
104
+
105
+ # --- Watch mode (context manager) ---
106
+ import time
107
+ watcher = watch(ConvertConfig(input="src/images")).start()
108
+ try:
109
+ while True:
110
+ time.sleep(1)
111
+ except KeyboardInterrupt:
112
+ watcher.stop()
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Output structure
118
+
119
+ Given `hero.jpg` in `src/images/`:
120
+
121
+ ```
122
+ src/images/
123
+ ├── hero.jpg (original, kept by default)
124
+ ├── hero.webp (full size)
125
+ ├── hero-800.webp (800px wide, aspect-ratio preserved)
126
+ └── hero-400.webp (400px wide)
127
+ ```
128
+
129
+ ---
130
+
131
+ ## HTML usage
132
+
133
+ ```html
134
+ <picture>
135
+ <source
136
+ type="image/webp"
137
+ srcset="hero-400.webp 400w, hero-800.webp 800w, hero.webp 1600w"
138
+ sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1600px"
139
+ />
140
+ <img src="hero.jpg" alt="Hero image" />
141
+ </picture>
142
+ ```
143
+
144
+ ---
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,133 @@
1
+ # webp-convert (Python)
2
+
3
+ > Automatically convert PNG/JPG/GIF/TIFF images to WebP with responsive size variants.
4
+ > Zero-config CLI, file watcher, and programmatic Python API.
5
+
6
+ ---
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip3 install webp-convert
12
+ ```
13
+
14
+ Requires Python 3.8+. Uses [Pillow](https://pillow.readthedocs.io/) — no external binaries required.
15
+
16
+ ---
17
+
18
+ ## Quick start
19
+
20
+ ```bash
21
+ # Convert everything in assets/images (default)
22
+ webp-convert
23
+
24
+ # Custom dir + quality
25
+ webp-convert src/img -q 90
26
+
27
+ # Watch mode — auto-converts on file change
28
+ webp-convert src/img --watch
29
+
30
+ # Custom responsive sizes
31
+ webp-convert src/img --sizes 1200,800,400
32
+
33
+ # Write outputs to a separate directory
34
+ webp-convert src/img -o dist/img
35
+ ```
36
+
37
+ ---
38
+
39
+ ## CLI reference
40
+
41
+ ```
42
+ Usage: webp-convert [INPUT_DIR] [OPTIONS]
43
+
44
+ Convert PNG/JPG images to WebP with responsive size variants.
45
+
46
+ Options:
47
+ -o, --output TEXT Output directory (default: same as source)
48
+ -q, --quality INT WebP quality 0-100 [default: 80]
49
+ --lossless Lossless encoding
50
+ --sizes W,W,... Responsive widths e.g. 1200,800,400 (0=full)
51
+ --no-full Skip full-size, only responsive variants
52
+ -r, --recursive Recurse into subdirectories
53
+ --no-overwrite Skip files that already have a .webp counterpart
54
+ -w, --watch Watch mode
55
+ --silent Suppress all output
56
+ -h, --help Show this message and exit
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Programmatic API
62
+
63
+ ```python
64
+ from webp_convert import convert, watch, convert_file, ConvertConfig, SizeSpec
65
+
66
+ # --- Convert a directory (keyword args shortcut) ---
67
+ results = convert(input="src/images", quality=85)
68
+
69
+ # --- Convert with a config object ---
70
+ cfg = ConvertConfig(
71
+ input="src/images",
72
+ output="public/images", # omit to write next to originals
73
+ quality=85,
74
+ lossless=False,
75
+ recursive=True,
76
+ overwrite=True,
77
+ sizes=[
78
+ SizeSpec(width=0, suffix=""), # full size
79
+ SizeSpec(width=1200, suffix="-1200"),
80
+ SizeSpec(width=800, suffix="-800"),
81
+ SizeSpec(width=400, suffix="-400"),
82
+ ],
83
+ on_file=lambda r: print(f"{r.src} → {r.dest} ({r.size_bytes} bytes)"),
84
+ )
85
+ results = convert(cfg)
86
+
87
+ # --- Convert a single file ---
88
+ results = convert_file("hero.png", cfg)
89
+
90
+ # --- Watch mode (context manager) ---
91
+ import time
92
+ watcher = watch(ConvertConfig(input="src/images")).start()
93
+ try:
94
+ while True:
95
+ time.sleep(1)
96
+ except KeyboardInterrupt:
97
+ watcher.stop()
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Output structure
103
+
104
+ Given `hero.jpg` in `src/images/`:
105
+
106
+ ```
107
+ src/images/
108
+ ├── hero.jpg (original, kept by default)
109
+ ├── hero.webp (full size)
110
+ ├── hero-800.webp (800px wide, aspect-ratio preserved)
111
+ └── hero-400.webp (400px wide)
112
+ ```
113
+
114
+ ---
115
+
116
+ ## HTML usage
117
+
118
+ ```html
119
+ <picture>
120
+ <source
121
+ type="image/webp"
122
+ srcset="hero-400.webp 400w, hero-800.webp 800w, hero.webp 1600w"
123
+ sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1600px"
124
+ />
125
+ <img src="hero.jpg" alt="Hero image" />
126
+ </picture>
127
+ ```
128
+
129
+ ---
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "webp-convert"
7
+ version = "1.0.0"
8
+ description = "Automatically convert PNG/JPG images to WebP with responsive sizes. CLI, file watcher, and Python API."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.8"
12
+ dependencies = [
13
+ "Pillow>=10.0.0",
14
+ "watchdog>=4.0.0",
15
+ "click>=8.1.0",
16
+ "rich>=13.0.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ webp-convert = "webp_convert.cli:main"
21
+
22
+ [project.optional-dependencies]
23
+ dev = ["pytest", "pytest-asyncio"]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["."]
27
+ include = ["webp_convert*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """
2
+ webp-convert — Python package
3
+ Convert PNG/JPG/GIF/TIFF images to WebP with responsive size variants.
4
+ """
5
+
6
+ from .converter import convert, convert_file, convert_dir, DEFAULT_CONFIG
7
+ from .watcher import watch
8
+
9
+ __all__ = ["convert", "convert_file", "convert_dir", "watch", "DEFAULT_CONFIG"]
10
+ __version__ = "1.0.0"
@@ -0,0 +1,89 @@
1
+ """
2
+ webp_convert.cli
3
+ Click-based CLI entry point.
4
+
5
+ Usage:
6
+ webp-convert [INPUT_DIR] [options]
7
+ """
8
+
9
+ import sys
10
+ import time
11
+ import click
12
+ from pathlib import Path
13
+ from .converter import ConvertConfig, SizeSpec, convert
14
+ from .watcher import watch as do_watch
15
+
16
+
17
+ def _parse_sizes(sizes_str: str):
18
+ """Parse '1200,800,400' → list of SizeSpec."""
19
+ specs = []
20
+ for part in sizes_str.split(","):
21
+ w = int(part.strip())
22
+ specs.append(SizeSpec(width=w, suffix="" if w == 0 else f"-{w}"))
23
+ return specs
24
+
25
+
26
+ @click.command(context_settings={"help_option_names": ["-h", "--help"]})
27
+ @click.argument("input_dir", default="assets/images", metavar="[INPUT_DIR]")
28
+ @click.option("-o", "--output", default=None, help="Output directory (default: same as source)")
29
+ @click.option("-q", "--quality", default=80, show_default=True, help="WebP quality 0-100")
30
+ @click.option("--lossless", is_flag=True, help="Lossless encoding")
31
+ @click.option(
32
+ "--sizes", "sizes_str",
33
+ default=None,
34
+ metavar="W,W,...",
35
+ help="Responsive widths e.g. 1200,800,400 (0=full). Default: 0,800,400",
36
+ )
37
+ @click.option("--no-full", is_flag=True, help="Skip full-size, only responsive variants")
38
+ @click.option("-r", "--recursive", is_flag=True, help="Recurse into subdirectories")
39
+ @click.option("--no-overwrite", is_flag=True, help="Skip files that already have a .webp")
40
+ @click.option("-w", "--watch", "watch_mode", is_flag=True, help="Watch mode")
41
+ @click.option("--silent", is_flag=True, help="Suppress all output")
42
+ def main(input_dir, output, quality, lossless, sizes_str, no_full, recursive,
43
+ no_overwrite, watch_mode, silent):
44
+ """
45
+ Convert PNG/JPG images to WebP with responsive size variants.
46
+
47
+ \b
48
+ Examples:
49
+ webp-convert
50
+ webp-convert src/img -q 90 -o dist/img
51
+ webp-convert src/img --sizes 1200,800,400
52
+ webp-convert src/img --watch
53
+ """
54
+
55
+ # Build sizes list
56
+ if sizes_str:
57
+ sizes = _parse_sizes(sizes_str)
58
+ if not no_full and not any(s.width == 0 for s in sizes):
59
+ sizes.insert(0, SizeSpec(width=0, suffix=""))
60
+ else:
61
+ sizes = [SizeSpec(0, ""), SizeSpec(800, "-800"), SizeSpec(400, "-400")]
62
+ if no_full:
63
+ sizes = [s for s in sizes if s.width != 0]
64
+
65
+ cfg = ConvertConfig(
66
+ input=input_dir,
67
+ output=output,
68
+ quality=quality,
69
+ lossless=lossless,
70
+ sizes=sizes,
71
+ overwrite=not no_overwrite,
72
+ recursive=recursive,
73
+ silent=silent,
74
+ )
75
+
76
+ if watch_mode:
77
+ watcher = do_watch(cfg).start()
78
+ try:
79
+ while True:
80
+ time.sleep(1)
81
+ except KeyboardInterrupt:
82
+ click.echo("\n stopping watcher...")
83
+ watcher.stop()
84
+ else:
85
+ convert(cfg)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,206 @@
1
+ """
2
+ webp_convert.converter
3
+ Core image conversion logic using Pillow.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Callable, List, Optional, Union
12
+
13
+ from PIL import Image
14
+
15
+ # ─── Config ───────────────────────────────────────────────────────────────────
16
+
17
+ @dataclass
18
+ class SizeSpec:
19
+ width: int # 0 = full size (no resize)
20
+ suffix: str # e.g. "-800", "" for full-size
21
+
22
+
23
+ DEFAULT_SIZES = [
24
+ SizeSpec(width=0, suffix=""),
25
+ SizeSpec(width=800, suffix="-800"),
26
+ SizeSpec(width=400, suffix="-400"),
27
+ ]
28
+
29
+ DEFAULT_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".tiff", ".bmp", ".webp"}
30
+
31
+ @dataclass
32
+ class ConvertConfig:
33
+ input: Union[str, List[str]] = "assets/images"
34
+ output: Optional[str] = None # None = same folder as source
35
+ quality: int = 80
36
+ lossless: bool = False
37
+ sizes: List[SizeSpec] = field(default_factory=lambda: list(DEFAULT_SIZES))
38
+ keep_originals: bool = True
39
+ overwrite: bool = True
40
+ recursive: bool = False
41
+ extensions: set = field(default_factory=lambda: set(DEFAULT_EXTENSIONS))
42
+ on_file: Optional[Callable] = None # called with ConvertResult per output
43
+ on_done: Optional[Callable] = None # called with list[ConvertResult] when batch finishes
44
+ silent: bool = False
45
+
46
+
47
+ DEFAULT_CONFIG = ConvertConfig()
48
+
49
+
50
+ # ─── Result ───────────────────────────────────────────────────────────────────
51
+
52
+ @dataclass
53
+ class ConvertResult:
54
+ src: Path
55
+ dest: Path
56
+ width: Union[int, str] # int or "original"
57
+ size_bytes: int
58
+
59
+
60
+ # ─── Helpers ──────────────────────────────────────────────────────────────────
61
+
62
+ def _get_output_path(src: Path, suffix: str, cfg: ConvertConfig) -> Path:
63
+ if cfg.output:
64
+ base_dir = Path(cfg.output)
65
+ else:
66
+ base_dir = src.parent
67
+ return base_dir / f"{src.stem}{suffix}.webp"
68
+
69
+
70
+ def _log(cfg: ConvertConfig, *args):
71
+ if not cfg.silent:
72
+ print(*args)
73
+
74
+
75
+ # ─── Single-file conversion ───────────────────────────────────────────────────
76
+
77
+ def convert_file(src: Union[str, Path], cfg: Optional[ConvertConfig] = None) -> List[ConvertResult]:
78
+ """
79
+ Convert a single image file to all configured WebP sizes.
80
+ Returns a list of ConvertResult objects.
81
+
82
+ Example::
83
+
84
+ from webp_convert import convert_file, ConvertConfig
85
+ results = convert_file("hero.jpg", ConvertConfig(quality=90))
86
+ """
87
+ cfg = cfg or ConvertConfig()
88
+ src = Path(src)
89
+ results = []
90
+
91
+ if cfg.output:
92
+ Path(cfg.output).mkdir(parents=True, exist_ok=True)
93
+
94
+ with Image.open(src) as img:
95
+ # Ensure RGB(A) for proper WebP encoding
96
+ if img.mode not in ("RGB", "RGBA"):
97
+ img = img.convert("RGBA" if img.mode == "P" and "transparency" in img.info else "RGB")
98
+
99
+ for spec in cfg.sizes:
100
+ dest = _get_output_path(src, spec.suffix, cfg)
101
+
102
+ if not cfg.overwrite and dest.exists():
103
+ _log(cfg, f" skip {dest} (exists)")
104
+ continue
105
+
106
+ if spec.width > 0:
107
+ ratio = spec.width / img.width
108
+ # Don't enlarge
109
+ if ratio < 1.0:
110
+ new_h = int(img.height * ratio)
111
+ resized = img.resize((spec.width, new_h), Image.LANCZOS)
112
+ else:
113
+ resized = img
114
+ else:
115
+ resized = img
116
+
117
+ save_kwargs = {"format": "WEBP", "quality": cfg.quality}
118
+ if cfg.lossless:
119
+ save_kwargs["lossless"] = True
120
+
121
+ resized.save(dest, **save_kwargs)
122
+ size_bytes = dest.stat().st_size
123
+ result = ConvertResult(
124
+ src=src,
125
+ dest=dest,
126
+ width=spec.width if spec.width > 0 else "original",
127
+ size_bytes=size_bytes,
128
+ )
129
+
130
+ _log(cfg, f" → {dest} ({size_bytes / 1024:.1f} KB)")
131
+
132
+ if cfg.on_file:
133
+ cfg.on_file(result)
134
+
135
+ results.append(result)
136
+
137
+ return results
138
+
139
+
140
+ # ─── Directory conversion ─────────────────────────────────────────────────────
141
+
142
+ def convert_dir(input_dir: Union[str, Path], cfg: Optional[ConvertConfig] = None) -> List[ConvertResult]:
143
+ """
144
+ Convert all matching images in a directory.
145
+ """
146
+ cfg = cfg or ConvertConfig()
147
+ input_dir = Path(input_dir)
148
+ all_results: List[ConvertResult] = []
149
+
150
+ pattern = "**/*" if cfg.recursive else "*"
151
+ for path in input_dir.glob(pattern):
152
+ if not path.is_file():
153
+ continue
154
+ if path.suffix.lower() not in cfg.extensions:
155
+ continue
156
+ if path.suffix.lower() == ".webp":
157
+ continue
158
+
159
+ _log(cfg, f"processing {path}")
160
+ try:
161
+ results = convert_file(path, cfg)
162
+ all_results.extend(results)
163
+ except Exception as exc:
164
+ print(f" error: {path}: {exc}")
165
+
166
+ return all_results
167
+
168
+
169
+ # ─── Batch (multi-dir) ────────────────────────────────────────────────────────
170
+
171
+ def convert(cfg: Optional[ConvertConfig] = None, **kwargs) -> List[ConvertResult]:
172
+ """
173
+ Main entry point. Convert all images in one or more input directories.
174
+
175
+ Can be called with a ConvertConfig object or keyword arguments::
176
+
177
+ # Using a config object
178
+ from webp_convert import convert, ConvertConfig
179
+ results = convert(ConvertConfig(input="src/images", quality=85))
180
+
181
+ # Using keyword args (creates a ConvertConfig internally)
182
+ results = convert(input="src/images", quality=85)
183
+ """
184
+ if cfg is None:
185
+ cfg = ConvertConfig(**kwargs)
186
+ elif kwargs:
187
+ # Merge kwargs into a copy of cfg
188
+ cfg_dict = cfg.__dict__.copy()
189
+ cfg_dict.update(kwargs)
190
+ cfg = ConvertConfig(**cfg_dict)
191
+
192
+ inputs = cfg.input if isinstance(cfg.input, list) else [cfg.input]
193
+ all_results: List[ConvertResult] = []
194
+
195
+ _log(cfg, "\n webp-convert — starting...\n")
196
+
197
+ for d in inputs:
198
+ results = convert_dir(d, cfg)
199
+ all_results.extend(results)
200
+
201
+ _log(cfg, f"\n ✓ done — converted {len(all_results)} file(s)")
202
+
203
+ if cfg.on_done:
204
+ cfg.on_done(all_results)
205
+
206
+ return all_results
@@ -0,0 +1,102 @@
1
+ """
2
+ webp_convert.watcher
3
+ File watching support using watchdog.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Optional, Union, List
11
+
12
+ from watchdog.events import FileSystemEventHandler, FileCreatedEvent, FileModifiedEvent
13
+ from watchdog.observers import Observer
14
+
15
+ from .converter import ConvertConfig, convert_file
16
+
17
+
18
+ class _ImageEventHandler(FileSystemEventHandler):
19
+ def __init__(self, cfg: ConvertConfig):
20
+ self.cfg = cfg
21
+
22
+ def _handle(self, src_path: str):
23
+ path = Path(src_path)
24
+ if path.suffix.lower() not in self.cfg.extensions:
25
+ return
26
+ if path.suffix.lower() == ".webp":
27
+ return
28
+ if not self.cfg.silent:
29
+ print(f"\n changed: {path}")
30
+ try:
31
+ convert_file(path, self.cfg)
32
+ except Exception as exc:
33
+ print(f" error: {exc}")
34
+
35
+ def on_created(self, event):
36
+ if not event.is_directory:
37
+ self._handle(event.src_path)
38
+
39
+ def on_modified(self, event):
40
+ if not event.is_directory:
41
+ self._handle(event.src_path)
42
+
43
+
44
+ class Watcher:
45
+ """
46
+ Wraps a watchdog Observer. Call .start() to begin watching, .stop() to end.
47
+ The context manager interface handles start/stop automatically.
48
+ """
49
+
50
+ def __init__(self, cfg: ConvertConfig):
51
+ self.cfg = cfg
52
+ self._observer = Observer()
53
+ inputs = cfg.input if isinstance(cfg.input, list) else [cfg.input]
54
+ handler = _ImageEventHandler(cfg)
55
+ for d in inputs:
56
+ self._observer.schedule(handler, str(d), recursive=cfg.recursive)
57
+
58
+ def start(self):
59
+ self._observer.start()
60
+ if not self.cfg.silent:
61
+ inputs = self.cfg.input if isinstance(self.cfg.input, list) else [self.cfg.input]
62
+ print("\n webp-convert — watching...")
63
+ for d in inputs:
64
+ print(f" watching: {d}")
65
+ return self
66
+
67
+ def stop(self):
68
+ self._observer.stop()
69
+ self._observer.join()
70
+
71
+ def __enter__(self):
72
+ return self.start()
73
+
74
+ def __exit__(self, *_):
75
+ self.stop()
76
+
77
+
78
+ def watch(cfg: Optional[ConvertConfig] = None, **kwargs) -> Watcher:
79
+ """
80
+ Watch input directories and auto-convert images on change.
81
+ Returns a Watcher instance. Call .start() to begin (or use as context manager).
82
+
83
+ Example::
84
+
85
+ from webp_convert import watch, ConvertConfig
86
+
87
+ # Context manager (blocks until Ctrl-C)
88
+ with watch(ConvertConfig(input="src/images")).start():
89
+ try:
90
+ while True:
91
+ import time; time.sleep(1)
92
+ except KeyboardInterrupt:
93
+ pass
94
+
95
+ # Manual control
96
+ watcher = watch(input="src/images").start()
97
+ # ... later:
98
+ watcher.stop()
99
+ """
100
+ if cfg is None:
101
+ cfg = ConvertConfig(**kwargs)
102
+ return Watcher(cfg)
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: webp-convert
3
+ Version: 1.0.0
4
+ Summary: Automatically convert PNG/JPG images to WebP with responsive sizes. CLI, file watcher, and Python API.
5
+ License: MIT
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: Pillow>=10.0.0
9
+ Requires-Dist: watchdog>=4.0.0
10
+ Requires-Dist: click>=8.1.0
11
+ Requires-Dist: rich>=13.0.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: pytest-asyncio; extra == "dev"
15
+
16
+ # webp-convert (Python)
17
+
18
+ > Automatically convert PNG/JPG/GIF/TIFF images to WebP with responsive size variants.
19
+ > Zero-config CLI, file watcher, and programmatic Python API.
20
+
21
+ ---
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip3 install webp-convert
27
+ ```
28
+
29
+ Requires Python 3.8+. Uses [Pillow](https://pillow.readthedocs.io/) — no external binaries required.
30
+
31
+ ---
32
+
33
+ ## Quick start
34
+
35
+ ```bash
36
+ # Convert everything in assets/images (default)
37
+ webp-convert
38
+
39
+ # Custom dir + quality
40
+ webp-convert src/img -q 90
41
+
42
+ # Watch mode — auto-converts on file change
43
+ webp-convert src/img --watch
44
+
45
+ # Custom responsive sizes
46
+ webp-convert src/img --sizes 1200,800,400
47
+
48
+ # Write outputs to a separate directory
49
+ webp-convert src/img -o dist/img
50
+ ```
51
+
52
+ ---
53
+
54
+ ## CLI reference
55
+
56
+ ```
57
+ Usage: webp-convert [INPUT_DIR] [OPTIONS]
58
+
59
+ Convert PNG/JPG images to WebP with responsive size variants.
60
+
61
+ Options:
62
+ -o, --output TEXT Output directory (default: same as source)
63
+ -q, --quality INT WebP quality 0-100 [default: 80]
64
+ --lossless Lossless encoding
65
+ --sizes W,W,... Responsive widths e.g. 1200,800,400 (0=full)
66
+ --no-full Skip full-size, only responsive variants
67
+ -r, --recursive Recurse into subdirectories
68
+ --no-overwrite Skip files that already have a .webp counterpart
69
+ -w, --watch Watch mode
70
+ --silent Suppress all output
71
+ -h, --help Show this message and exit
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Programmatic API
77
+
78
+ ```python
79
+ from webp_convert import convert, watch, convert_file, ConvertConfig, SizeSpec
80
+
81
+ # --- Convert a directory (keyword args shortcut) ---
82
+ results = convert(input="src/images", quality=85)
83
+
84
+ # --- Convert with a config object ---
85
+ cfg = ConvertConfig(
86
+ input="src/images",
87
+ output="public/images", # omit to write next to originals
88
+ quality=85,
89
+ lossless=False,
90
+ recursive=True,
91
+ overwrite=True,
92
+ sizes=[
93
+ SizeSpec(width=0, suffix=""), # full size
94
+ SizeSpec(width=1200, suffix="-1200"),
95
+ SizeSpec(width=800, suffix="-800"),
96
+ SizeSpec(width=400, suffix="-400"),
97
+ ],
98
+ on_file=lambda r: print(f"{r.src} → {r.dest} ({r.size_bytes} bytes)"),
99
+ )
100
+ results = convert(cfg)
101
+
102
+ # --- Convert a single file ---
103
+ results = convert_file("hero.png", cfg)
104
+
105
+ # --- Watch mode (context manager) ---
106
+ import time
107
+ watcher = watch(ConvertConfig(input="src/images")).start()
108
+ try:
109
+ while True:
110
+ time.sleep(1)
111
+ except KeyboardInterrupt:
112
+ watcher.stop()
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Output structure
118
+
119
+ Given `hero.jpg` in `src/images/`:
120
+
121
+ ```
122
+ src/images/
123
+ ├── hero.jpg (original, kept by default)
124
+ ├── hero.webp (full size)
125
+ ├── hero-800.webp (800px wide, aspect-ratio preserved)
126
+ └── hero-400.webp (400px wide)
127
+ ```
128
+
129
+ ---
130
+
131
+ ## HTML usage
132
+
133
+ ```html
134
+ <picture>
135
+ <source
136
+ type="image/webp"
137
+ srcset="hero-400.webp 400w, hero-800.webp 800w, hero.webp 1600w"
138
+ sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1600px"
139
+ />
140
+ <img src="hero.jpg" alt="Hero image" />
141
+ </picture>
142
+ ```
143
+
144
+ ---
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,13 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ webp_convert/__init__.py
5
+ webp_convert/cli.py
6
+ webp_convert/converter.py
7
+ webp_convert/watcher.py
8
+ webp_convert.egg-info/PKG-INFO
9
+ webp_convert.egg-info/SOURCES.txt
10
+ webp_convert.egg-info/dependency_links.txt
11
+ webp_convert.egg-info/entry_points.txt
12
+ webp_convert.egg-info/requires.txt
13
+ webp_convert.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ webp-convert = webp_convert.cli:main
@@ -0,0 +1,8 @@
1
+ Pillow>=10.0.0
2
+ watchdog>=4.0.0
3
+ click>=8.1.0
4
+ rich>=13.0.0
5
+
6
+ [dev]
7
+ pytest
8
+ pytest-asyncio
@@ -0,0 +1 @@
1
+ webp_convert