fotolab 0.34.1__tar.gz → 0.34.3__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.
- {fotolab-0.34.1/src/fotolab.egg-info → fotolab-0.34.3}/PKG-INFO +1 -1
- {fotolab-0.34.1 → fotolab-0.34.3}/pyproject.toml +8 -8
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/__init__.py +12 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/__init__.py +1 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/animate.py +6 -30
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/auto.py +3 -10
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/border.py +14 -37
- fotolab-0.34.1/src/fotolab/subcommands/montage.py → fotolab-0.34.3/src/fotolab/subcommands/common.py +21 -46
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/contrast.py +27 -50
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/halftone.py +18 -41
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/info.py +3 -1
- fotolab-0.34.3/src/fotolab/subcommands/montage.py +72 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/resize.py +59 -83
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/rotate.py +12 -35
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/sharpen.py +22 -43
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/watermark.py +17 -38
- {fotolab-0.34.1 → fotolab-0.34.3/src/fotolab.egg-info}/PKG-INFO +1 -1
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab.egg-info/SOURCES.txt +1 -0
- fotolab-0.34.3/tests/test_resize_subcommand.py +63 -0
- fotolab-0.34.1/tests/test_resize_subcommand.py +0 -20
- {fotolab-0.34.1 → fotolab-0.34.3}/LICENSE.md +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/README.md +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/setup.cfg +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/__main__.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/cli.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab/subcommands/env.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab.egg-info/dependency_links.txt +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab.egg-info/entry_points.txt +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab.egg-info/requires.txt +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/src/fotolab.egg-info/top_level.txt +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_animate_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_auto_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_border_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_contrast_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_env_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_halftone_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_help_flag.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_info_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_montage_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_quiet_flag.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_rotate_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_sharpen_subcommand.py +0 -0
- {fotolab-0.34.1 → fotolab-0.34.3}/tests/test_watermark_subcommand.py +0 -0
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[tool.setuptools.packages.find]
|
|
6
|
-
where = ["src"]
|
|
7
|
-
|
|
8
1
|
[project]
|
|
9
2
|
name = "fotolab"
|
|
10
|
-
version = "0.34.
|
|
3
|
+
version = "0.34.3"
|
|
11
4
|
description = "A console program to manipulate photos."
|
|
12
5
|
authors = [{name = "Kian-Meng Ang", email = "kianmeng@cpan.org"}]
|
|
13
6
|
requires-python = ">=3.10"
|
|
@@ -59,6 +52,13 @@ lint = [
|
|
|
59
52
|
"ruff",
|
|
60
53
|
]
|
|
61
54
|
|
|
55
|
+
[build-system]
|
|
56
|
+
requires = ["setuptools>=61.0"]
|
|
57
|
+
build-backend = "setuptools.build_meta"
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["src"]
|
|
61
|
+
|
|
62
62
|
# verify through: uv run ruff check --show-settings
|
|
63
63
|
[tool.ruff]
|
|
64
64
|
line-length = 79
|
|
@@ -22,6 +22,8 @@ import logging
|
|
|
22
22
|
import os
|
|
23
23
|
import subprocess
|
|
24
24
|
import sys
|
|
25
|
+
from contextlib import contextmanager
|
|
26
|
+
from typing import Iterator
|
|
25
27
|
|
|
26
28
|
from PIL import Image
|
|
27
29
|
|
|
@@ -98,3 +100,13 @@ def open_image(filename: Path):
|
|
|
98
100
|
|
|
99
101
|
except (OSError, FileNotFoundError) as error:
|
|
100
102
|
log.error("Error opening image: %s -> %s", filename, error)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@contextmanager
|
|
106
|
+
def load_image(filename: Path) -> Iterator[Image.Image]:
|
|
107
|
+
"""Load image using a context manager to ensure file handle is closed."""
|
|
108
|
+
try:
|
|
109
|
+
image = Image.open(filename)
|
|
110
|
+
yield image
|
|
111
|
+
finally:
|
|
112
|
+
image.close()
|
|
@@ -20,9 +20,9 @@ import logging
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from contextlib import ExitStack
|
|
22
22
|
|
|
23
|
-
from PIL import Image
|
|
24
23
|
|
|
25
|
-
from fotolab import open_image
|
|
24
|
+
from fotolab import load_image, open_image
|
|
25
|
+
from .common import add_common_arguments, log_args_decorator
|
|
26
26
|
|
|
27
27
|
log = logging.getLogger(__name__)
|
|
28
28
|
|
|
@@ -48,14 +48,7 @@ def build_subparser(subparsers) -> None:
|
|
|
48
48
|
|
|
49
49
|
animate_parser.set_defaults(func=run)
|
|
50
50
|
|
|
51
|
-
animate_parser
|
|
52
|
-
dest="image_paths",
|
|
53
|
-
help="set the image filenames",
|
|
54
|
-
nargs="+",
|
|
55
|
-
type=str,
|
|
56
|
-
default=None,
|
|
57
|
-
metavar="IMAGE_PATHS",
|
|
58
|
-
)
|
|
51
|
+
add_common_arguments(animate_parser)
|
|
59
52
|
|
|
60
53
|
animate_parser.add_argument(
|
|
61
54
|
"-f",
|
|
@@ -91,15 +84,6 @@ def build_subparser(subparsers) -> None:
|
|
|
91
84
|
metavar="LOOP",
|
|
92
85
|
)
|
|
93
86
|
|
|
94
|
-
animate_parser.add_argument(
|
|
95
|
-
"-op",
|
|
96
|
-
"--open",
|
|
97
|
-
default=False,
|
|
98
|
-
action="store_true",
|
|
99
|
-
dest="open",
|
|
100
|
-
help="open the image using default program (default: '%(default)s')",
|
|
101
|
-
)
|
|
102
|
-
|
|
103
87
|
animate_parser.add_argument(
|
|
104
88
|
"--webp-quality",
|
|
105
89
|
dest="webp_quality",
|
|
@@ -131,14 +115,6 @@ def build_subparser(subparsers) -> None:
|
|
|
131
115
|
metavar="METHOD",
|
|
132
116
|
)
|
|
133
117
|
|
|
134
|
-
animate_parser.add_argument(
|
|
135
|
-
"-od",
|
|
136
|
-
"--output-dir",
|
|
137
|
-
dest="output_dir",
|
|
138
|
-
default="output",
|
|
139
|
-
help="set default output folder (default: '%(default)s')",
|
|
140
|
-
)
|
|
141
|
-
|
|
142
118
|
animate_parser.add_argument(
|
|
143
119
|
"-of",
|
|
144
120
|
"--output-filename",
|
|
@@ -148,6 +124,7 @@ def build_subparser(subparsers) -> None:
|
|
|
148
124
|
)
|
|
149
125
|
|
|
150
126
|
|
|
127
|
+
@log_args_decorator
|
|
151
128
|
def run(args: argparse.Namespace) -> None:
|
|
152
129
|
"""Run animate subcommand.
|
|
153
130
|
|
|
@@ -157,17 +134,16 @@ def run(args: argparse.Namespace) -> None:
|
|
|
157
134
|
Returns:
|
|
158
135
|
None
|
|
159
136
|
"""
|
|
160
|
-
log.debug(args)
|
|
161
137
|
|
|
162
138
|
image_filepaths = [Path(f) for f in args.image_paths]
|
|
163
139
|
first_image_filepath = image_filepaths[0]
|
|
164
140
|
other_frames = []
|
|
165
141
|
|
|
166
142
|
with ExitStack() as stack:
|
|
167
|
-
main_frame = stack.enter_context(
|
|
143
|
+
main_frame = stack.enter_context(load_image(first_image_filepath))
|
|
168
144
|
|
|
169
145
|
for image_filepath in image_filepaths[1:]:
|
|
170
|
-
img = stack.enter_context(
|
|
146
|
+
img = stack.enter_context(load_image(image_filepath))
|
|
171
147
|
other_frames.append(img)
|
|
172
148
|
|
|
173
149
|
if args.output_filename:
|
|
@@ -23,6 +23,7 @@ import fotolab.subcommands.contrast
|
|
|
23
23
|
import fotolab.subcommands.resize
|
|
24
24
|
import fotolab.subcommands.sharpen
|
|
25
25
|
import fotolab.subcommands.watermark
|
|
26
|
+
from .common import add_common_arguments, log_args_decorator
|
|
26
27
|
|
|
27
28
|
log = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -35,14 +36,7 @@ def build_subparser(subparsers) -> None:
|
|
|
35
36
|
|
|
36
37
|
auto_parser.set_defaults(func=run)
|
|
37
38
|
|
|
38
|
-
auto_parser
|
|
39
|
-
dest="image_paths",
|
|
40
|
-
help="set the image filename",
|
|
41
|
-
nargs="+",
|
|
42
|
-
type=str,
|
|
43
|
-
default=None,
|
|
44
|
-
metavar="IMAGE_PATHS",
|
|
45
|
-
)
|
|
39
|
+
add_common_arguments(auto_parser)
|
|
46
40
|
|
|
47
41
|
auto_parser.add_argument(
|
|
48
42
|
"-t",
|
|
@@ -65,6 +59,7 @@ def build_subparser(subparsers) -> None:
|
|
|
65
59
|
)
|
|
66
60
|
|
|
67
61
|
|
|
62
|
+
@log_args_decorator
|
|
68
63
|
def run(args: argparse.Namespace) -> None:
|
|
69
64
|
"""Run auto subcommand.
|
|
70
65
|
|
|
@@ -106,7 +101,6 @@ def run(args: argparse.Namespace) -> None:
|
|
|
106
101
|
combined_args.overwrite = True
|
|
107
102
|
combined_args.open = False
|
|
108
103
|
|
|
109
|
-
log.debug(args)
|
|
110
104
|
log.debug(combined_args)
|
|
111
105
|
|
|
112
106
|
fotolab.subcommands.resize.run(combined_args)
|
|
@@ -118,7 +112,6 @@ def run(args: argparse.Namespace) -> None:
|
|
|
118
112
|
output_filename = (
|
|
119
113
|
args.title.lower().replace(",", "").replace(" ", "_") + ".gif"
|
|
120
114
|
)
|
|
121
|
-
combined_args.output_dir = "output"
|
|
122
115
|
combined_args.format = "gif"
|
|
123
116
|
combined_args.duration = 2500
|
|
124
117
|
combined_args.loop = 0
|
|
@@ -20,9 +20,10 @@ import logging
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from typing import Tuple
|
|
22
22
|
|
|
23
|
-
from PIL import
|
|
23
|
+
from PIL import ImageColor, ImageOps
|
|
24
24
|
|
|
25
|
-
from fotolab import save_image
|
|
25
|
+
from fotolab import load_image, save_image
|
|
26
|
+
from .common import add_common_arguments, log_args_decorator
|
|
26
27
|
|
|
27
28
|
log = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -33,14 +34,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
33
34
|
|
|
34
35
|
border_parser.set_defaults(func=run)
|
|
35
36
|
|
|
36
|
-
border_parser
|
|
37
|
-
dest="image_paths",
|
|
38
|
-
help="set the image filenames",
|
|
39
|
-
nargs="+",
|
|
40
|
-
type=str,
|
|
41
|
-
default=None,
|
|
42
|
-
metavar="IMAGE_PATHS",
|
|
43
|
-
)
|
|
37
|
+
add_common_arguments(border_parser)
|
|
44
38
|
|
|
45
39
|
border_parser.add_argument(
|
|
46
40
|
"-c",
|
|
@@ -106,24 +100,8 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
106
100
|
metavar="WIDTH",
|
|
107
101
|
)
|
|
108
102
|
|
|
109
|
-
border_parser.add_argument(
|
|
110
|
-
"-op",
|
|
111
|
-
"--open",
|
|
112
|
-
default=False,
|
|
113
|
-
action="store_true",
|
|
114
|
-
dest="open",
|
|
115
|
-
help="open the image using default program (default: '%(default)s')",
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
border_parser.add_argument(
|
|
119
|
-
"-od",
|
|
120
|
-
"--output-dir",
|
|
121
|
-
dest="output_dir",
|
|
122
|
-
default="output",
|
|
123
|
-
help="set default output folder (default: '%(default)s')",
|
|
124
|
-
)
|
|
125
|
-
|
|
126
103
|
|
|
104
|
+
@log_args_decorator
|
|
127
105
|
def run(args: argparse.Namespace) -> None:
|
|
128
106
|
"""Run border subcommand.
|
|
129
107
|
|
|
@@ -133,18 +111,17 @@ def run(args: argparse.Namespace) -> None:
|
|
|
133
111
|
Returns:
|
|
134
112
|
None
|
|
135
113
|
"""
|
|
136
|
-
log.debug(args)
|
|
137
114
|
|
|
138
115
|
for image_filepath in [Path(f) for f in args.image_paths]:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
116
|
+
with load_image(image_filepath) as original_image:
|
|
117
|
+
border = get_border(args)
|
|
118
|
+
bordered_image = ImageOps.expand(
|
|
119
|
+
original_image,
|
|
120
|
+
border=border,
|
|
121
|
+
fill=ImageColor.getrgb(args.color),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
save_image(args, bordered_image, image_filepath, "border")
|
|
148
125
|
|
|
149
126
|
|
|
150
127
|
def get_border(
|
fotolab-0.34.1/src/fotolab/subcommands/montage.py → fotolab-0.34.3/src/fotolab/subcommands/common.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
5
|
+
#
|
|
5
6
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
6
7
|
# later version.
|
|
7
8
|
#
|
|
@@ -13,28 +14,34 @@
|
|
|
13
14
|
# You should have received a copy of the GNU Affero General Public License
|
|
14
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
16
|
|
|
16
|
-
"""
|
|
17
|
+
"""Common argument parsing for subcommands."""
|
|
17
18
|
|
|
18
19
|
import argparse
|
|
19
20
|
import logging
|
|
20
|
-
from
|
|
21
|
+
from functools import wraps
|
|
22
|
+
from typing import Callable
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
log = logging.getLogger(__name__)
|
|
23
25
|
|
|
24
|
-
from fotolab import save_image
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
def log_args_decorator(func: Callable) -> Callable:
|
|
28
|
+
"""Decorator to log the arguments passed to a function."""
|
|
27
29
|
|
|
30
|
+
@wraps(func)
|
|
31
|
+
def wrapper(args: argparse.Namespace) -> None:
|
|
32
|
+
log.debug(args)
|
|
33
|
+
return func(args)
|
|
34
|
+
|
|
35
|
+
return wrapper
|
|
28
36
|
|
|
29
|
-
def build_subparser(subparsers) -> None:
|
|
30
|
-
"""Build the subparser."""
|
|
31
|
-
montage_parser = subparsers.add_parser(
|
|
32
|
-
"montage", help="montage a list of image"
|
|
33
|
-
)
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
def add_common_arguments(parser: argparse.ArgumentParser) -> None:
|
|
39
|
+
"""Add common arguments to a subparser.
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
Args:
|
|
42
|
+
parser (argparse.ArgumentParser): The subparser to add arguments to.
|
|
43
|
+
"""
|
|
44
|
+
parser.add_argument(
|
|
38
45
|
dest="image_paths",
|
|
39
46
|
help="set the image filenames",
|
|
40
47
|
nargs="+",
|
|
@@ -43,7 +50,7 @@ def build_subparser(subparsers) -> None:
|
|
|
43
50
|
metavar="IMAGE_PATHS",
|
|
44
51
|
)
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
parser.add_argument(
|
|
47
54
|
"-op",
|
|
48
55
|
"--open",
|
|
49
56
|
default=False,
|
|
@@ -52,42 +59,10 @@ def build_subparser(subparsers) -> None:
|
|
|
52
59
|
help="open the image using default program (default: '%(default)s')",
|
|
53
60
|
)
|
|
54
61
|
|
|
55
|
-
|
|
62
|
+
parser.add_argument(
|
|
56
63
|
"-od",
|
|
57
64
|
"--output-dir",
|
|
58
65
|
dest="output_dir",
|
|
59
66
|
default="output",
|
|
60
67
|
help="set default output folder (default: '%(default)s')",
|
|
61
68
|
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def run(args: argparse.Namespace) -> None:
|
|
65
|
-
"""Run montage subcommand.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
args (argparse.Namespace): Config from command line arguments
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
None
|
|
72
|
-
"""
|
|
73
|
-
log.debug(args)
|
|
74
|
-
images = []
|
|
75
|
-
for image_path_str in args.image_paths:
|
|
76
|
-
image_filename = Path(image_path_str)
|
|
77
|
-
images.append(Image.open(image_filename))
|
|
78
|
-
|
|
79
|
-
if len(images) < 2:
|
|
80
|
-
raise ValueError("at least two images is required for montage")
|
|
81
|
-
|
|
82
|
-
total_width = sum(img.width for img in images)
|
|
83
|
-
total_height = max(img.height for img in images)
|
|
84
|
-
|
|
85
|
-
montaged_image = Image.new("RGB", (total_width, total_height))
|
|
86
|
-
|
|
87
|
-
x_offset = 0
|
|
88
|
-
for image in images:
|
|
89
|
-
montaged_image.paste(image, (x_offset, 0))
|
|
90
|
-
x_offset += image.width
|
|
91
|
-
|
|
92
|
-
output_image_filename = Path(args.image_paths[0])
|
|
93
|
-
save_image(args, montaged_image, output_image_filename, "montage")
|
|
@@ -19,9 +19,10 @@ import argparse
|
|
|
19
19
|
import logging
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
|
|
22
|
-
from PIL import
|
|
22
|
+
from PIL import ImageOps
|
|
23
23
|
|
|
24
|
-
from fotolab import save_image
|
|
24
|
+
from fotolab import load_image, save_image
|
|
25
|
+
from .common import add_common_arguments, log_args_decorator
|
|
25
26
|
|
|
26
27
|
log = logging.getLogger(__name__)
|
|
27
28
|
|
|
@@ -50,14 +51,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
50
51
|
|
|
51
52
|
contrast_parser.set_defaults(func=run)
|
|
52
53
|
|
|
53
|
-
contrast_parser
|
|
54
|
-
dest="image_paths",
|
|
55
|
-
help="set the image filename",
|
|
56
|
-
nargs="+",
|
|
57
|
-
type=str,
|
|
58
|
-
default=None,
|
|
59
|
-
metavar="IMAGE_PATHS",
|
|
60
|
-
)
|
|
54
|
+
add_common_arguments(contrast_parser)
|
|
61
55
|
|
|
62
56
|
contrast_parser.add_argument(
|
|
63
57
|
"-c",
|
|
@@ -73,24 +67,8 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
73
67
|
metavar="CUTOFF",
|
|
74
68
|
)
|
|
75
69
|
|
|
76
|
-
contrast_parser.add_argument(
|
|
77
|
-
"-op",
|
|
78
|
-
"--open",
|
|
79
|
-
default=False,
|
|
80
|
-
action="store_true",
|
|
81
|
-
dest="open",
|
|
82
|
-
help="open the image using default program (default: '%(default)s')",
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
contrast_parser.add_argument(
|
|
86
|
-
"-od",
|
|
87
|
-
"--output-dir",
|
|
88
|
-
dest="output_dir",
|
|
89
|
-
default="output",
|
|
90
|
-
help="set default output folder (default: '%(default)s')",
|
|
91
|
-
)
|
|
92
|
-
|
|
93
70
|
|
|
71
|
+
@log_args_decorator
|
|
94
72
|
def run(args: argparse.Namespace) -> None:
|
|
95
73
|
"""Run contrast subcommand.
|
|
96
74
|
|
|
@@ -100,28 +78,27 @@ def run(args: argparse.Namespace) -> None:
|
|
|
100
78
|
Returns:
|
|
101
79
|
None
|
|
102
80
|
"""
|
|
103
|
-
log.debug(args)
|
|
104
81
|
|
|
105
82
|
for image_path_str in args.image_paths:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
83
|
+
with load_image(Path(image_path_str)) as original_image:
|
|
84
|
+
if original_image.mode == "RGBA":
|
|
85
|
+
# Split the image into RGB and Alpha channels
|
|
86
|
+
rgb_image = original_image.convert("RGB")
|
|
87
|
+
alpha_channel = original_image.getchannel("A")
|
|
88
|
+
|
|
89
|
+
# Apply autocontrast to the RGB part
|
|
90
|
+
contrasted_rgb = ImageOps.autocontrast(
|
|
91
|
+
rgb_image, cutoff=args.cutoff
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Merge the contrasted RGB part with the original Alpha channel
|
|
95
|
+
contrasted_rgb.putalpha(alpha_channel)
|
|
96
|
+
contrast_image = contrasted_rgb
|
|
97
|
+
else:
|
|
98
|
+
# For other modes (like RGB, L, etc.), apply autocontrast
|
|
99
|
+
# directly
|
|
100
|
+
contrast_image = ImageOps.autocontrast(
|
|
101
|
+
original_image, cutoff=args.cutoff
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
save_image(args, contrast_image, Path(image_path_str), "contrast")
|
|
@@ -23,7 +23,8 @@ from typing import NamedTuple
|
|
|
23
23
|
|
|
24
24
|
from PIL import Image, ImageDraw
|
|
25
25
|
|
|
26
|
-
from fotolab import save_gif_image, save_image
|
|
26
|
+
from fotolab import load_image, save_gif_image, save_image
|
|
27
|
+
from .common import add_common_arguments, log_args_decorator
|
|
27
28
|
|
|
28
29
|
log = logging.getLogger(__name__)
|
|
29
30
|
|
|
@@ -44,14 +45,7 @@ def build_subparser(subparsers) -> None:
|
|
|
44
45
|
|
|
45
46
|
halftone_parser.set_defaults(func=run)
|
|
46
47
|
|
|
47
|
-
halftone_parser
|
|
48
|
-
dest="image_paths",
|
|
49
|
-
help="set the image filename",
|
|
50
|
-
nargs="+",
|
|
51
|
-
type=str,
|
|
52
|
-
default=None,
|
|
53
|
-
metavar="IMAGE_PATHS",
|
|
54
|
-
)
|
|
48
|
+
add_common_arguments(halftone_parser)
|
|
55
49
|
|
|
56
50
|
halftone_parser.add_argument(
|
|
57
51
|
"-ba",
|
|
@@ -62,23 +56,6 @@ def build_subparser(subparsers) -> None:
|
|
|
62
56
|
help="generate a GIF showing before and after changes",
|
|
63
57
|
)
|
|
64
58
|
|
|
65
|
-
halftone_parser.add_argument(
|
|
66
|
-
"-op",
|
|
67
|
-
"--open",
|
|
68
|
-
default=False,
|
|
69
|
-
action="store_true",
|
|
70
|
-
dest="open",
|
|
71
|
-
help="open the image using default program (default: '%(default)s')",
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
halftone_parser.add_argument(
|
|
75
|
-
"-od",
|
|
76
|
-
"--output-dir",
|
|
77
|
-
dest="output_dir",
|
|
78
|
-
default="output",
|
|
79
|
-
help="set default output folder (default: '%(default)s')",
|
|
80
|
-
)
|
|
81
|
-
|
|
82
59
|
halftone_parser.add_argument(
|
|
83
60
|
"-c",
|
|
84
61
|
"--cells",
|
|
@@ -100,6 +77,7 @@ def build_subparser(subparsers) -> None:
|
|
|
100
77
|
)
|
|
101
78
|
|
|
102
79
|
|
|
80
|
+
@log_args_decorator
|
|
103
81
|
def run(args: argparse.Namespace) -> None:
|
|
104
82
|
"""Run halftone subcommand.
|
|
105
83
|
|
|
@@ -109,25 +87,24 @@ def run(args: argparse.Namespace) -> None:
|
|
|
109
87
|
Returns:
|
|
110
88
|
None
|
|
111
89
|
"""
|
|
112
|
-
log.debug(args)
|
|
113
90
|
|
|
114
91
|
for image_path_str in args.image_paths:
|
|
115
92
|
image_filename = Path(image_path_str)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
if args.before_after:
|
|
122
|
-
save_gif_image(
|
|
123
|
-
args,
|
|
124
|
-
image_filename,
|
|
125
|
-
original_image,
|
|
126
|
-
halftone_image,
|
|
127
|
-
"halftone",
|
|
93
|
+
with load_image(image_filename) as original_image:
|
|
94
|
+
halftone_image = create_halftone_image(
|
|
95
|
+
original_image, args.cells, args.grayscale
|
|
128
96
|
)
|
|
129
|
-
|
|
130
|
-
|
|
97
|
+
|
|
98
|
+
if args.before_after:
|
|
99
|
+
save_gif_image(
|
|
100
|
+
args,
|
|
101
|
+
image_filename,
|
|
102
|
+
original_image,
|
|
103
|
+
halftone_image,
|
|
104
|
+
"halftone",
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
save_image(args, halftone_image, image_filename, "halftone")
|
|
131
108
|
|
|
132
109
|
|
|
133
110
|
def _draw_halftone_dot(
|
|
@@ -20,6 +20,8 @@ import logging
|
|
|
20
20
|
|
|
21
21
|
from PIL import ExifTags, Image
|
|
22
22
|
|
|
23
|
+
from .common import log_args_decorator
|
|
24
|
+
|
|
23
25
|
log = logging.getLogger(__name__)
|
|
24
26
|
|
|
25
27
|
|
|
@@ -62,6 +64,7 @@ def build_subparser(subparsers) -> None:
|
|
|
62
64
|
)
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
@log_args_decorator
|
|
65
68
|
def run(args: argparse.Namespace) -> None:
|
|
66
69
|
"""Run info subcommand.
|
|
67
70
|
|
|
@@ -71,7 +74,6 @@ def run(args: argparse.Namespace) -> None:
|
|
|
71
74
|
Returns:
|
|
72
75
|
None
|
|
73
76
|
"""
|
|
74
|
-
log.debug(args)
|
|
75
77
|
|
|
76
78
|
with Image.open(args.image_filename) as image:
|
|
77
79
|
exif_tags = extract_exif_tags(image, args.sort)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify it under
|
|
4
|
+
# the terms of the GNU Affero General Public License as published by the Free
|
|
5
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
6
|
+
# later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
9
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
10
|
+
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
11
|
+
# details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""Montage subcommand."""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from contextlib import ExitStack
|
|
22
|
+
|
|
23
|
+
from PIL import Image
|
|
24
|
+
|
|
25
|
+
from fotolab import load_image, save_image
|
|
26
|
+
from .common import add_common_arguments, log_args_decorator
|
|
27
|
+
|
|
28
|
+
log = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_subparser(subparsers) -> None:
|
|
32
|
+
"""Build the subparser."""
|
|
33
|
+
montage_parser = subparsers.add_parser(
|
|
34
|
+
"montage", help="montage a list of image"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
montage_parser.set_defaults(func=run)
|
|
38
|
+
|
|
39
|
+
add_common_arguments(montage_parser)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@log_args_decorator
|
|
43
|
+
def run(args: argparse.Namespace) -> None:
|
|
44
|
+
"""Run montage subcommand.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
args (argparse.Namespace): Config from command line arguments
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
None
|
|
51
|
+
"""
|
|
52
|
+
images = []
|
|
53
|
+
with ExitStack() as stack:
|
|
54
|
+
for image_path_str in args.image_paths:
|
|
55
|
+
image_filename = Path(image_path_str)
|
|
56
|
+
images.append(stack.enter_context(load_image(image_filename)))
|
|
57
|
+
|
|
58
|
+
if len(images) < 2:
|
|
59
|
+
raise ValueError("at least two images is required for montage")
|
|
60
|
+
|
|
61
|
+
total_width = sum(img.width for img in images)
|
|
62
|
+
total_height = max(img.height for img in images)
|
|
63
|
+
|
|
64
|
+
montaged_image = Image.new("RGB", (total_width, total_height))
|
|
65
|
+
|
|
66
|
+
x_offset = 0
|
|
67
|
+
for image in images:
|
|
68
|
+
montaged_image.paste(image, (x_offset, 0))
|
|
69
|
+
x_offset += image.width
|
|
70
|
+
|
|
71
|
+
output_image_filename = Path(args.image_paths[0])
|
|
72
|
+
save_image(args, montaged_image, output_image_filename, "montage")
|