fotolab 0.14.0__py2.py3-none-any.whl → 0.16.4__py2.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 CHANGED
@@ -21,7 +21,7 @@ import subprocess
21
21
  import sys
22
22
  from pathlib import Path
23
23
 
24
- __version__ = "0.14.0"
24
+ __version__ = "0.16.4"
25
25
 
26
26
  log = logging.getLogger(__name__)
27
27
 
@@ -48,7 +48,7 @@ def save_image(args, new_image, output_filename, subcommand):
48
48
  )
49
49
  new_filename.parent.mkdir(parents=True, exist_ok=True)
50
50
 
51
- log.info("%s image: %s", subcommand, new_filename)
51
+ log.info("%s image: %s", subcommand, new_filename.resolve())
52
52
  new_image.save(new_filename)
53
53
 
54
54
  if args.open:
@@ -64,4 +64,4 @@ def _open_image(filename):
64
64
  elif sys.platform == "windows":
65
65
  os.startfile(filename)
66
66
 
67
- log.info("open image: %s using default program.", filename.resolve())
67
+ log.info("open image: %s", filename.resolve())
fotolab/cli.py CHANGED
@@ -33,6 +33,7 @@ import fotolab.env
33
33
  import fotolab.info
34
34
  import fotolab.montage
35
35
  import fotolab.resize
36
+ import fotolab.rotate
36
37
  import fotolab.sharpen
37
38
  import fotolab.watermark
38
39
  from fotolab import __version__
@@ -42,6 +43,9 @@ log = logging.getLogger(__name__)
42
43
 
43
44
  def setup_logging(args: argparse.Namespace) -> None:
44
45
  """Set up logging by level."""
46
+ if args.verbose == 0:
47
+ logging.getLogger("PIL").setLevel(logging.ERROR)
48
+
45
49
  if args.quiet:
46
50
  logging.disable(logging.NOTSET)
47
51
  else:
@@ -105,6 +109,15 @@ def build_parser() -> argparse.ArgumentParser:
105
109
  help="suppress all logging",
106
110
  )
107
111
 
112
+ parser.add_argument(
113
+ "-v",
114
+ "--verbose",
115
+ default=0,
116
+ action="count",
117
+ dest="verbose",
118
+ help="show verbosity of debugging log, use -vv, -vvv for more details",
119
+ )
120
+
108
121
  parser.add_argument(
109
122
  "-d",
110
123
  "--debug",
@@ -128,6 +141,7 @@ def build_parser() -> argparse.ArgumentParser:
128
141
  fotolab.contrast.build_subparser(subparsers)
129
142
  fotolab.info.build_subparser(subparsers)
130
143
  fotolab.resize.build_subparser(subparsers)
144
+ fotolab.rotate.build_subparser(subparsers)
131
145
  fotolab.montage.build_subparser(subparsers)
132
146
  fotolab.sharpen.build_subparser(subparsers)
133
147
  fotolab.watermark.build_subparser(subparsers)
fotolab/resize.py CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  import argparse
19
19
  import logging
20
+ import math
20
21
 
21
22
  from PIL import Image
22
23
 
@@ -24,6 +25,9 @@ from fotolab import save_image
24
25
 
25
26
  log = logging.getLogger(__name__)
26
27
 
28
+ DEFAULT_WIDTH = 600
29
+ DEFAULT_HEIGHT = 277
30
+
27
31
 
28
32
  def build_subparser(subparsers) -> None:
29
33
  """Build the subparser."""
@@ -40,23 +44,25 @@ def build_subparser(subparsers) -> None:
40
44
  metavar="IMAGE_FILENAMES",
41
45
  )
42
46
 
43
- resize_parser.add_argument(
47
+ group = resize_parser.add_mutually_exclusive_group(required=False)
48
+
49
+ group.add_argument(
44
50
  "-wh",
45
51
  "--width",
46
52
  dest="width",
47
53
  help="set the width of the image (default: '%(default)s')",
48
54
  type=int,
49
- default="600",
55
+ default=DEFAULT_WIDTH,
50
56
  metavar="WIDTH",
51
57
  )
52
58
 
53
- resize_parser.add_argument(
59
+ group.add_argument(
54
60
  "-ht",
55
61
  "--height",
56
62
  dest="height",
57
63
  help="set the height of the image (default: '%(default)s')",
58
64
  type=int,
59
- default="277",
65
+ default=DEFAULT_HEIGHT,
60
66
  metavar="HEIGHT",
61
67
  )
62
68
 
@@ -74,9 +80,37 @@ def run(args: argparse.Namespace) -> None:
74
80
 
75
81
  for image_filename in args.image_filenames:
76
82
  original_image = Image.open(image_filename)
83
+
84
+ new_width, new_height = _calc_new_image_dimension(original_image, args)
77
85
  resized_image = original_image.copy()
78
86
  resized_image = resized_image.resize(
79
- (args.width, args.height), Image.Resampling.LANCZOS
87
+ (new_width, new_height), Image.Resampling.LANCZOS
80
88
  )
81
89
 
82
90
  save_image(args, resized_image, image_filename, "resize")
91
+
92
+
93
+ def _calc_new_image_dimension(image, args) -> tuple:
94
+ new_width = args.width
95
+ new_height = args.height
96
+
97
+ old_width, old_height = image.size
98
+ log.debug("old image dimension: %d x %d", old_width, old_height)
99
+
100
+ if args.width != DEFAULT_WIDTH:
101
+ aspect_ratio = old_height / old_width
102
+ log.debug("aspect ratio: %f", aspect_ratio)
103
+
104
+ new_height = math.ceil(args.width * aspect_ratio)
105
+ log.debug("new height: %d", new_height)
106
+
107
+ if args.height != DEFAULT_HEIGHT:
108
+ aspect_ratio = old_width / old_height
109
+ log.debug("aspect ratio: %f", aspect_ratio)
110
+
111
+ new_width = math.floor(args.height * aspect_ratio)
112
+ log.debug("new width: %d", new_width)
113
+
114
+ log.debug("new image dimension: %d x %d", new_width, new_height)
115
+
116
+ 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 CHANGED
@@ -44,7 +44,7 @@ def build_subparser(subparsers) -> None:
44
44
  "-r",
45
45
  "--radius",
46
46
  dest="radius",
47
- type=str,
47
+ type=int,
48
48
  default=1,
49
49
  help="set the radius or size of edges (default: '%(default)s')",
50
50
  metavar="RADIUS",
@@ -54,7 +54,7 @@ def build_subparser(subparsers) -> None:
54
54
  "-p",
55
55
  "--percent",
56
56
  dest="percent",
57
- type=str,
57
+ type=int,
58
58
  default=100,
59
59
  help=(
60
60
  "set the amount of overall strength of sharpening effect "
@@ -67,7 +67,7 @@ def build_subparser(subparsers) -> None:
67
67
  "-t",
68
68
  "--threshold",
69
69
  dest="threshold",
70
- type=str,
70
+ type=int,
71
71
  default=3,
72
72
  help=(
73
73
  "set the minimum brightness changed to be sharpened "
fotolab/watermark.py CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  import argparse
19
19
  import logging
20
+ import math
20
21
 
21
22
  from PIL import Image, ImageColor, ImageDraw, ImageFont
22
23
 
@@ -24,6 +25,9 @@ from fotolab import save_image
24
25
 
25
26
  log = logging.getLogger(__name__)
26
27
 
28
+ FONT_SIZE_ASPECT_RATIO = 12 / 600
29
+ FONT_PADDING_ASPECT_RATIO = 15 / 600
30
+ FONT_OUTLINE_WIDTH_ASPECT_RATIO = 2 / 600
27
31
  POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right"]
28
32
 
29
33
 
@@ -140,19 +144,21 @@ def run(args: argparse.Namespace) -> None:
140
144
 
141
145
  draw = ImageDraw.Draw(watermarked_image)
142
146
 
143
- font = ImageFont.truetype("arial.ttf", args.font_size)
147
+ font = ImageFont.truetype(
148
+ "arial.ttf", calc_font_size(original_image, args)
149
+ )
144
150
 
145
151
  (left, top, right, bottom) = draw.textbbox(
146
152
  xy=(0, 0), text=args.text, font=font
147
153
  )
148
154
  text_width = right - left
149
155
  text_height = bottom - top
150
- (position_x, position_y) = calculate_position(
156
+ (position_x, position_y) = calc_position(
151
157
  watermarked_image,
152
158
  text_width,
153
159
  text_height,
154
160
  args.position,
155
- args.padding,
161
+ calc_padding(original_image, args),
156
162
  )
157
163
 
158
164
  draw.text(
@@ -160,16 +166,49 @@ def run(args: argparse.Namespace) -> None:
160
166
  args.text,
161
167
  font=font,
162
168
  fill=(*ImageColor.getrgb(args.font_color), 128),
163
- stroke_width=args.outline_width,
169
+ stroke_width=calc_font_outline_width(original_image, args),
164
170
  stroke_fill=(*ImageColor.getrgb(args.outline_color), 128),
165
171
  )
166
172
 
167
173
  save_image(args, watermarked_image, image_filename, "watermark")
168
174
 
169
175
 
170
- def calculate_position(
171
- image, text_width, text_height, position, padding
172
- ) -> tuple:
176
+ def calc_font_size(image, args) -> int:
177
+ """Calculate the font size based on the width of the image."""
178
+ width, _height = image.size
179
+ new_font_size = args.font_size
180
+ if width > 600:
181
+ new_font_size = math.floor(FONT_SIZE_ASPECT_RATIO * width)
182
+
183
+ log.debug("new font size: %d", new_font_size)
184
+ return new_font_size
185
+
186
+
187
+ def calc_font_outline_width(image, args) -> int:
188
+ """Calculate the font padding based on the width of the image."""
189
+ width, _height = image.size
190
+ new_font_outline_width = args.outline_width
191
+ if width > 600:
192
+ new_font_outline_width = math.floor(
193
+ FONT_OUTLINE_WIDTH_ASPECT_RATIO * width
194
+ )
195
+
196
+ log.debug("new font outline width: %d", new_font_outline_width)
197
+ return new_font_outline_width
198
+
199
+
200
+ def calc_padding(image, args) -> int:
201
+ """Calculate the font padding based on the width of the image."""
202
+ width, _height = image.size
203
+ new_padding = args.padding
204
+ if width > 600:
205
+ new_padding = math.floor(FONT_PADDING_ASPECT_RATIO * width)
206
+
207
+ log.debug("new padding: %d", new_padding)
208
+ return new_padding
209
+
210
+
211
+ def calc_position(image, text_width, text_height, position, padding) -> tuple:
173
212
  """Calculate the boundary coordinates of the watermark text."""
174
213
  if position == "top-left":
175
214
  position_x = 0 + padding
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fotolab
3
- Version: 0.14.0
3
+ Version: 0.16.4
4
4
  Summary: A console program that manipulate images.
5
5
  Keywords: photography,photo
6
6
  Author-email: Kian-Meng Ang <kianmeng@cpan.org>
@@ -57,9 +57,8 @@ fotolab -h
57
57
  ```
58
58
 
59
59
  ```console
60
-
61
- usage: fotolab [-h] [-o] [-op] [-od OUTPUT_DIR] [-q] [-d] [-V]
62
- {animate,auto,border,contrast,info,resize,montage,sharpen,watermark,env}
60
+ usage: fotolab [-h] [-o] [-op] [-od OUTPUT_DIR] [-q] [-v] [-d] [-V]
61
+ {animate,auto,border,contrast,info,resize,rotate,montage,sharpen,watermark,env}
63
62
  ...
64
63
 
65
64
  A console program to manipulate photos.
@@ -69,7 +68,7 @@ A console program to manipulate photos.
69
68
  issues: https://github.com/kianmeng/fotolab/issues
70
69
 
71
70
  positional arguments:
72
- {animate,auto,border,contrast,info,resize,montage,sharpen,watermark,env}
71
+ {animate,auto,border,contrast,info,resize,rotate,montage,sharpen,watermark,env}
73
72
  sub-command help
74
73
  animate animate an image
75
74
  auto auto adjust (resize, contrast, and watermark) a photo
@@ -77,6 +76,7 @@ positional arguments:
77
76
  contrast contrast an image
78
77
  info info an image
79
78
  resize resize an image
79
+ rotate rotate an image
80
80
  montage montage a list of image
81
81
  sharpen sharpen an image
82
82
  watermark watermark an image
@@ -89,6 +89,7 @@ optional arguments:
89
89
  -od OUTPUT_DIR, --output-dir OUTPUT_DIR
90
90
  set default output folder (default: 'output')
91
91
  -q, --quiet suppress all logging
92
+ -v, --verbose show verbosity of debugging log, use -vv, -vvv for more details
92
93
  -d, --debug show debugging log and stacktrace
93
94
  -V, --version show program's version number and exit
94
95
  ```
@@ -100,7 +101,6 @@ fotolab animate -h
100
101
  ```
101
102
 
102
103
  ```console
103
-
104
104
  usage: fotolab animate [-h] [-f FORMAT] [-d DURATION] [-l LOOP]
105
105
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
106
106
 
@@ -197,6 +197,22 @@ optional arguments:
197
197
  -h, --help show this help message and exit
198
198
  ```
199
199
 
200
+ ### fotolab rotate
201
+
202
+ ```console
203
+ fotolab rotate -h
204
+ ```
205
+
206
+ ```console
207
+ usage: fotolab rotate [-h] IMAGE_FILENAMES [IMAGE_FILENAMES ...]
208
+
209
+ positional arguments:
210
+ IMAGE_FILENAMES set the image filenames
211
+
212
+ optional arguments:
213
+ -h, --help show this help message and exit
214
+ ```
215
+
200
216
  ### fotolab montage
201
217
 
202
218
  ```console
@@ -204,7 +220,6 @@ fotolab montage -h
204
220
  ```
205
221
 
206
222
  ```console
207
-
208
223
  usage: fotolab montage [-h] IMAGE_FILENAMES [IMAGE_FILENAMES ...]
209
224
 
210
225
  positional arguments:
@@ -221,7 +236,7 @@ fotolab resize -h
221
236
  ```
222
237
 
223
238
  ```console
224
- usage: fotolab resize [-h] [-wh WIDTH] [-ht HEIGHT]
239
+ usage: fotolab resize [-h] [-wh WIDTH | -ht HEIGHT]
225
240
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
226
241
 
227
242
  positional arguments:
@@ -0,0 +1,19 @@
1
+ fotolab/__init__.py,sha256=sInRXcXp8kCs9qdIt-LjBlW_vLfcwWH50vEgCt9M6l0,2061
2
+ fotolab/__main__.py,sha256=aboOURPs_snOXTEWYR0q8oq1UTY9e-NxCd1j33V0wHI,833
3
+ fotolab/animate.py,sha256=ejimhTozo9DN7BbqqcV4x8zLnanZRKq1pxBBFeOdr6Q,2967
4
+ fotolab/auto.py,sha256=1Toxe8pA_tq15g1-imMFuHf1L94Ac7EthPTu7E8SAzE,2217
5
+ fotolab/border.py,sha256=5ch2d7LVPhB2OFuuXSW5ci6Cn967CPDQu0qSfaO7uMg,3591
6
+ fotolab/cli.py,sha256=YudfGpAdd7FlNk9GOoJvPuBOURRBqc-lzQz3cxlCH20,5008
7
+ fotolab/contrast.py,sha256=l7Bs5p8W8ypN9Cg3fFHnU-A20UwMKtjTiPk6D0PRwpM,2095
8
+ fotolab/env.py,sha256=NTTvfISWBBfIw5opWrUfg0BtkaAtdUtcISBAJC2gVUk,1449
9
+ fotolab/info.py,sha256=DawXTQJiQDBwy0Ml5Ysk8MvKga3ikp_aIw73AR3LdZo,1687
10
+ fotolab/montage.py,sha256=lUVY-zDSH7mwH-s34_XefdNp7CoDJHkwpbTUGiyJGgs,2037
11
+ fotolab/resize.py,sha256=cvPfh4wUfydM23Do7VnP6Bx2EqMHKfYFYrpiNhyWzCU,3259
12
+ fotolab/rotate.py,sha256=l_vQgf0IcI8AR1TSVsk4PrMZtJ3j_wpU77rKiGJ-KTA,1715
13
+ fotolab/sharpen.py,sha256=wUPtJdtB6mCRmcHrA0CoEVO0O0ROBJWhejTvUeL67QU,2655
14
+ fotolab/watermark.py,sha256=3yHqtrh6WtFcGArgrpKAL6849Y2R1Fk-g3FeKdejrz0,6513
15
+ fotolab-0.16.4.dist-info/entry_points.txt,sha256=mvw7AY_yZkIyjAxPtHNed9X99NZeLnMxEeAfEJUbrCM,44
16
+ fotolab-0.16.4.dist-info/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
17
+ fotolab-0.16.4.dist-info/WHEEL,sha256=Sgu64hAMa6g5FdzHxXv9Xdse9yxpGGMeagVtPMWpJQY,99
18
+ fotolab-0.16.4.dist-info/METADATA,sha256=r8xQtv-cY3vLTn_QRnHCMdk906j-8WKL3NQXfQ-gZz4,10153
19
+ fotolab-0.16.4.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- fotolab/__init__.py,sha256=cpDAMSgpSA1exqkB5BGL2OrPVQfTzxV8nOTS5iP0rUE,2074
2
- fotolab/__main__.py,sha256=aboOURPs_snOXTEWYR0q8oq1UTY9e-NxCd1j33V0wHI,833
3
- fotolab/animate.py,sha256=ejimhTozo9DN7BbqqcV4x8zLnanZRKq1pxBBFeOdr6Q,2967
4
- fotolab/auto.py,sha256=1Toxe8pA_tq15g1-imMFuHf1L94Ac7EthPTu7E8SAzE,2217
5
- fotolab/border.py,sha256=5ch2d7LVPhB2OFuuXSW5ci6Cn967CPDQu0qSfaO7uMg,3591
6
- fotolab/cli.py,sha256=ZbXkftGAIrlcOgWju6KIeCBA8-EDVop2mkRN5f1Sa34,4641
7
- fotolab/contrast.py,sha256=l7Bs5p8W8ypN9Cg3fFHnU-A20UwMKtjTiPk6D0PRwpM,2095
8
- fotolab/env.py,sha256=NTTvfISWBBfIw5opWrUfg0BtkaAtdUtcISBAJC2gVUk,1449
9
- fotolab/info.py,sha256=DawXTQJiQDBwy0Ml5Ysk8MvKga3ikp_aIw73AR3LdZo,1687
10
- fotolab/montage.py,sha256=lUVY-zDSH7mwH-s34_XefdNp7CoDJHkwpbTUGiyJGgs,2037
11
- fotolab/resize.py,sha256=y3JT2IZUOeWf4gjORtaJ_ZseklO2jG6XvR-d6EdhS0k,2242
12
- fotolab/sharpen.py,sha256=DsbIe4VU0Ty5oVzKnU70p9dMfhKr1i_UAQDimzAGX-Y,2655
13
- fotolab/watermark.py,sha256=6LeK5g6W7Gq5kpLinwqBCwFGCtp0Uz_hrqBE3BD_CEU,5210
14
- fotolab-0.14.0.dist-info/entry_points.txt,sha256=mvw7AY_yZkIyjAxPtHNed9X99NZeLnMxEeAfEJUbrCM,44
15
- fotolab-0.14.0.dist-info/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
16
- fotolab-0.14.0.dist-info/WHEEL,sha256=Sgu64hAMa6g5FdzHxXv9Xdse9yxpGGMeagVtPMWpJQY,99
17
- fotolab-0.14.0.dist-info/METADATA,sha256=jHoSNuIcWosz5li1NRXRTHRxpIFX3sjPv11jFAi7kMQ,9744
18
- fotolab-0.14.0.dist-info/RECORD,,