imgopt-cli 1.0.0__py3-none-any.whl
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.
- imgopt/__init__.py +1 -0
- imgopt/__main__.py +4 -0
- imgopt/cli.py +314 -0
- imgopt_cli-1.0.0.dist-info/METADATA +90 -0
- imgopt_cli-1.0.0.dist-info/RECORD +9 -0
- imgopt_cli-1.0.0.dist-info/WHEEL +5 -0
- imgopt_cli-1.0.0.dist-info/entry_points.txt +2 -0
- imgopt_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- imgopt_cli-1.0.0.dist-info/top_level.txt +1 -0
imgopt/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
imgopt/__main__.py
ADDED
imgopt/cli.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import signal
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from typing import Optional, Tuple, List, Union, Any, NamedTuple
|
|
11
|
+
|
|
12
|
+
# --- Project Metadata ---
|
|
13
|
+
__version__ = "1.0.0"
|
|
14
|
+
__prog_name__ = "imgopt"
|
|
15
|
+
|
|
16
|
+
# --- Configuration ---
|
|
17
|
+
EXTENSIONS = {'.png', '.jpg', '.jpeg', '.bmp', '.tiff'}
|
|
18
|
+
|
|
19
|
+
# --- Logging Setup ---
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=logging.INFO,
|
|
22
|
+
format='%(message)s',
|
|
23
|
+
stream=sys.stdout
|
|
24
|
+
)
|
|
25
|
+
logger = logging.getLogger()
|
|
26
|
+
|
|
27
|
+
class ImageTask(NamedTuple):
|
|
28
|
+
file_path: Path
|
|
29
|
+
output_root: Path
|
|
30
|
+
input_root: Path
|
|
31
|
+
quality: int
|
|
32
|
+
max_width: Optional[int]
|
|
33
|
+
|
|
34
|
+
def signal_handler(sig, frame) -> None:
|
|
35
|
+
"""Handle termination signals (Ctrl+C) gracefully."""
|
|
36
|
+
logger.error("\n[!] Process interrupted. Exiting...")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
40
|
+
if hasattr(signal, 'SIGTERM'):
|
|
41
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
42
|
+
|
|
43
|
+
def get_input_with_validation(prompt: str, validation_func: callable, default_value: Optional[str] = None) -> Any:
|
|
44
|
+
"""
|
|
45
|
+
Prompts user for input and validates it in a loop.
|
|
46
|
+
Supports 'q' to quit.
|
|
47
|
+
"""
|
|
48
|
+
while True:
|
|
49
|
+
display_prompt = f"{prompt}"
|
|
50
|
+
if default_value:
|
|
51
|
+
display_prompt += f" (Default: {default_value})"
|
|
52
|
+
|
|
53
|
+
user_input = input(f"{display_prompt}: ").strip()
|
|
54
|
+
|
|
55
|
+
if user_input.lower() in ['q', 'quit', 'exit']:
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
if not user_input and default_value:
|
|
59
|
+
return default_value
|
|
60
|
+
|
|
61
|
+
result = validation_func(user_input)
|
|
62
|
+
if result is not None:
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
logger.warning("Invalid input. Try again.")
|
|
66
|
+
|
|
67
|
+
def validate_path(path_str: str) -> Optional[Path]:
|
|
68
|
+
"""Checks if the path exists."""
|
|
69
|
+
if not path_str:
|
|
70
|
+
return None
|
|
71
|
+
path = Path(path_str).resolve()
|
|
72
|
+
if path.exists():
|
|
73
|
+
return path
|
|
74
|
+
logger.warning(f"Path not found: {path_str}")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def validate_width(width_str: str) -> Union[int, str, None]:
|
|
78
|
+
"""Parses width input. Returns int, 'SKIP', or None."""
|
|
79
|
+
if not width_str:
|
|
80
|
+
return None
|
|
81
|
+
if width_str.lower() in ['0', 'n', 'no', 'skip']:
|
|
82
|
+
return 'SKIP'
|
|
83
|
+
try:
|
|
84
|
+
val = int(width_str)
|
|
85
|
+
if val >= 0:
|
|
86
|
+
return val if val > 0 else 'SKIP'
|
|
87
|
+
return None
|
|
88
|
+
except ValueError:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def validate_yes_no(val_str: str) -> Optional[bool]:
|
|
92
|
+
"""Parses yes/no string to boolean."""
|
|
93
|
+
if not val_str:
|
|
94
|
+
return None
|
|
95
|
+
if val_str.lower().startswith('y'):
|
|
96
|
+
return True
|
|
97
|
+
if val_str.lower().startswith('n'):
|
|
98
|
+
return False
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def get_unique_output_folder(base_folder: Path, name: str) -> Path:
|
|
102
|
+
"""Ensures output folder name is unique to avoid collisions."""
|
|
103
|
+
output_path = base_folder / name
|
|
104
|
+
if output_path.exists() and output_path.is_file():
|
|
105
|
+
counter = 1
|
|
106
|
+
while True:
|
|
107
|
+
new_name = f"{name}_{counter}"
|
|
108
|
+
new_path = base_folder / new_name
|
|
109
|
+
if not new_path.is_file():
|
|
110
|
+
return new_path
|
|
111
|
+
counter += 1
|
|
112
|
+
return output_path
|
|
113
|
+
|
|
114
|
+
def process_single_image(task: ImageTask) -> Tuple[bool, str, int, int]:
|
|
115
|
+
"""
|
|
116
|
+
Core image processing logic.
|
|
117
|
+
Handles resizing and conversion to WebP.
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
relative_path = task.file_path.relative_to(task.input_root)
|
|
121
|
+
output_file_path = task.output_root / relative_path.with_suffix('.webp')
|
|
122
|
+
output_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
original_size = task.file_path.stat().st_size
|
|
125
|
+
|
|
126
|
+
with Image.open(task.file_path) as img:
|
|
127
|
+
# Smart Resize
|
|
128
|
+
if task.max_width and img.width > task.max_width:
|
|
129
|
+
ratio = task.max_width / img.width
|
|
130
|
+
new_height = int(img.height * ratio)
|
|
131
|
+
img = img.resize((task.max_width, new_height), Image.Resampling.LANCZOS)
|
|
132
|
+
|
|
133
|
+
img.save(output_file_path, 'webp', quality=task.quality, method=6)
|
|
134
|
+
|
|
135
|
+
new_size = output_file_path.stat().st_size
|
|
136
|
+
return (True, task.file_path.name, original_size, new_size)
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
return (False, f"{task.file_path.name}: {e}", 0, 0)
|
|
140
|
+
|
|
141
|
+
def main():
|
|
142
|
+
"""
|
|
143
|
+
Main entry point for the CLI.
|
|
144
|
+
Parses arguments, handles interactive mode, and orchestrates the batch processing.
|
|
145
|
+
"""
|
|
146
|
+
# Setting prog='imgopt' hides 'optimize.py' and makes it look like a binary
|
|
147
|
+
parser = argparse.ArgumentParser(
|
|
148
|
+
prog=__prog_name__,
|
|
149
|
+
description="A high-performance CLI tool to batch optimize images for the web.",
|
|
150
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
151
|
+
epilog="Examples:\n"
|
|
152
|
+
" imgopt (Interactive Wizard)\n"
|
|
153
|
+
" imgopt ./photos -q 90 (Quick mode, high quality)\n"
|
|
154
|
+
" imgopt ./photos -w 0 (Convert only, no resize)\n"
|
|
155
|
+
" imgopt ./photos --output dist (Custom output folder)"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# --- Professional CLI Arguments ---
|
|
159
|
+
parser.add_argument("-v", "--version", action="version", version=f"{__prog_name__} {__version__}")
|
|
160
|
+
|
|
161
|
+
parser.add_argument("path", nargs="?", help="Input directory path containing images.")
|
|
162
|
+
|
|
163
|
+
parser.add_argument("-o", "--output", type=str, default="optimized_webp",
|
|
164
|
+
help="Name of the output folder (default: optimized_webp).")
|
|
165
|
+
|
|
166
|
+
parser.add_argument("-w", "--width", type=str, default="1920",
|
|
167
|
+
help="Max width in pixels. Use '0' to keep original size (default: 1920).")
|
|
168
|
+
|
|
169
|
+
parser.add_argument("-q", "--quality", type=int, default=80,
|
|
170
|
+
help="WebP quality (0-100) (default: 80).")
|
|
171
|
+
|
|
172
|
+
parser.add_argument("-i", "--interactive", action="store_true",
|
|
173
|
+
help="Force launch of the interactive wizard.")
|
|
174
|
+
|
|
175
|
+
parser.add_argument("--quiet", action="store_true",
|
|
176
|
+
help="Suppress per-file logs, showing only the final summary.")
|
|
177
|
+
|
|
178
|
+
parser.add_argument("--no-sound", action="store_true",
|
|
179
|
+
help="Disable the completion notification sound (Beep).")
|
|
180
|
+
|
|
181
|
+
args = parser.parse_args()
|
|
182
|
+
|
|
183
|
+
# Defaults
|
|
184
|
+
input_dir = None
|
|
185
|
+
target_width = None
|
|
186
|
+
output_folder_name = args.output
|
|
187
|
+
quality = args.quality
|
|
188
|
+
verbose = not args.quiet
|
|
189
|
+
play_sound = not args.no_sound
|
|
190
|
+
is_interactive = args.interactive
|
|
191
|
+
|
|
192
|
+
# Auto-trigger interactive if no path is given
|
|
193
|
+
if len(sys.argv) == 1:
|
|
194
|
+
is_interactive = True
|
|
195
|
+
|
|
196
|
+
# --- Interactive Wizard Logic ---
|
|
197
|
+
if is_interactive:
|
|
198
|
+
logger.info(f"{__prog_name__} v{__version__}")
|
|
199
|
+
logger.info("Interactive Mode (Press 'q' to quit at any time).\n")
|
|
200
|
+
|
|
201
|
+
input_dir = get_input_with_validation("Input folder path", validate_path)
|
|
202
|
+
|
|
203
|
+
width_result = get_input_with_validation(
|
|
204
|
+
"Max width (0 for original)",
|
|
205
|
+
validate_width,
|
|
206
|
+
default_value="1920"
|
|
207
|
+
)
|
|
208
|
+
target_width = None if width_result == 'SKIP' else width_result
|
|
209
|
+
|
|
210
|
+
output_folder_name = get_input_with_validation(
|
|
211
|
+
"Output folder name",
|
|
212
|
+
lambda x: x if x else None,
|
|
213
|
+
default_value="optimized_webp"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
verbose = get_input_with_validation(
|
|
217
|
+
"Show details? (y/n)",
|
|
218
|
+
validate_yes_no,
|
|
219
|
+
default_value="n"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Audio preference in interactive mode
|
|
223
|
+
play_sound = get_input_with_validation(
|
|
224
|
+
"Play sound when done? (y/n)",
|
|
225
|
+
validate_yes_no,
|
|
226
|
+
default_value="y"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
else:
|
|
230
|
+
# --- CLI Mode Logic ---
|
|
231
|
+
input_dir = validate_path(args.path)
|
|
232
|
+
if not input_dir:
|
|
233
|
+
# If path is invalid in CLI mode, show help
|
|
234
|
+
parser.print_help()
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
# Parse width argument from CLI
|
|
238
|
+
w_val = validate_width(args.width)
|
|
239
|
+
target_width = None if w_val == 'SKIP' else w_val
|
|
240
|
+
|
|
241
|
+
# --- Execution Logic ---
|
|
242
|
+
output_dir = get_unique_output_folder(input_dir, output_folder_name)
|
|
243
|
+
if input_dir == output_dir:
|
|
244
|
+
logger.error("Error: Input and Output folders cannot be the same.")
|
|
245
|
+
sys.exit(1)
|
|
246
|
+
|
|
247
|
+
output_dir.mkdir(exist_ok=True)
|
|
248
|
+
|
|
249
|
+
logger.info("Scanning...")
|
|
250
|
+
files = [
|
|
251
|
+
f for f in input_dir.rglob("*")
|
|
252
|
+
if f.suffix.lower() in EXTENSIONS
|
|
253
|
+
and f.is_file()
|
|
254
|
+
and output_dir not in f.parents
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
if not files:
|
|
258
|
+
logger.warning("No images found.")
|
|
259
|
+
sys.exit(0)
|
|
260
|
+
|
|
261
|
+
# Info Summary
|
|
262
|
+
logger.info("-" * 40)
|
|
263
|
+
logger.info(f"Source: {input_dir}")
|
|
264
|
+
logger.info(f"Target: {output_dir.name}")
|
|
265
|
+
logger.info(f"Files: {len(files)}")
|
|
266
|
+
logger.info(f"Width: {target_width if target_width else 'Original'}")
|
|
267
|
+
logger.info(f"Quality: {quality}")
|
|
268
|
+
logger.info("-" * 40)
|
|
269
|
+
|
|
270
|
+
start_time = time.time()
|
|
271
|
+
|
|
272
|
+
# Create Task Objects
|
|
273
|
+
tasks = [
|
|
274
|
+
ImageTask(f, output_dir, input_dir, quality, target_width)
|
|
275
|
+
for f in files
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
success = 0
|
|
279
|
+
failed = 0
|
|
280
|
+
orig_total = 0
|
|
281
|
+
new_total = 0
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
with ProcessPoolExecutor() as executor:
|
|
285
|
+
results = executor.map(process_single_image, tasks)
|
|
286
|
+
for is_ok, msg, orig, new_s in results:
|
|
287
|
+
if is_ok:
|
|
288
|
+
success += 1
|
|
289
|
+
orig_total += orig
|
|
290
|
+
new_total += new_s
|
|
291
|
+
if verbose: logger.info(f"OK: {msg}")
|
|
292
|
+
else:
|
|
293
|
+
failed += 1
|
|
294
|
+
logger.error(f"FAIL: {msg}")
|
|
295
|
+
|
|
296
|
+
except KeyboardInterrupt:
|
|
297
|
+
logger.error("\nCancelled.")
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
|
|
300
|
+
saved = orig_total - new_total
|
|
301
|
+
saved_mb = saved / (1024 * 1024)
|
|
302
|
+
pct = (saved / orig_total * 100) if orig_total > 0 else 0
|
|
303
|
+
|
|
304
|
+
logger.info("\n" + "=" * 40)
|
|
305
|
+
logger.info(f"Finished: {success} OK | {failed} Failed")
|
|
306
|
+
logger.info(f"Saved: {saved_mb:.2f} MB ({pct:.1f}%)")
|
|
307
|
+
logger.info("=" * 40)
|
|
308
|
+
|
|
309
|
+
if play_sound: print('\a')
|
|
310
|
+
if success == 0 and len(files) > 0: sys.exit(1)
|
|
311
|
+
sys.exit(0)
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
main()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imgopt-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A high-performance CLI tool to batch optimize images for the web.
|
|
5
|
+
Author: Ahmed Samy El-khouly
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Ahmed Samy El-khouly
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/ahmedthebest31/imgopt
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/ahmedthebest31/imgopt/issues
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
34
|
+
Requires-Python: >=3.8
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: Pillow>=10.3.0
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# imgopt 🚀
|
|
41
|
+
|
|
42
|
+
**The Intelligent WebP Converter for Modern Web Development.**
|
|
43
|
+
|
|
44
|
+
`imgopt` is a high-performance, accessibility-first CLI tool designed to batch convert and optimize images (PNG, JPG, TIFF) into efficient WebP format. It features smart resizing, concurrency for speed, and an interactive wizard mode.
|
|
45
|
+
|
|
46
|
+
## Features ✨
|
|
47
|
+
|
|
48
|
+
- **Batch Processing:** Convert hundreds of images in seconds using multi-core processing.
|
|
49
|
+
- **Smart Resizing:** Automatically downscale images to a target width (e.g., 1920px) while maintaining aspect ratio.
|
|
50
|
+
- **Interactive Wizard:** No need to memorize flags; just run `imgopt` and follow the prompts.
|
|
51
|
+
- **Accessibility Friendly:** Screen-reader friendly outputs and audio cues upon completion.
|
|
52
|
+
- **Safe:** Never overwrites your original files.
|
|
53
|
+
|
|
54
|
+
## Installation 📦
|
|
55
|
+
|
|
56
|
+
You can install `imgopt` directly from PyPI (coming soon) or from source:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install .
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage 🛠️
|
|
63
|
+
|
|
64
|
+
### Interactive Mode (Wizard)
|
|
65
|
+
Just run the command without arguments:
|
|
66
|
+
```bash
|
|
67
|
+
imgopt
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Quick CLI Mode
|
|
71
|
+
Optimize all images in a folder to 80% quality WebP, max width 1920px:
|
|
72
|
+
```bash
|
|
73
|
+
imgopt ./photos
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Advanced Usage
|
|
77
|
+
```bash
|
|
78
|
+
# Custom output folder, no resizing, 90% quality
|
|
79
|
+
imgopt ./raw_images --output ./web_ready --width 0 --quality 90
|
|
80
|
+
|
|
81
|
+
# Quiet mode (no per-file logs)
|
|
82
|
+
imgopt ./assets --quiet
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Requirements
|
|
86
|
+
- Python 3.8+
|
|
87
|
+
- Pillow
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
imgopt/__init__.py,sha256=ZhzQKWZ8RFrTIkj7z87B144DI95e8LMfA5w8NDWQDtg,23
|
|
2
|
+
imgopt/__main__.py,sha256=EClCwCzb6h6YBpt0hrnG4h0mlNhNePyg_xBNNSVm1os,65
|
|
3
|
+
imgopt/cli.py,sha256=24FtX6HQFCP41ru4V1fA0-jDh6s6s4Av2HHIWXVq-NY,10515
|
|
4
|
+
imgopt_cli-1.0.0.dist-info/licenses/LICENSE,sha256=5SKr7MM_5tOOQ9ssCZot370lzfeR2KzmuVFSx5u-FjE,1097
|
|
5
|
+
imgopt_cli-1.0.0.dist-info/METADATA,sha256=M7yVuZaiZ8zkN26C0PRPA8NBhG405YcaX2QMV_9hp0E,3439
|
|
6
|
+
imgopt_cli-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
imgopt_cli-1.0.0.dist-info/entry_points.txt,sha256=aUIZvtmLeoKCZMQGMc3_skQuHK03qWrA-QaYX6ppOUI,43
|
|
8
|
+
imgopt_cli-1.0.0.dist-info/top_level.txt,sha256=t3Sc_lwb3YfyD2-5WTDK3_fmSKYdYUTx6mP1TdkkCLY,7
|
|
9
|
+
imgopt_cli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ahmed Samy El-khouly
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
imgopt
|