fotolab 0.21.1__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.
- fotolab/__init__.py +67 -0
- fotolab/__main__.py +22 -0
- fotolab/animate.py +114 -0
- fotolab/auto.py +83 -0
- fotolab/border.py +139 -0
- fotolab/cli.py +180 -0
- fotolab/contrast.py +77 -0
- fotolab/env.py +52 -0
- fotolab/info.py +103 -0
- fotolab/montage.py +71 -0
- fotolab/resize.py +176 -0
- fotolab/rotate.py +61 -0
- fotolab/sharpen.py +98 -0
- fotolab/watermark.py +262 -0
- fotolab-0.21.1.dist-info/LICENSE.md +616 -0
- fotolab-0.21.1.dist-info/METADATA +399 -0
- fotolab-0.21.1.dist-info/RECORD +19 -0
- fotolab-0.21.1.dist-info/WHEEL +4 -0
- fotolab-0.21.1.dist-info/entry_points.txt +3 -0
fotolab/__init__.py
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Copyright (C) 2024 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
|
+
"""A console program that manipulate images."""
|
17
|
+
|
18
|
+
import logging
|
19
|
+
import os
|
20
|
+
import subprocess
|
21
|
+
import sys
|
22
|
+
from pathlib import Path
|
23
|
+
|
24
|
+
__version__ = "0.21.1"
|
25
|
+
|
26
|
+
log = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
|
29
|
+
def save_image(args, new_image, output_filename, subcommand):
|
30
|
+
"""Save image after image operation.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
args (argparse.Namespace): Config from command line arguments
|
34
|
+
new_image(PIL.Image.Image): Modified image
|
35
|
+
subcommand(str): Subcommand used to call this function
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
None
|
39
|
+
"""
|
40
|
+
image_file = Path(output_filename)
|
41
|
+
|
42
|
+
if args.overwrite:
|
43
|
+
new_filename = image_file.with_name(image_file.name)
|
44
|
+
else:
|
45
|
+
new_filename = Path(
|
46
|
+
args.output_dir,
|
47
|
+
image_file.with_name(f"{subcommand}_{image_file.name}"),
|
48
|
+
)
|
49
|
+
new_filename.parent.mkdir(parents=True, exist_ok=True)
|
50
|
+
|
51
|
+
log.info("%s image: %s", subcommand, new_filename.resolve())
|
52
|
+
new_image.save(new_filename)
|
53
|
+
|
54
|
+
if args.open:
|
55
|
+
_open_image(new_filename)
|
56
|
+
|
57
|
+
|
58
|
+
def _open_image(filename):
|
59
|
+
"""Open generated image using default program."""
|
60
|
+
if sys.platform == "linux":
|
61
|
+
subprocess.call(["xdg-open", filename])
|
62
|
+
elif sys.platform == "darwin":
|
63
|
+
subprocess.call(["open", filename])
|
64
|
+
elif sys.platform == "windows":
|
65
|
+
os.startfile(filename)
|
66
|
+
|
67
|
+
log.info("open image: %s", filename.resolve())
|
fotolab/__main__.py
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright (C) 2023 Kian-Meng Ang
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU Affero General Public License as published
|
5
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU Affero General Public License for more 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
|
+
"""Main entry point when running as module."""
|
17
|
+
|
18
|
+
from fotolab.cli import main
|
19
|
+
|
20
|
+
if __name__ == "__main__":
|
21
|
+
main()
|
22
|
+
raise SystemExit()
|
fotolab/animate.py
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Copyright (C) 2024 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
|
+
"""Animate subcommand."""
|
17
|
+
|
18
|
+
import argparse
|
19
|
+
import logging
|
20
|
+
from pathlib import Path
|
21
|
+
|
22
|
+
from PIL import Image
|
23
|
+
|
24
|
+
from fotolab import _open_image
|
25
|
+
|
26
|
+
log = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
|
29
|
+
def build_subparser(subparsers) -> None:
|
30
|
+
"""Build the subparser."""
|
31
|
+
animate_parser = subparsers.add_parser("animate", help="animate an image")
|
32
|
+
|
33
|
+
animate_parser.set_defaults(func=run)
|
34
|
+
|
35
|
+
animate_parser.add_argument(
|
36
|
+
dest="image_filenames",
|
37
|
+
help="set the image filenames",
|
38
|
+
nargs="+",
|
39
|
+
type=str,
|
40
|
+
default=None,
|
41
|
+
metavar="IMAGE_FILENAMES",
|
42
|
+
)
|
43
|
+
|
44
|
+
animate_parser.add_argument(
|
45
|
+
"-f",
|
46
|
+
"--format",
|
47
|
+
dest="format",
|
48
|
+
type=str,
|
49
|
+
choices=["gif", "webp"],
|
50
|
+
default="gif",
|
51
|
+
help="set the image format (default: '%(default)s')",
|
52
|
+
metavar="FORMAT",
|
53
|
+
)
|
54
|
+
|
55
|
+
animate_parser.add_argument(
|
56
|
+
"-d",
|
57
|
+
"--duration",
|
58
|
+
dest="duration",
|
59
|
+
type=int,
|
60
|
+
default=2500,
|
61
|
+
help="set the duration in milliseconds (default: '%(default)s')",
|
62
|
+
metavar="DURATION",
|
63
|
+
)
|
64
|
+
|
65
|
+
animate_parser.add_argument(
|
66
|
+
"-l",
|
67
|
+
"--loop",
|
68
|
+
dest="loop",
|
69
|
+
type=int,
|
70
|
+
default=0,
|
71
|
+
help="set the loop cycle (default: '%(default)s')",
|
72
|
+
metavar="LOOP",
|
73
|
+
)
|
74
|
+
|
75
|
+
|
76
|
+
def run(args: argparse.Namespace) -> None:
|
77
|
+
"""Run animate subcommand.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
config (argparse.Namespace): Config from command line arguments
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
None
|
84
|
+
"""
|
85
|
+
log.debug(args)
|
86
|
+
|
87
|
+
first_image = args.image_filenames[0]
|
88
|
+
animated_image = Image.open(first_image)
|
89
|
+
|
90
|
+
append_images = []
|
91
|
+
for image_filename in args.image_filenames[1:]:
|
92
|
+
append_images.append(Image.open(image_filename))
|
93
|
+
|
94
|
+
image_file = Path(first_image)
|
95
|
+
new_filename = Path(
|
96
|
+
args.output_dir,
|
97
|
+
image_file.with_name(f"animate_{image_file.stem}.{args.format}"),
|
98
|
+
)
|
99
|
+
new_filename.parent.mkdir(parents=True, exist_ok=True)
|
100
|
+
|
101
|
+
log.info("animate image: %s", new_filename)
|
102
|
+
|
103
|
+
animated_image.save(
|
104
|
+
new_filename,
|
105
|
+
format=args.format,
|
106
|
+
append_images=append_images,
|
107
|
+
save_all=True,
|
108
|
+
duration=args.duration,
|
109
|
+
loop=args.loop,
|
110
|
+
optimize=True,
|
111
|
+
)
|
112
|
+
|
113
|
+
if args.open:
|
114
|
+
_open_image(new_filename)
|
fotolab/auto.py
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Copyright (C) 2024 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
|
+
"""Auto subcommand."""
|
17
|
+
|
18
|
+
import argparse
|
19
|
+
import logging
|
20
|
+
|
21
|
+
import fotolab.contrast
|
22
|
+
import fotolab.resize
|
23
|
+
import fotolab.sharpen
|
24
|
+
import fotolab.watermark
|
25
|
+
|
26
|
+
log = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
|
29
|
+
def build_subparser(subparsers) -> None:
|
30
|
+
"""Build the subparser."""
|
31
|
+
auto_parser = subparsers.add_parser(
|
32
|
+
"auto", help="auto adjust (resize, contrast, and watermark) a photo"
|
33
|
+
)
|
34
|
+
|
35
|
+
auto_parser.set_defaults(func=run)
|
36
|
+
|
37
|
+
auto_parser.add_argument(
|
38
|
+
dest="image_filenames",
|
39
|
+
help="set the image filename",
|
40
|
+
nargs="+",
|
41
|
+
type=str,
|
42
|
+
default=None,
|
43
|
+
metavar="IMAGE_FILENAMES",
|
44
|
+
)
|
45
|
+
|
46
|
+
|
47
|
+
def run(args: argparse.Namespace) -> None:
|
48
|
+
"""Run auto subcommand.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
config (argparse.Namespace): Config from command line arguments
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
None
|
55
|
+
"""
|
56
|
+
extra_args = {
|
57
|
+
"width": 600,
|
58
|
+
"height": 277,
|
59
|
+
"cutoff": 1,
|
60
|
+
"radius": 1,
|
61
|
+
"percent": 100,
|
62
|
+
"threshold": 2,
|
63
|
+
"text": "kianmeng.org",
|
64
|
+
"position": "bottom-left",
|
65
|
+
"font_size": 12,
|
66
|
+
"font_color": "white",
|
67
|
+
"outline_width": 2,
|
68
|
+
"outline_color": "black",
|
69
|
+
"padding": 15,
|
70
|
+
"camera": False,
|
71
|
+
"canvas": False,
|
72
|
+
"lowercase": False,
|
73
|
+
}
|
74
|
+
combined_args = argparse.Namespace(**vars(args), **extra_args)
|
75
|
+
combined_args.overwrite = True
|
76
|
+
combined_args.open = False
|
77
|
+
log.debug(args)
|
78
|
+
log.debug(combined_args)
|
79
|
+
|
80
|
+
fotolab.resize.run(combined_args)
|
81
|
+
fotolab.contrast.run(combined_args)
|
82
|
+
fotolab.sharpen.run(combined_args)
|
83
|
+
fotolab.watermark.run(combined_args)
|
fotolab/border.py
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# Copyright (C) 2024 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
|
+
"""Border subcommand."""
|
17
|
+
|
18
|
+
import argparse
|
19
|
+
import logging
|
20
|
+
|
21
|
+
from PIL import Image, ImageColor, ImageOps
|
22
|
+
|
23
|
+
from fotolab import save_image
|
24
|
+
|
25
|
+
log = logging.getLogger(__name__)
|
26
|
+
|
27
|
+
|
28
|
+
def build_subparser(subparsers) -> None:
|
29
|
+
"""Build the subparser."""
|
30
|
+
border_parser = subparsers.add_parser("border", help="add border to image")
|
31
|
+
|
32
|
+
border_parser.set_defaults(func=run)
|
33
|
+
|
34
|
+
border_parser.add_argument(
|
35
|
+
dest="image_filenames",
|
36
|
+
help="set the image filenames",
|
37
|
+
nargs="+",
|
38
|
+
type=str,
|
39
|
+
default=None,
|
40
|
+
metavar="IMAGE_FILENAMES",
|
41
|
+
)
|
42
|
+
|
43
|
+
border_parser.add_argument(
|
44
|
+
"-c",
|
45
|
+
"--color",
|
46
|
+
dest="color",
|
47
|
+
type=str,
|
48
|
+
default="black",
|
49
|
+
help="set the color of border (default: '%(default)s')",
|
50
|
+
metavar="COLOR",
|
51
|
+
)
|
52
|
+
|
53
|
+
border_parser.add_argument(
|
54
|
+
"-w",
|
55
|
+
"--width",
|
56
|
+
dest="width",
|
57
|
+
type=str,
|
58
|
+
default=10,
|
59
|
+
help="set the width of border (default: '%(default)s')",
|
60
|
+
metavar="WIDTH",
|
61
|
+
)
|
62
|
+
|
63
|
+
border_parser.add_argument(
|
64
|
+
"-wt",
|
65
|
+
"--width-top",
|
66
|
+
dest="width_top",
|
67
|
+
type=str,
|
68
|
+
default=0,
|
69
|
+
help="set the width of top border (default: '%(default)s')",
|
70
|
+
metavar="WIDTH",
|
71
|
+
)
|
72
|
+
|
73
|
+
border_parser.add_argument(
|
74
|
+
"-wr",
|
75
|
+
"--width-right",
|
76
|
+
dest="width_right",
|
77
|
+
type=str,
|
78
|
+
default=0,
|
79
|
+
help="set the width of right border (default: '%(default)s')",
|
80
|
+
metavar="WIDTH",
|
81
|
+
)
|
82
|
+
|
83
|
+
border_parser.add_argument(
|
84
|
+
"-wb",
|
85
|
+
"--width-bottom",
|
86
|
+
dest="width_bottom",
|
87
|
+
type=str,
|
88
|
+
default=0,
|
89
|
+
help="set the width of bottom border (default: '%(default)s')",
|
90
|
+
metavar="WIDTH",
|
91
|
+
)
|
92
|
+
|
93
|
+
border_parser.add_argument(
|
94
|
+
"-wl",
|
95
|
+
"--width-left",
|
96
|
+
dest="width_left",
|
97
|
+
type=str,
|
98
|
+
default=0,
|
99
|
+
help="set the width of left border (default: '%(default)s')",
|
100
|
+
metavar="WIDTH",
|
101
|
+
)
|
102
|
+
|
103
|
+
|
104
|
+
def run(args: argparse.Namespace) -> None:
|
105
|
+
"""Run border subcommand.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
config (argparse.Namespace): Config from command line arguments
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
None
|
112
|
+
"""
|
113
|
+
log.debug(args)
|
114
|
+
|
115
|
+
for image_filename in args.image_filenames:
|
116
|
+
original_image = Image.open(image_filename)
|
117
|
+
|
118
|
+
if (
|
119
|
+
args.width_left
|
120
|
+
or args.width_top
|
121
|
+
or args.width_right
|
122
|
+
or args.width_bottom
|
123
|
+
):
|
124
|
+
border = (
|
125
|
+
int(args.width_left),
|
126
|
+
int(args.width_top),
|
127
|
+
int(args.width_right),
|
128
|
+
int(args.width_bottom),
|
129
|
+
)
|
130
|
+
else:
|
131
|
+
border = args.width
|
132
|
+
|
133
|
+
bordered_image = ImageOps.expand(
|
134
|
+
original_image,
|
135
|
+
border=border,
|
136
|
+
fill=ImageColor.getrgb(args.color),
|
137
|
+
)
|
138
|
+
|
139
|
+
save_image(args, bordered_image, image_filename, "watermark")
|
fotolab/cli.py
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
# Copyright (c) 2024 Kian-Meng Ang
|
2
|
+
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU Generals Public License for more details.
|
12
|
+
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
"""A console program to manipulate photos.
|
17
|
+
|
18
|
+
website: https://github.com/kianmeng/fotolab
|
19
|
+
changelog: https://github.com/kianmeng/fotolab/blob/master/CHANGELOG.md
|
20
|
+
issues: https://github.com/kianmeng/fotolab/issues
|
21
|
+
"""
|
22
|
+
|
23
|
+
import argparse
|
24
|
+
import logging
|
25
|
+
import sys
|
26
|
+
from typing import Dict, Optional, Sequence
|
27
|
+
|
28
|
+
import fotolab.animate
|
29
|
+
import fotolab.auto
|
30
|
+
import fotolab.border
|
31
|
+
import fotolab.contrast
|
32
|
+
import fotolab.env
|
33
|
+
import fotolab.info
|
34
|
+
import fotolab.montage
|
35
|
+
import fotolab.resize
|
36
|
+
import fotolab.rotate
|
37
|
+
import fotolab.sharpen
|
38
|
+
import fotolab.watermark
|
39
|
+
from fotolab import __version__
|
40
|
+
|
41
|
+
log = logging.getLogger(__name__)
|
42
|
+
|
43
|
+
|
44
|
+
def setup_logging(args: argparse.Namespace) -> None:
|
45
|
+
"""Set up logging by level."""
|
46
|
+
if args.verbose == 0:
|
47
|
+
logging.getLogger("PIL").setLevel(logging.ERROR)
|
48
|
+
|
49
|
+
if args.quiet:
|
50
|
+
logging.disable(logging.NOTSET)
|
51
|
+
else:
|
52
|
+
conf: Dict = {
|
53
|
+
True: {
|
54
|
+
"level": logging.DEBUG,
|
55
|
+
"msg": "[%(asctime)s] %(levelname)s: %(name)s: %(message)s",
|
56
|
+
},
|
57
|
+
False: {"level": logging.INFO, "msg": "%(message)s"},
|
58
|
+
}
|
59
|
+
|
60
|
+
logging.basicConfig(
|
61
|
+
level=conf[args.debug]["level"],
|
62
|
+
stream=sys.stdout,
|
63
|
+
format=conf[args.debug]["msg"],
|
64
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
65
|
+
)
|
66
|
+
|
67
|
+
|
68
|
+
def build_parser() -> argparse.ArgumentParser:
|
69
|
+
"""Build the CLI parser."""
|
70
|
+
parser = argparse.ArgumentParser(
|
71
|
+
prog="fotolab",
|
72
|
+
description=__doc__,
|
73
|
+
formatter_class=lambda prog: argparse.RawTextHelpFormatter(
|
74
|
+
prog, max_help_position=20
|
75
|
+
),
|
76
|
+
)
|
77
|
+
|
78
|
+
parser.add_argument(
|
79
|
+
"-o",
|
80
|
+
"--overwrite",
|
81
|
+
default=False,
|
82
|
+
action="store_true",
|
83
|
+
dest="overwrite",
|
84
|
+
help="overwrite existing image",
|
85
|
+
)
|
86
|
+
|
87
|
+
parser.add_argument(
|
88
|
+
"-op",
|
89
|
+
"--open",
|
90
|
+
default=False,
|
91
|
+
action="store_true",
|
92
|
+
dest="open",
|
93
|
+
help="open the image using default program (default: '%(default)s')",
|
94
|
+
)
|
95
|
+
|
96
|
+
parser.add_argument(
|
97
|
+
"-od",
|
98
|
+
"--output-dir",
|
99
|
+
dest="output_dir",
|
100
|
+
default="output",
|
101
|
+
help="set default output folder (default: '%(default)s')",
|
102
|
+
)
|
103
|
+
|
104
|
+
parser.add_argument(
|
105
|
+
"-q",
|
106
|
+
"--quiet",
|
107
|
+
default=False,
|
108
|
+
action="store_true",
|
109
|
+
dest="quiet",
|
110
|
+
help="suppress all logging",
|
111
|
+
)
|
112
|
+
|
113
|
+
parser.add_argument(
|
114
|
+
"-v",
|
115
|
+
"--verbose",
|
116
|
+
default=0,
|
117
|
+
action="count",
|
118
|
+
dest="verbose",
|
119
|
+
help="show verbosity of debugging log, use -vv, -vvv for more details",
|
120
|
+
)
|
121
|
+
|
122
|
+
parser.add_argument(
|
123
|
+
"-d",
|
124
|
+
"--debug",
|
125
|
+
default=False,
|
126
|
+
action="store_true",
|
127
|
+
dest="debug",
|
128
|
+
help="show debugging log and stacktrace",
|
129
|
+
)
|
130
|
+
|
131
|
+
parser.add_argument(
|
132
|
+
"-V",
|
133
|
+
"--version",
|
134
|
+
action="version",
|
135
|
+
version=f"%(prog)s {__version__}",
|
136
|
+
)
|
137
|
+
|
138
|
+
subparsers = parser.add_subparsers(help="sub-command help")
|
139
|
+
fotolab.animate.build_subparser(subparsers)
|
140
|
+
fotolab.auto.build_subparser(subparsers)
|
141
|
+
fotolab.border.build_subparser(subparsers)
|
142
|
+
fotolab.contrast.build_subparser(subparsers)
|
143
|
+
fotolab.info.build_subparser(subparsers)
|
144
|
+
fotolab.resize.build_subparser(subparsers)
|
145
|
+
fotolab.rotate.build_subparser(subparsers)
|
146
|
+
fotolab.montage.build_subparser(subparsers)
|
147
|
+
fotolab.sharpen.build_subparser(subparsers)
|
148
|
+
fotolab.watermark.build_subparser(subparsers)
|
149
|
+
fotolab.env.build_subparser(subparsers)
|
150
|
+
|
151
|
+
return parser
|
152
|
+
|
153
|
+
|
154
|
+
def main(args: Optional[Sequence[str]] = None) -> None:
|
155
|
+
"""Run the main program flow."""
|
156
|
+
args = args or sys.argv[1:]
|
157
|
+
log.debug(args)
|
158
|
+
|
159
|
+
try:
|
160
|
+
parser = build_parser()
|
161
|
+
if len(args) == 0:
|
162
|
+
parser.print_help(sys.stderr)
|
163
|
+
else:
|
164
|
+
parsed_args = parser.parse_args(args)
|
165
|
+
setup_logging(parsed_args)
|
166
|
+
|
167
|
+
if hasattr(parsed_args, "func"):
|
168
|
+
log.debug(parsed_args)
|
169
|
+
parsed_args.func(parsed_args)
|
170
|
+
else:
|
171
|
+
parser.print_help(sys.stderr)
|
172
|
+
|
173
|
+
except Exception as error:
|
174
|
+
log.error(
|
175
|
+
"error: %s",
|
176
|
+
getattr(error, "message", str(error)),
|
177
|
+
exc_info=("-d" in args or "--debug" in args),
|
178
|
+
)
|
179
|
+
|
180
|
+
raise SystemExit(1) from None
|
fotolab/contrast.py
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# Copyright (C) 2024 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
|
+
"""Contrast subcommand."""
|
17
|
+
|
18
|
+
import argparse
|
19
|
+
import logging
|
20
|
+
|
21
|
+
from PIL import Image, ImageOps
|
22
|
+
|
23
|
+
from fotolab import save_image
|
24
|
+
|
25
|
+
log = logging.getLogger(__name__)
|
26
|
+
|
27
|
+
|
28
|
+
def build_subparser(subparsers) -> None:
|
29
|
+
"""Build the subparser."""
|
30
|
+
contrast_parser = subparsers.add_parser(
|
31
|
+
"contrast", help="contrast an image"
|
32
|
+
)
|
33
|
+
|
34
|
+
contrast_parser.set_defaults(func=run)
|
35
|
+
|
36
|
+
contrast_parser.add_argument(
|
37
|
+
dest="image_filenames",
|
38
|
+
help="set the image filename",
|
39
|
+
nargs="+",
|
40
|
+
type=str,
|
41
|
+
default=None,
|
42
|
+
metavar="IMAGE_FILENAMES",
|
43
|
+
)
|
44
|
+
|
45
|
+
contrast_parser.add_argument(
|
46
|
+
"-c",
|
47
|
+
"--cutoff",
|
48
|
+
dest="cutoff",
|
49
|
+
type=float,
|
50
|
+
default=1,
|
51
|
+
help=(
|
52
|
+
"set the percentage of lightest or darkest pixels"
|
53
|
+
" to discard from histogram"
|
54
|
+
" (default: '%(default)s')"
|
55
|
+
),
|
56
|
+
metavar="CUTOFF",
|
57
|
+
)
|
58
|
+
|
59
|
+
|
60
|
+
def run(args: argparse.Namespace) -> None:
|
61
|
+
"""Run contrast subcommand.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
config (argparse.Namespace): Config from command line arguments
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
None
|
68
|
+
"""
|
69
|
+
log.debug(args)
|
70
|
+
|
71
|
+
for image_filename in args.image_filenames:
|
72
|
+
original_image = Image.open(image_filename)
|
73
|
+
contrast_image = ImageOps.autocontrast(
|
74
|
+
original_image, cutoff=args.cutoff
|
75
|
+
)
|
76
|
+
|
77
|
+
save_image(args, contrast_image, image_filename, "contrast")
|