Skip to content

Commit a1e03ba

Browse files
authored
Merge pull request #9 from john-liu2/task/random
Add `orientation.py` and related tests. Move multiprocess feature into `common.py`. Finish `rotate.py`
2 parents 40e4729 + 9f3b3d1 commit a1e03ba

File tree

12 files changed

+431
-179
lines changed

12 files changed

+431
-179
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ clean:
77
rm -fr build .eggs batch_img.egg-info run_*.log .out dist wheels tests/data/.DS_Store
88
find . -name '*.pyc' -exec rm -f {} +
99
find . -name '__pycache__' -exec rm -fr {} +
10-
rm -fr tests/.out tests/.DS_Store .coverage htmlcov .pytest_cache uv.lock
10+
rm -fr tests/.out tests/.DS_Store .coverage* htmlcov .pytest_cache uv.lock
1111
rm -fr docs/build out_*.yaml tmp_*
1212

1313
lint: clean

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ Options:
5151
x>=0]
5252
-bc, --border_color TEXT Add border to image file(s) with the
5353
border_color string [default: gray]
54+
-o, --output TEXT Output file path. If skipped, use the
55+
current dir path [default: ""]
5456
--help Show this message and exit.
5557
```
5658

@@ -64,7 +66,9 @@ Usage: batch_img defaults [OPTIONS] SRC_PATH
6466
5-pixel gray color border; 3) auto-rotate if needed
6567
6668
Options:
67-
--help Show this message and exit.
69+
-o, --output TEXT Output file path. If skipped, use the current dir path
70+
[default: ""]
71+
--help Show this message and exit.
6872
```
6973

7074
#### The `resize` sub-command CLI options:
@@ -78,6 +82,8 @@ Usage: batch_img resize [OPTIONS] SRC_PATH
7882
Options:
7983
-l, --length INTEGER RANGE Resize image file(s) on original aspect ratio to
8084
the length. 0 - no resize [default: 0; x>=0]
85+
-o, --output TEXT Output file path. If skipped, use the current
86+
dir path [default: ""]
8187
--help Show this message and exit.
8288
```
8389

@@ -90,7 +96,9 @@ Usage: batch_img rotate [OPTIONS] SRC_PATH
9096
Rotate image file(s)
9197
9298
Options:
93-
-d, --degree INTEGER RANGE Rotate image file(s) to the degree clock-wise. 0
94-
- no rotate [default: 0; x>=0]
95-
--help Show this message and exit.
99+
-a, --angle INTEGER RANGE Rotate image file(s) to the clockwise angle. 0 -
100+
no rotate [default: 0; x>=0]
101+
-o, --output TEXT Output file path. If skipped, use the current dir
102+
path [default: ""]
103+
--help Show this message and exit.
96104
```

batch_img/common.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
Copyright © 2025 John Liu
33
"""
44

5+
import itertools
56
import json
67
import subprocess
78
import tomllib
89
from datetime import datetime
910
from importlib.metadata import version
11+
from multiprocessing import Pool, cpu_count
1012
from os.path import getmtime, getsize
1113
from pathlib import Path
1214

@@ -15,8 +17,9 @@
1517
from loguru import logger
1618
from PIL import Image, ImageChops
1719
from PIL.TiffImagePlugin import IFDRational
20+
from tqdm import tqdm
1821

19-
from batch_img.const import PKG_NAME, TS_FORMAT, VER
22+
from batch_img.const import PATTERNS, PKG_NAME, TS_FORMAT, VER
2023

2124
pillow_heif.register_heif_opener() # allow Pillow to open HEIC files
2225

@@ -194,3 +197,45 @@ def are_images_equal(path1: Path, path2: Path) -> bool:
194197
f"{path2}:\n{json.dumps(meta2, indent=2, default=Common.jsn_serial)}"
195198
)
196199
return ImageChops.difference(data1, data2).getbbox() is None
200+
201+
@staticmethod
202+
def prepare_all_files(in_path: Path, out_path: Path):
203+
"""
204+
205+
Args:
206+
in_path: input dir path
207+
out_path: output dir path
208+
209+
Returns:
210+
iterable: files list generator
211+
"""
212+
out_path.mkdir(parents=True, exist_ok=True)
213+
_files = itertools.chain.from_iterable(in_path.glob(p) for p in PATTERNS)
214+
return _files
215+
216+
@staticmethod
217+
def multiprocess_progress_bar(func, desc, tasks: list) -> int:
218+
"""Run task in multiprocess with progress bar
219+
220+
Args:
221+
func: function to be run in multiprocess
222+
desc: description str
223+
tasks: tasks list for multiprocess pool
224+
225+
Returns:
226+
int: success_cnt
227+
"""
228+
success_cnt = 0
229+
files_cnt = len(tasks)
230+
# Limit to 4 workers if cpu cores cnt > 4
231+
workers = min(cpu_count(), 4)
232+
233+
with Pool(workers) as pool:
234+
with tqdm(total=files_cnt, desc=desc) as pbar:
235+
for ok, res in pool.starmap(func, tasks):
236+
if ok:
237+
success_cnt += 1
238+
else:
239+
tqdm.write(f"Error: {res}")
240+
pbar.update()
241+
return success_cnt

batch_img/const.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,13 @@
1111
MSG_BAD = "❌ Failed to process the image file(s)."
1212

1313
TS_FORMAT = "%Y-%m-%d_%H-%M-%S"
14+
PATTERNS = (
15+
"*.HEIC",
16+
"*.heic",
17+
"*.JPG",
18+
"*.jpg",
19+
"*.JPEG",
20+
"*.jpeg",
21+
"*.PNG",
22+
"*.png",
23+
)

batch_img/interface.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,22 @@ def cli(ctx, version): # pragma: no cover
3838
show_default=True,
3939
help="Add border to image file(s) with the border_color string",
4040
)
41-
def border(src_path, border_width, border_color):
41+
@click.option(
42+
"-o",
43+
"--output",
44+
default="",
45+
show_default=True,
46+
type=str,
47+
help="Output file path. If skipped, use the current dir path",
48+
)
49+
def border(src_path, border_width, border_color, output):
4250
options = {
4351
"src_path": src_path,
4452
"border_width": border_width,
4553
"border_color": border_color,
54+
"output": output,
4655
}
47-
res = Main.add_border(options)
56+
res = Main.border(options)
4857
msg = MSG_OK if res else MSG_BAD
4958
click.secho(msg)
5059

@@ -57,15 +66,21 @@ def border(src_path, border_width, border_color):
5766
"src_path",
5867
required=True,
5968
)
60-
def defaults(src_path):
69+
@click.option(
70+
"-o",
71+
"--output",
72+
default="",
73+
show_default=True,
74+
type=str,
75+
help="Output file path. If skipped, use the current dir path",
76+
)
77+
def defaults(src_path, output):
6178
"""Do the default action on the image file(s):
6279
* Resize to 1280 pixels as the max length
6380
* Add a border: 5 pixel width, gray color
6481
* Auto-rotate if upside down or sideways
6582
"""
66-
options = {
67-
"src_path": src_path,
68-
}
83+
options = {"src_path": src_path, "output": output}
6984
res = Main.default_run(options)
7085
msg = MSG_OK if res else MSG_BAD
7186
click.secho(msg)
@@ -85,11 +100,16 @@ def defaults(src_path):
85100
type=click.IntRange(min=0),
86101
help="Resize image file(s) on original aspect ratio to the length. 0 - no resize",
87102
)
88-
def resize(src_path, length):
89-
options = {
90-
"src_path": src_path,
91-
"length": length,
92-
}
103+
@click.option(
104+
"-o",
105+
"--output",
106+
default="",
107+
show_default=True,
108+
type=str,
109+
help="Output file path. If skipped, use the current dir path",
110+
)
111+
def resize(src_path, length, output):
112+
options = {"src_path": src_path, "length": length, "output": output}
93113
res = Main.resize(options)
94114
msg = MSG_OK if res else MSG_BAD
95115
click.secho(msg)
@@ -101,18 +121,27 @@ def resize(src_path, length):
101121
required=True,
102122
)
103123
@click.option(
104-
"-d",
105-
"--degree",
124+
"-a",
125+
"--angle",
106126
is_flag=False,
107127
default=0,
108128
show_default=True,
109129
type=click.IntRange(min=0),
110-
help="Rotate image file(s) to the degree clock-wise. 0 - no rotate",
130+
help="Rotate image file(s) to the clockwise angle. 0 - no rotate",
131+
)
132+
@click.option(
133+
"-o",
134+
"--output",
135+
default="",
136+
show_default=True,
137+
type=str,
138+
help="Output file path. If skipped, use the current dir path",
111139
)
112-
def rotate(src_path, degree):
140+
def rotate(src_path, angle, output):
113141
options = {
114142
"src_path": src_path,
115-
"degree": degree,
143+
"angle": angle,
144+
"output": output,
116145
}
117146
res = Main.rotate(options)
118147
msg = MSG_OK if res else MSG_BAD

batch_img/main.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from batch_img.const import PKG_NAME, TS_FORMAT
1313
from batch_img.resize import Resize
14+
from batch_img.rotate import Rotate
1415

1516

1617
class Main:
@@ -39,18 +40,25 @@ def resize(options: dict) -> bool:
3940
"""
4041
logger.info(f"{json.dumps(options, indent=2)}")
4142
Main.init_log_file()
42-
# Resize one file
4343
in_path = Path(options["src_path"])
4444
length = options.get("length")
45-
if not length:
45+
output = options.get("output")
46+
if not length or length == 0:
4647
logger.warning(f"No resize due to bad {length=}")
4748
return False
48-
ok, _ = Resize.resize_an_image(in_path, Path(os.getcwd()), length)
49+
if not output:
50+
output = Path(os.getcwd())
51+
else:
52+
output = Path(output)
53+
if in_path.is_file():
54+
ok, _ = Resize.resize_an_image(in_path, output, length)
55+
else:
56+
ok = Resize.resize_all_progress_bar(in_path, output, length)
4957
return ok
5058

5159
@staticmethod
52-
def add_border(options: dict) -> bool:
53-
"""Add border to the image file(s)
60+
def rotate(options: dict) -> bool:
61+
"""Rotate the image file(s)
5462
5563
Args:
5664
options: input options dict
@@ -60,12 +68,25 @@ def add_border(options: dict) -> bool:
6068
"""
6169
logger.info(f"{json.dumps(options, indent=2)}")
6270
Main.init_log_file()
63-
# To-do
64-
return True
71+
in_path = Path(options["src_path"])
72+
angle = options.get("angle")
73+
output = options.get("output")
74+
if not angle or angle == 0:
75+
logger.warning(f"No rotate due to bad {angle=}")
76+
return False
77+
if not output:
78+
output = Path(os.getcwd())
79+
else:
80+
output = Path(output)
81+
if in_path.is_file():
82+
ok, _ = Rotate.rotate_1_image_file(in_path, output, angle)
83+
else:
84+
ok = Rotate.rotate_all_in_dir(in_path, output, angle)
85+
return ok
6586

6687
@staticmethod
67-
def rotate(options: dict) -> bool:
68-
"""Rotate the image file(s)
88+
def border(options: dict) -> bool:
89+
"""Add border to the image file(s)
6990
7091
Args:
7192
options: input options dict

0 commit comments

Comments
 (0)