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/env.py ADDED
@@ -0,0 +1,52 @@
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
+ """Env subcommand."""
17
+
18
+ import argparse
19
+ import logging
20
+ import platform
21
+ import sys
22
+
23
+ from fotolab import __version__
24
+
25
+ log = logging.getLogger(__name__)
26
+
27
+
28
+ def build_subparser(subparsers) -> None:
29
+ """Build the subparser."""
30
+ env_parser = subparsers.add_parser(
31
+ "env", help="print environment information for bug reporting"
32
+ )
33
+
34
+ env_parser.set_defaults(func=run)
35
+
36
+
37
+ def run(_args: argparse.Namespace) -> None:
38
+ """Run env subcommand.
39
+
40
+ Args:
41
+ config (argparse.Namespace): Config from command line arguments
42
+
43
+ Returns:
44
+ None
45
+ """
46
+ sys_version = sys.version.replace("\n", "")
47
+ print(
48
+ f"fotolab: {__version__}",
49
+ f"python: {sys_version}",
50
+ f"platform: {platform.platform()}",
51
+ sep="\n",
52
+ )
fotolab/info.py ADDED
@@ -0,0 +1,103 @@
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
+ """Info subcommand."""
17
+
18
+ import argparse
19
+ import logging
20
+
21
+ from PIL import ExifTags, Image
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+
26
+ def build_subparser(subparsers) -> None:
27
+ """Build the subparser."""
28
+ info_parser = subparsers.add_parser("info", help="info an image")
29
+
30
+ info_parser.set_defaults(func=run)
31
+
32
+ info_parser.add_argument(
33
+ dest="image_filename",
34
+ help="set the image filename",
35
+ type=str,
36
+ default=None,
37
+ metavar="IMAGE_FILENAME",
38
+ )
39
+
40
+ info_parser.add_argument(
41
+ "-s",
42
+ "--sort",
43
+ default=False,
44
+ action="store_true",
45
+ dest="sort",
46
+ help="show image info by sorted field name",
47
+ )
48
+
49
+ info_parser.add_argument(
50
+ "--camera",
51
+ default=False,
52
+ action="store_true",
53
+ dest="camera",
54
+ help="show the camera maker details",
55
+ )
56
+
57
+
58
+ def run(args: argparse.Namespace) -> None:
59
+ """Run info subcommand.
60
+
61
+ Args:
62
+ config (argparse.Namespace): Config from command line arguments
63
+
64
+ Returns:
65
+ None
66
+ """
67
+ log.debug(args)
68
+ if args.camera:
69
+ print(camera_metadata(args.image_filename))
70
+ else:
71
+ exif_tags = extract_exif_tags(args.image_filename)
72
+ if exif_tags:
73
+ tag_name_width = max(map(len, exif_tags))
74
+ for tag_name, tag_value in exif_tags.items():
75
+ print(f"{tag_name:<{tag_name_width}}: {tag_value}")
76
+ else:
77
+ print("No metadata found!")
78
+
79
+
80
+ def extract_exif_tags(image_filename: str, sort: bool = False) -> dict:
81
+ """Extract Exif metadata from image."""
82
+ image = Image.open(image_filename)
83
+ exif = image._getexif()
84
+ log.debug(exif)
85
+
86
+ info = {}
87
+ if exif:
88
+ info = {ExifTags.TAGS.get(tag_id): exif.get(tag_id) for tag_id in exif}
89
+
90
+ filtered_info = {
91
+ key: value for key, value in info.items() if key is not None
92
+ }
93
+ if sort:
94
+ filtered_info = dict(sorted(filtered_info.items()))
95
+
96
+ return filtered_info
97
+
98
+
99
+ def camera_metadata(image_filename):
100
+ """Extract camera and model metadata."""
101
+ exif_tags = extract_exif_tags(image_filename)
102
+ metadata = f'{exif_tags["Make"]} {exif_tags["Model"]}'
103
+ return metadata.strip()
fotolab/montage.py ADDED
@@ -0,0 +1,71 @@
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
+ """Montage subcommand."""
17
+
18
+ import argparse
19
+ import logging
20
+
21
+ from PIL import Image
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
+ montage_parser = subparsers.add_parser(
31
+ "montage", help="montage a list of image"
32
+ )
33
+
34
+ montage_parser.set_defaults(func=run)
35
+
36
+ montage_parser.add_argument(
37
+ dest="image_filenames",
38
+ help="set the image filenames",
39
+ nargs="+",
40
+ type=argparse.FileType("r"),
41
+ default=None,
42
+ metavar="IMAGE_FILENAMES",
43
+ )
44
+
45
+
46
+ def run(args: argparse.Namespace) -> None:
47
+ """Run montage subcommand.
48
+
49
+ Args:
50
+ config (argparse.Namespace): Config from command line arguments
51
+
52
+ Returns:
53
+ None
54
+ """
55
+ log.debug(args)
56
+ images = []
57
+ for image_filename in args.image_filenames:
58
+ images.append(Image.open(image_filename.name))
59
+
60
+ total_width = sum(img.width for img in images)
61
+ total_height = max(img.height for img in images)
62
+
63
+ montaged_image = Image.new("RGB", (total_width, total_height))
64
+
65
+ x_offset = 0
66
+ for img in images:
67
+ montaged_image.paste(img, (x_offset, 0))
68
+ x_offset += img.width
69
+
70
+ output_image_filename = args.image_filenames[0].name
71
+ save_image(args, montaged_image, output_image_filename, "montage")
fotolab/resize.py ADDED
@@ -0,0 +1,176 @@
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
+ """Resize subcommand."""
17
+
18
+ import argparse
19
+ import logging
20
+ import math
21
+ import sys
22
+
23
+ from PIL import Image, ImageColor
24
+
25
+ from fotolab import save_image
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+ DEFAULT_WIDTH = 600
30
+ DEFAULT_HEIGHT = 277
31
+
32
+
33
+ def build_subparser(subparsers) -> None:
34
+ """Build the subparser."""
35
+ resize_parser = subparsers.add_parser("resize", help="resize an image")
36
+
37
+ resize_parser.set_defaults(func=run)
38
+
39
+ resize_parser.add_argument(
40
+ dest="image_filenames",
41
+ help="set the image filename",
42
+ nargs="+",
43
+ type=str,
44
+ default=None,
45
+ metavar="IMAGE_FILENAMES",
46
+ )
47
+
48
+ resize_parser.add_argument(
49
+ "-c",
50
+ "--canvas",
51
+ default=False,
52
+ action="store_true",
53
+ dest="canvas",
54
+ help="paste image onto a larger canvas",
55
+ )
56
+
57
+ resize_parser.add_argument(
58
+ "-l",
59
+ "--canvas-color",
60
+ default="black",
61
+ dest="canvas_color",
62
+ help=(
63
+ "the color of the extended larger canvas"
64
+ "(default: '%(default)s')"
65
+ ),
66
+ )
67
+
68
+ if "-c" in sys.argv or "--canvas" in sys.argv:
69
+ resize_parser.add_argument(
70
+ "-W",
71
+ "--width",
72
+ dest="width",
73
+ help="set the width of the image (default: '%(default)s')",
74
+ type=int,
75
+ required=True,
76
+ metavar="WIDTH",
77
+ )
78
+
79
+ resize_parser.add_argument(
80
+ "-H",
81
+ "--height",
82
+ dest="height",
83
+ help="set the height of the image (default: '%(default)s')",
84
+ type=int,
85
+ required=True,
86
+ metavar="HEIGHT",
87
+ )
88
+ else:
89
+ group = resize_parser.add_mutually_exclusive_group(required=False)
90
+
91
+ group.add_argument(
92
+ "-W",
93
+ "--width",
94
+ dest="width",
95
+ help="set the width of the image (default: '%(default)s')",
96
+ type=int,
97
+ default=DEFAULT_WIDTH,
98
+ metavar="WIDTH",
99
+ )
100
+
101
+ group.add_argument(
102
+ "-H",
103
+ "--height",
104
+ dest="height",
105
+ help="set the height of the image (default: '%(default)s')",
106
+ type=int,
107
+ default=DEFAULT_HEIGHT,
108
+ metavar="HEIGHT",
109
+ )
110
+
111
+
112
+ def run(args: argparse.Namespace) -> None:
113
+ """Run resize subcommand.
114
+
115
+ Args:
116
+ args (argparse.Namespace): Config from command line arguments
117
+
118
+ Returns:
119
+ None
120
+ """
121
+ log.debug(args)
122
+
123
+ for image_filename in args.image_filenames:
124
+ original_image = Image.open(image_filename)
125
+ if args.canvas:
126
+ resized_image = _resize_image_onto_canvas(original_image, args)
127
+ else:
128
+ resized_image = _resize_image(original_image, args)
129
+ save_image(args, resized_image, image_filename, "resize")
130
+
131
+
132
+ def _resize_image_onto_canvas(original_image, args):
133
+ resized_image = Image.new(
134
+ "RGB",
135
+ (args.width, args.height),
136
+ (*ImageColor.getrgb(args.canvas_color), 128),
137
+ )
138
+ x_offset = (args.width - original_image.width) // 2
139
+ y_offset = (args.height - original_image.height) // 2
140
+ resized_image.paste(original_image, (x_offset, y_offset))
141
+ return resized_image
142
+
143
+
144
+ def _resize_image(original_image, args):
145
+ new_width, new_height = _calc_new_image_dimension(original_image, args)
146
+ resized_image = original_image.copy()
147
+ resized_image = resized_image.resize(
148
+ (new_width, new_height), Image.Resampling.LANCZOS
149
+ )
150
+ return resized_image
151
+
152
+
153
+ def _calc_new_image_dimension(image, args) -> tuple:
154
+ new_width = args.width
155
+ new_height = args.height
156
+
157
+ old_width, old_height = image.size
158
+ log.debug("old image dimension: %d x %d", old_width, old_height)
159
+
160
+ if args.width != DEFAULT_WIDTH:
161
+ aspect_ratio = old_height / old_width
162
+ log.debug("aspect ratio: %f", aspect_ratio)
163
+
164
+ new_height = math.ceil(args.width * aspect_ratio)
165
+ log.debug("new height: %d", new_height)
166
+
167
+ if args.height != DEFAULT_HEIGHT:
168
+ aspect_ratio = old_width / old_height
169
+ log.debug("aspect ratio: %f", aspect_ratio)
170
+
171
+ new_width = math.floor(args.height * aspect_ratio)
172
+ log.debug("new width: %d", new_width)
173
+
174
+ log.debug("new image dimension: %d x %d", new_width, new_height)
175
+
176
+ return (new_width, new_height)
fotolab/rotate.py ADDED
@@ -0,0 +1,61 @@
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
+ """Rotate subcommand."""
17
+
18
+ import argparse
19
+ import logging
20
+
21
+ from PIL import Image
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
+ rotate_parser = subparsers.add_parser("rotate", help="rotate an image")
31
+
32
+ rotate_parser.set_defaults(func=run)
33
+
34
+ rotate_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
+
44
+ def run(args: argparse.Namespace) -> None:
45
+ """Run rotate subcommand.
46
+
47
+ Args:
48
+ config (argparse.Namespace): Config from command line arguments
49
+
50
+ Returns:
51
+ None
52
+ """
53
+ log.debug(args)
54
+
55
+ for image_filename in args.image_filenames:
56
+ original_image = Image.open(image_filename)
57
+ rotated_image = original_image.rotate(
58
+ 180,
59
+ expand=True,
60
+ )
61
+ save_image(args, rotated_image, image_filename, "rotate")
fotolab/sharpen.py ADDED
@@ -0,0 +1,98 @@
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
+ """Sharpen subcommand."""
17
+
18
+ import argparse
19
+ import logging
20
+
21
+ from PIL import Image, ImageFilter
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
+ sharpen_parser = subparsers.add_parser("sharpen", help="sharpen an image")
31
+
32
+ sharpen_parser.set_defaults(func=run)
33
+
34
+ sharpen_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
+ sharpen_parser.add_argument(
44
+ "-r",
45
+ "--radius",
46
+ dest="radius",
47
+ type=int,
48
+ default=1,
49
+ help="set the radius or size of edges (default: '%(default)s')",
50
+ metavar="RADIUS",
51
+ )
52
+
53
+ sharpen_parser.add_argument(
54
+ "-p",
55
+ "--percent",
56
+ dest="percent",
57
+ type=int,
58
+ default=100,
59
+ help=(
60
+ "set the amount of overall strength of sharpening effect "
61
+ "(default: '%(default)s')"
62
+ ),
63
+ metavar="PERCENT",
64
+ )
65
+
66
+ sharpen_parser.add_argument(
67
+ "-t",
68
+ "--threshold",
69
+ dest="threshold",
70
+ type=int,
71
+ default=3,
72
+ help=(
73
+ "set the minimum brightness changed to be sharpened "
74
+ "(default: '%(default)s')"
75
+ ),
76
+ metavar="THRESHOLD",
77
+ )
78
+
79
+
80
+ def run(args: argparse.Namespace) -> None:
81
+ """Run sharpen subcommand.
82
+
83
+ Args:
84
+ config (argparse.Namespace): Config from command line arguments
85
+
86
+ Returns:
87
+ None
88
+ """
89
+ log.debug(args)
90
+
91
+ for image_filename in args.image_filenames:
92
+ original_image = Image.open(image_filename)
93
+ sharpen_image = original_image.filter(
94
+ ImageFilter.UnsharpMask(
95
+ args.radius, percent=args.percent, threshold=args.threshold
96
+ )
97
+ )
98
+ save_image(args, sharpen_image, image_filename, "sharpen")