mil-kit 0.3.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.
- mil_kit/__init__.py +2 -0
- mil_kit/job.py +308 -0
- mil_kit/main.py +45 -0
- mil_kit/psd/__init__.py +0 -0
- mil_kit/psd/processor.py +77 -0
- mil_kit-0.3.0.dist-info/METADATA +126 -0
- mil_kit-0.3.0.dist-info/RECORD +10 -0
- mil_kit-0.3.0.dist-info/WHEEL +4 -0
- mil_kit-0.3.0.dist-info/entry_points.txt +2 -0
- mil_kit-0.3.0.dist-info/licenses/LICENSE +21 -0
mil_kit/__init__.py
ADDED
mil_kit/job.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Batch Processing Module for PSD Files
|
|
3
|
+
Manages parallel processing of PSD files with enhanced error handling and logging.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, Tuple, List
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import logging
|
|
11
|
+
from tqdm import tqdm
|
|
12
|
+
from mil_kit.psd.processor import PSDProcessor
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BatchJob:
|
|
18
|
+
"""
|
|
19
|
+
Manages the processing of a directory of files with parallel execution.
|
|
20
|
+
|
|
21
|
+
Features:
|
|
22
|
+
- Parallel processing using ThreadPoolExecutor
|
|
23
|
+
- Progress tracking with tqdm
|
|
24
|
+
- Detailed logging and error handling
|
|
25
|
+
- Flexible output options
|
|
26
|
+
- Processing statistics and reporting
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
SUPPORTED_FORMATS = ["png", "jpg", "jpeg", "tiff", "bmp", "webp"]
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
input_dir: str,
|
|
34
|
+
output_dir: Optional[str] = None,
|
|
35
|
+
recursive: bool = False,
|
|
36
|
+
output_format: str = "png",
|
|
37
|
+
max_workers: Optional[int] = None,
|
|
38
|
+
log_file: Optional[str] = None,
|
|
39
|
+
overwrite: bool = True,
|
|
40
|
+
verbose: bool = True,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize the BatchJob processor.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
input_dir: Directory containing PSD files
|
|
47
|
+
output_dir: Output directory (defaults to input_dir)
|
|
48
|
+
recursive: Search subdirectories recursively
|
|
49
|
+
output_format: Output image format (png, jpg, etc.)
|
|
50
|
+
max_workers: Max parallel workers (None = CPU count)
|
|
51
|
+
log_file: Path to log file (None = no file logging)
|
|
52
|
+
overwrite: Overwrite existing output files
|
|
53
|
+
verbose: Print detailed progress messages
|
|
54
|
+
"""
|
|
55
|
+
self.input_dir = Path(input_dir)
|
|
56
|
+
self.recursive = recursive
|
|
57
|
+
self.output_format = output_format.lower()
|
|
58
|
+
self.max_workers = max_workers
|
|
59
|
+
self.overwrite = overwrite
|
|
60
|
+
self.verbose = verbose
|
|
61
|
+
|
|
62
|
+
# Validate input directory
|
|
63
|
+
if not self.input_dir.exists():
|
|
64
|
+
raise FileNotFoundError(f"Input directory not found: {self.input_dir}")
|
|
65
|
+
|
|
66
|
+
if not self.input_dir.is_dir():
|
|
67
|
+
raise NotADirectoryError(f"Input path is not a directory: {self.input_dir}")
|
|
68
|
+
|
|
69
|
+
# Validate output format
|
|
70
|
+
if self.output_format not in self.SUPPORTED_FORMATS:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Unsupported format: {self.output_format}. "
|
|
73
|
+
f"Supported formats: {', '.join(self.SUPPORTED_FORMATS)}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Set up output directory
|
|
77
|
+
self.output_dir = Path(output_dir) if output_dir else self.input_dir
|
|
78
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
|
|
80
|
+
# Initialize statistics
|
|
81
|
+
self.stats = {
|
|
82
|
+
"success": 0,
|
|
83
|
+
"failed": 0,
|
|
84
|
+
"skipped": 0,
|
|
85
|
+
"total_layers_hidden": 0,
|
|
86
|
+
"start_time": None,
|
|
87
|
+
"end_time": None,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
self.failed_files = [] # Track failed files for reporting
|
|
91
|
+
|
|
92
|
+
# Set up logging
|
|
93
|
+
self._setup_logging(log_file)
|
|
94
|
+
|
|
95
|
+
def _setup_logging(self, log_file: Optional[str]):
|
|
96
|
+
"""Configure logging for the batch job."""
|
|
97
|
+
self.logger = logging.getLogger(__name__)
|
|
98
|
+
self.logger.setLevel(logging.DEBUG if self.verbose else logging.INFO)
|
|
99
|
+
|
|
100
|
+
# Clear existing handlers
|
|
101
|
+
self.logger.handlers.clear()
|
|
102
|
+
|
|
103
|
+
# Console handler
|
|
104
|
+
console_handler = logging.StreamHandler()
|
|
105
|
+
console_handler.setLevel(logging.INFO)
|
|
106
|
+
console_format = logging.Formatter('%(message)s')
|
|
107
|
+
console_handler.setFormatter(console_format)
|
|
108
|
+
self.logger.addHandler(console_handler)
|
|
109
|
+
|
|
110
|
+
# File handler (if specified)
|
|
111
|
+
if log_file:
|
|
112
|
+
log_path = Path(log_file)
|
|
113
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
file_handler = logging.FileHandler(log_file, mode='a')
|
|
115
|
+
file_handler.setLevel(logging.DEBUG)
|
|
116
|
+
file_format = logging.Formatter(
|
|
117
|
+
'%(asctime)s - %(levelname)s - %(message)s',
|
|
118
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
119
|
+
)
|
|
120
|
+
file_handler.setFormatter(file_format)
|
|
121
|
+
self.logger.addHandler(file_handler)
|
|
122
|
+
|
|
123
|
+
def run(self):
|
|
124
|
+
"""
|
|
125
|
+
Execute the batch processing with parallel execution.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dictionary containing processing statistics
|
|
129
|
+
"""
|
|
130
|
+
self.stats["start_time"] = datetime.now()
|
|
131
|
+
|
|
132
|
+
files = list(self._get_files())
|
|
133
|
+
total_files = len(files)
|
|
134
|
+
|
|
135
|
+
if total_files == 0:
|
|
136
|
+
message = f"No PSD files found in {self.input_dir}"
|
|
137
|
+
if self.recursive:
|
|
138
|
+
message += " (including subdirectories)"
|
|
139
|
+
self.logger.warning(message)
|
|
140
|
+
return self.stats
|
|
141
|
+
|
|
142
|
+
self.logger.info(
|
|
143
|
+
f"Found {total_files} PSD file(s) in {self.input_dir}"
|
|
144
|
+
)
|
|
145
|
+
self.logger.info(f"Output directory: {self.output_dir}")
|
|
146
|
+
self.logger.info(f"Output format: {self.output_format.upper()}")
|
|
147
|
+
self.logger.info(f"Max workers: {self.max_workers or 'Auto (CPU count)'}\n")
|
|
148
|
+
|
|
149
|
+
if total_files == 1:
|
|
150
|
+
# Single file - no need for parallelism
|
|
151
|
+
self._process_single_file_wrapper(files[0])
|
|
152
|
+
else:
|
|
153
|
+
# Multiple files - use parallel processing
|
|
154
|
+
self._process_multiple_files(files, total_files)
|
|
155
|
+
|
|
156
|
+
self.stats["end_time"] = datetime.now()
|
|
157
|
+
self._print_summary(total_files)
|
|
158
|
+
self._copy_failed_file()
|
|
159
|
+
|
|
160
|
+
def _process_multiple_files(self, files: List[Path], total_files: int):
|
|
161
|
+
"""Process multiple files in parallel."""
|
|
162
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
163
|
+
# Submit all tasks
|
|
164
|
+
futures = {
|
|
165
|
+
executor.submit(self._process_single_file, psd_path): psd_path
|
|
166
|
+
for psd_path in files
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
with tqdm(
|
|
170
|
+
total=total_files,
|
|
171
|
+
desc="Processing PSD files",
|
|
172
|
+
unit="file",
|
|
173
|
+
disable=not self.verbose,
|
|
174
|
+
) as pbar:
|
|
175
|
+
for future in as_completed(futures):
|
|
176
|
+
psd_path = futures[future]
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
success, message, count = future.result()
|
|
180
|
+
self._update_stats(success, count, psd_path if not success else None)
|
|
181
|
+
|
|
182
|
+
if self.verbose:
|
|
183
|
+
tqdm.write(message)
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
self._update_stats(False, 0, psd_path)
|
|
187
|
+
error_msg = f"✗ {psd_path.name}: Unexpected error - {e}"
|
|
188
|
+
if self.verbose:
|
|
189
|
+
tqdm.write(error_msg)
|
|
190
|
+
self.logger.error(error_msg)
|
|
191
|
+
|
|
192
|
+
pbar.update(1)
|
|
193
|
+
|
|
194
|
+
def _process_single_file_wrapper(self, psd_path: Path):
|
|
195
|
+
"""Wrapper for processing a single file (non-parallel)."""
|
|
196
|
+
try:
|
|
197
|
+
success, message, count = self._process_single_file(psd_path)
|
|
198
|
+
self._update_stats(success, count, psd_path if not success else None)
|
|
199
|
+
self.logger.info(message)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
self._update_stats(False, 0, psd_path)
|
|
202
|
+
error_msg = f"✗ {psd_path.name}: Unexpected error - {e}"
|
|
203
|
+
self.logger.error(error_msg)
|
|
204
|
+
|
|
205
|
+
def _process_single_file(self, psd_path: Path) -> Tuple[bool, str, int]:
|
|
206
|
+
"""
|
|
207
|
+
Process a single PSD file.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Tuple of (success: bool, message: str, layers_hidden: int)
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
dest_path = self._generate_output_path(psd_path)
|
|
214
|
+
|
|
215
|
+
# Check if output exists and overwrite is disabled
|
|
216
|
+
if dest_path.exists() and not self.overwrite:
|
|
217
|
+
return (
|
|
218
|
+
False,
|
|
219
|
+
f"⊘ {psd_path.name}: Skipped (output exists, overwrite=False)",
|
|
220
|
+
0,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Load and process PSD
|
|
224
|
+
processor = PSDProcessor(psd_path)
|
|
225
|
+
processor.load()
|
|
226
|
+
|
|
227
|
+
# Hide text layers
|
|
228
|
+
count = processor.hide_non_image_layers()
|
|
229
|
+
|
|
230
|
+
# Export to specified format
|
|
231
|
+
processor.export(dest_path, format=self.output_format)
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
True,
|
|
235
|
+
f"✓ {psd_path.name}: Hidden {count} text layer(s) → {dest_path.name}",
|
|
236
|
+
count,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
except FileNotFoundError as e:
|
|
240
|
+
return (False, f"✗ {psd_path.name}: File not found - {e}", 0)
|
|
241
|
+
|
|
242
|
+
except PermissionError as e:
|
|
243
|
+
return (False, f"✗ {psd_path.name}: Permission denied - {e}", 0)
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
return (False, f"✗ {psd_path.name}: Processing failed - {e}", 0)
|
|
247
|
+
|
|
248
|
+
def _update_stats(self, success: bool, layers_hidden: int, failed_path: Optional[Path] = None):
|
|
249
|
+
"""Update processing statistics."""
|
|
250
|
+
if success:
|
|
251
|
+
self.stats["success"] += 1
|
|
252
|
+
self.stats["total_layers_hidden"] += layers_hidden
|
|
253
|
+
else:
|
|
254
|
+
self.stats["failed"] += 1
|
|
255
|
+
if failed_path:
|
|
256
|
+
self.failed_files.append(failed_path)
|
|
257
|
+
|
|
258
|
+
def _get_files(self) -> List[Path]:
|
|
259
|
+
"""Generator that yields PSD files."""
|
|
260
|
+
pattern = "**/*.psd" if self.recursive else "*.psd"
|
|
261
|
+
for path in self.input_dir.glob(pattern):
|
|
262
|
+
if path.is_file():
|
|
263
|
+
yield path
|
|
264
|
+
|
|
265
|
+
def _generate_output_path(self, psd_path: Path) -> Path:
|
|
266
|
+
"""Generate the output file path, preserving directory structure if recursive."""
|
|
267
|
+
if self.recursive and self.output_dir != self.input_dir:
|
|
268
|
+
# Preserve subdirectory structure
|
|
269
|
+
relative_path = psd_path.relative_to(self.input_dir)
|
|
270
|
+
output_subdir = self.output_dir / relative_path.parent
|
|
271
|
+
output_subdir.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
return output_subdir / f"{psd_path.stem}.{self.output_format}"
|
|
273
|
+
else:
|
|
274
|
+
return self.output_dir / f"{psd_path.stem}.{self.output_format}"
|
|
275
|
+
|
|
276
|
+
def _copy_failed_file(self):
|
|
277
|
+
"""Copy failed file to output directory for review (if needed)."""
|
|
278
|
+
failed_dir = os.path.join(self.output_dir, "failed_files")
|
|
279
|
+
os.makedirs(failed_dir, exist_ok=True)
|
|
280
|
+
self.logger.info(f"Copying failed files to {failed_dir} for review.")
|
|
281
|
+
for file in self.failed_files:
|
|
282
|
+
shutil.copy(file, failed_dir)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _print_summary(self, total_files: int):
|
|
286
|
+
"""Print detailed processing summary."""
|
|
287
|
+
duration = self.stats["end_time"] - self.stats["start_time"]
|
|
288
|
+
|
|
289
|
+
print("\n" + "=" * 60)
|
|
290
|
+
print("PROCESSING COMPLETE")
|
|
291
|
+
print("=" * 60)
|
|
292
|
+
print(f"Total files: {total_files}")
|
|
293
|
+
print(f"✓ Successful: {self.stats['success']}")
|
|
294
|
+
print(f"✗ Failed: {self.stats['failed']}")
|
|
295
|
+
print(f"⊘ Skipped: {self.stats['skipped']}")
|
|
296
|
+
print(f"Text layers hidden: {self.stats['total_layers_hidden']}")
|
|
297
|
+
print(f"Processing time: {duration}")
|
|
298
|
+
print(f"Output directory: {self.output_dir}")
|
|
299
|
+
print("=" * 60)
|
|
300
|
+
|
|
301
|
+
# Log failed files if any
|
|
302
|
+
if self.failed_files:
|
|
303
|
+
print("\nFailed files:")
|
|
304
|
+
for failed_file in self.failed_files:
|
|
305
|
+
print(f" - {failed_file}")
|
|
306
|
+
|
|
307
|
+
# Log to file
|
|
308
|
+
self.logger.info(f"Batch job completed: {self.stats['success']}/{total_files} successful")
|
mil_kit/main.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from mil_kit.job import BatchJob
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
description="Batch hide text layers in PSDs and export PNGs."
|
|
9
|
+
)
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"-d",
|
|
12
|
+
"--dir",
|
|
13
|
+
required=True,
|
|
14
|
+
help="Input directory containing PSD files",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"-o",
|
|
18
|
+
"--output",
|
|
19
|
+
help="Output directory (default: input directory)",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"-f",
|
|
23
|
+
"--output-format",
|
|
24
|
+
default="png",
|
|
25
|
+
help="Output format (default: png)",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"-r",
|
|
29
|
+
"--recursive",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Process subdirectories recursively",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
job = BatchJob(args.dir, args.output, args.recursive, output_format=args.output_format)
|
|
38
|
+
job.run()
|
|
39
|
+
except Exception as e:
|
|
40
|
+
print(f"Critical Error: {e}")
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
main()
|
mil_kit/psd/__init__.py
ADDED
|
File without changes
|
mil_kit/psd/processor.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from psd_tools import PSDImage
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PSDProcessor:
|
|
6
|
+
"""
|
|
7
|
+
Handles the loading, modification, and exporting of a single PSD file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, file_path):
|
|
11
|
+
self.file_path = Path(file_path)
|
|
12
|
+
self.psd = None
|
|
13
|
+
self.hidden_count = 0
|
|
14
|
+
|
|
15
|
+
def load(self):
|
|
16
|
+
"""Loads the PSD file."""
|
|
17
|
+
try:
|
|
18
|
+
self.psd = PSDImage.open(self.file_path)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
raise IOError(f"Failed to open PSD: {e}")
|
|
21
|
+
|
|
22
|
+
def hide_text_layers(self):
|
|
23
|
+
"""Iterates through all layers and hides those of kind 'type'."""
|
|
24
|
+
if not self.psd:
|
|
25
|
+
raise RuntimeError("PSD not loaded. Call load() first.")
|
|
26
|
+
|
|
27
|
+
self.hidden_count = 0
|
|
28
|
+
# descendants() iterates recursively through groups
|
|
29
|
+
for layer in self.psd.descendants():
|
|
30
|
+
if layer.kind == "type" and layer.visible:
|
|
31
|
+
layer.visible = False
|
|
32
|
+
self.hidden_count += 1
|
|
33
|
+
|
|
34
|
+
return self.hidden_count
|
|
35
|
+
|
|
36
|
+
def hide_non_image_layers(self):
|
|
37
|
+
"""
|
|
38
|
+
Hides all non-raster layers including text, vectors, shapes, and adjustments.
|
|
39
|
+
Only keeps pixel/image layers visible.
|
|
40
|
+
"""
|
|
41
|
+
if not self.psd:
|
|
42
|
+
raise RuntimeError("PSD not loaded. Call load() first.")
|
|
43
|
+
|
|
44
|
+
self.hidden_count = 0
|
|
45
|
+
self.hidden_by_type = {
|
|
46
|
+
"type": 0, # Text layers
|
|
47
|
+
"shape": 0, # Vector/shape layers
|
|
48
|
+
"adjustment": 0, # Adjustment layers
|
|
49
|
+
"other": 0 # Other non-pixel layers
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# descendants() iterates recursively through groups
|
|
53
|
+
for layer in self.psd.descendants():
|
|
54
|
+
# Only keep pixel/image layers visible
|
|
55
|
+
if layer.visible and layer.kind != "pixel":
|
|
56
|
+
layer.visible = False
|
|
57
|
+
self.hidden_count += 1
|
|
58
|
+
|
|
59
|
+
# Track what type was hidden
|
|
60
|
+
if layer.kind in self.hidden_by_type:
|
|
61
|
+
self.hidden_by_type[layer.kind] += 1
|
|
62
|
+
else:
|
|
63
|
+
self.hidden_by_type["other"] += 1
|
|
64
|
+
|
|
65
|
+
return self.hidden_count
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def export(self, output_path, format="png"):
|
|
69
|
+
"""Composites the PSD and saves as PNG."""
|
|
70
|
+
if not self.psd:
|
|
71
|
+
raise RuntimeError("PSD not loaded.")
|
|
72
|
+
|
|
73
|
+
# Ensure the target directory exists
|
|
74
|
+
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
# Composite merges layers; save exports using PIL/Pillow
|
|
77
|
+
self.psd.composite().save(output_path, format=format.upper())
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mil-kit
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A toolkit for processing the ASM's Mammal Image Library (MIL).
|
|
5
|
+
Project-URL: Homepage, https://github.com/hhandika/mil-kit
|
|
6
|
+
Project-URL: Repository, https://github.com/hhandika/mil-kit
|
|
7
|
+
Project-URL: Issues, https://github.com/hhandika/mil-kit/issues
|
|
8
|
+
Author-email: Heru Handika <herubiolog@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: automation,batch-processing,image-processing,photoshop,psd
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: pillow>=12.0.0
|
|
25
|
+
Requires-Dist: psd-tools>=1.12.0
|
|
26
|
+
Requires-Dist: tqdm>=4.67.1
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# mil-kit
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+

|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
A Python toolkit for batch processing the Mammal Image Library (MIL) images. Reshape, convert, and optimize images for the mammal diversity database and other applications.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- 🚀 Batch process multiple PSD files in a directory
|
|
40
|
+
- ⚡ Parallel processing for faster execution
|
|
41
|
+
- 📊 Progress bar with detailed status
|
|
42
|
+
- 📝 Automatically hide all text layers
|
|
43
|
+
- 🖼️ Export processed files as PNG (default) or other formats
|
|
44
|
+
- 📁 Support for recursive directory processing
|
|
45
|
+
- ⚡ Preserve folder structure in output
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
Install using pip:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install mil-kit
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or using uv:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv add mil-kit
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
### Command Line
|
|
64
|
+
|
|
65
|
+
Process PSD files in a directory:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
mil-kit -d /path/to/psd/files
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Process recursively, specify output directory, and use JPEG format:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
mil-kit -d /path/to/psd/files -o /path/to/output -r -f jpeg
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Options
|
|
78
|
+
|
|
79
|
+
- `-d, --dir`: Input directory containing PSD files (required)
|
|
80
|
+
- `-o, --output`: Output directory for processed files (default: input directory)
|
|
81
|
+
- `-f, --output-format`: Output image format (default: png)
|
|
82
|
+
- `-r, --recursive`: Process subdirectories recursively
|
|
83
|
+
|
|
84
|
+
### Python API
|
|
85
|
+
|
|
86
|
+
You can also use mil-kit as a Python library:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from mil_kit.psd.processor import PSDProcessor
|
|
90
|
+
from mil_kit.job import BatchJob
|
|
91
|
+
|
|
92
|
+
# Process a single file
|
|
93
|
+
processor = PSDProcessor("image.psd")
|
|
94
|
+
processor.load()
|
|
95
|
+
processor.hide_non_image_layers()
|
|
96
|
+
processor.export("output.jpg", format="jpeg")
|
|
97
|
+
|
|
98
|
+
# Batch process
|
|
99
|
+
job = BatchJob(
|
|
100
|
+
input_dir="./psd_files",
|
|
101
|
+
output_dir="./output",
|
|
102
|
+
recursive=True,
|
|
103
|
+
output_format="png",
|
|
104
|
+
max_workers=4
|
|
105
|
+
)
|
|
106
|
+
job.run()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Requirements
|
|
110
|
+
|
|
111
|
+
- Python >= 3.10
|
|
112
|
+
- psd-tools >= 1.12.0
|
|
113
|
+
- pillow >= 12.0.0
|
|
114
|
+
- tqdm >= 4.67.1
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
119
|
+
|
|
120
|
+
## Contributing
|
|
121
|
+
|
|
122
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
123
|
+
|
|
124
|
+
## Issues
|
|
125
|
+
|
|
126
|
+
Report bugs and request features on [GitHub Issues](https://github.com/hhandika/psd-toolkit/issues).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
mil_kit/__init__.py,sha256=dsQfNrRHFHjthFmgDbgWHD2Ho2VUjv2glCibmLg9XDQ,57
|
|
2
|
+
mil_kit/job.py,sha256=TpPWMGVk00m4TDNixvGZHVXz_SlYidDfzyJAbyssHkc,11643
|
|
3
|
+
mil_kit/main.py,sha256=Yw8TpRWWnxVkjxfJ8EXUUPgoi6kVv_mF126jFiBVWD0,1020
|
|
4
|
+
mil_kit/psd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
mil_kit/psd/processor.py,sha256=mKYTZv32fKfuP_uBwjL4jwdWcMGK4rhu2uuDgvx9SrI,2577
|
|
6
|
+
mil_kit-0.3.0.dist-info/METADATA,sha256=XL5sJfhDfL4MyPUgoG61W9WmWym_1JAo3B6SZoPbNk8,3418
|
|
7
|
+
mil_kit-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
mil_kit-0.3.0.dist-info/entry_points.txt,sha256=pdJaTXH8FgBxIi_p4-yEKeWONK6kD4fqUJRE65dYukw,46
|
|
9
|
+
mil_kit-0.3.0.dist-info/licenses/LICENSE,sha256=KFqQz-OHVHOQWPwC7dROatkfKRXVDlH0wZnTJd44LsU,1069
|
|
10
|
+
mil_kit-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Heru Handika
|
|
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.
|