fotolab 0.34.0__py3-none-any.whl → 0.34.2__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
@@ -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,8 @@ 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
26
25
 
27
26
  log = logging.getLogger(__name__)
28
27
 
@@ -49,12 +48,12 @@ def build_subparser(subparsers) -> None:
49
48
  animate_parser.set_defaults(func=run)
50
49
 
51
50
  animate_parser.add_argument(
52
- dest="image_filenames",
51
+ dest="image_paths",
53
52
  help="set the image filenames",
54
53
  nargs="+",
55
54
  type=str,
56
55
  default=None,
57
- metavar="IMAGE_FILENAMES",
56
+ metavar="IMAGE_PATHS",
58
57
  )
59
58
 
60
59
  animate_parser.add_argument(
@@ -159,15 +158,15 @@ def run(args: argparse.Namespace) -> None:
159
158
  """
160
159
  log.debug(args)
161
160
 
162
- image_filepaths = [Path(f) for f in args.image_filenames]
161
+ image_filepaths = [Path(f) for f in args.image_paths]
163
162
  first_image_filepath = image_filepaths[0]
164
163
  other_frames = []
165
164
 
166
165
  with ExitStack() as stack:
167
- main_frame = stack.enter_context(Image.open(first_image_filepath))
166
+ main_frame = stack.enter_context(load_image(first_image_filepath))
168
167
 
169
168
  for image_filepath in image_filepaths[1:]:
170
- img = stack.enter_context(Image.open(image_filepath))
169
+ img = stack.enter_context(load_image(image_filepath))
171
170
  other_frames.append(img)
172
171
 
173
172
  if args.output_filename:
@@ -36,12 +36,12 @@ def build_subparser(subparsers) -> None:
36
36
  auto_parser.set_defaults(func=run)
37
37
 
38
38
  auto_parser.add_argument(
39
- dest="image_filenames",
39
+ dest="image_paths",
40
40
  help="set the image filename",
41
41
  nargs="+",
42
42
  type=str,
43
43
  default=None,
44
- metavar="IMAGE_FILENAMES",
44
+ metavar="IMAGE_PATHS",
45
45
  )
46
46
 
47
47
  auto_parser.add_argument(
@@ -114,7 +114,7 @@ def run(args: argparse.Namespace) -> None:
114
114
  fotolab.subcommands.sharpen.run(combined_args)
115
115
  fotolab.subcommands.watermark.run(combined_args)
116
116
 
117
- if len(args.image_filenames) > 1:
117
+ if len(args.image_paths) > 1:
118
118
  output_filename = (
119
119
  args.title.lower().replace(",", "").replace(" ", "_") + ".gif"
120
120
  )
@@ -20,9 +20,9 @@ import logging
20
20
  from pathlib import Path
21
21
  from typing import Tuple
22
22
 
23
- from PIL import Image, ImageColor, ImageOps
23
+ from PIL import ImageColor, ImageOps
24
24
 
25
- from fotolab import save_image
25
+ from fotolab import load_image, save_image
26
26
 
27
27
  log = logging.getLogger(__name__)
28
28
 
@@ -34,12 +34,12 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
34
34
  border_parser.set_defaults(func=run)
35
35
 
36
36
  border_parser.add_argument(
37
- dest="image_filenames",
37
+ dest="image_paths",
38
38
  help="set the image filenames",
39
39
  nargs="+",
40
40
  type=str,
41
41
  default=None,
42
- metavar="IMAGE_FILENAMES",
42
+ metavar="IMAGE_PATHS",
43
43
  )
44
44
 
45
45
  border_parser.add_argument(
@@ -135,16 +135,16 @@ def run(args: argparse.Namespace) -> None:
135
135
  """
136
136
  log.debug(args)
137
137
 
138
- for image_filepath in [Path(f) for f in args.image_filenames]:
139
- original_image = Image.open(image_filepath)
140
- border = get_border(args)
141
- bordered_image = ImageOps.expand(
142
- original_image,
143
- border=border,
144
- fill=ImageColor.getrgb(args.color),
145
- )
138
+ for image_filepath in [Path(f) for f in args.image_paths]:
139
+ with load_image(image_filepath) as original_image:
140
+ border = get_border(args)
141
+ bordered_image = ImageOps.expand(
142
+ original_image,
143
+ border=border,
144
+ fill=ImageColor.getrgb(args.color),
145
+ )
146
146
 
147
- save_image(args, bordered_image, image_filepath, "border")
147
+ save_image(args, bordered_image, image_filepath, "border")
148
148
 
149
149
 
150
150
  def get_border(
@@ -19,9 +19,9 @@ import argparse
19
19
  import logging
20
20
  from pathlib import Path
21
21
 
22
- from PIL import Image, ImageOps
22
+ from PIL import ImageOps
23
23
 
24
- from fotolab import save_image
24
+ from fotolab import load_image, save_image
25
25
 
26
26
  log = logging.getLogger(__name__)
27
27
 
@@ -51,12 +51,12 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
51
51
  contrast_parser.set_defaults(func=run)
52
52
 
53
53
  contrast_parser.add_argument(
54
- dest="image_filenames",
54
+ dest="image_paths",
55
55
  help="set the image filename",
56
56
  nargs="+",
57
57
  type=str,
58
58
  default=None,
59
- metavar="IMAGE_FILENAMES",
59
+ metavar="IMAGE_PATHS",
60
60
  )
61
61
 
62
62
  contrast_parser.add_argument(
@@ -102,26 +102,26 @@ def run(args: argparse.Namespace) -> None:
102
102
  """
103
103
  log.debug(args)
104
104
 
105
- for image_filename in args.image_filenames:
106
- original_image = Image.open(image_filename)
107
-
108
- if original_image.mode == "RGBA":
109
- # Split the image into RGB and Alpha channels
110
- rgb_image = original_image.convert("RGB")
111
- alpha_channel = original_image.getchannel("A")
112
-
113
- # Apply autocontrast to the RGB part
114
- contrasted_rgb = ImageOps.autocontrast(
115
- rgb_image, cutoff=args.cutoff
116
- )
117
-
118
- # Merge the contrasted RGB part with the original Alpha channel
119
- contrasted_rgb.putalpha(alpha_channel)
120
- contrast_image = contrasted_rgb
121
- else:
122
- # For other modes (like RGB, L, etc.), apply autocontrast directly
123
- contrast_image = ImageOps.autocontrast(
124
- original_image, cutoff=args.cutoff
125
- )
126
-
127
- save_image(args, contrast_image, Path(image_filename), "contrast")
105
+ for image_path_str in args.image_paths:
106
+ with load_image(Path(image_path_str)) as original_image:
107
+ if original_image.mode == "RGBA":
108
+ # Split the image into RGB and Alpha channels
109
+ rgb_image = original_image.convert("RGB")
110
+ alpha_channel = original_image.getchannel("A")
111
+
112
+ # Apply autocontrast to the RGB part
113
+ contrasted_rgb = ImageOps.autocontrast(
114
+ rgb_image, cutoff=args.cutoff
115
+ )
116
+
117
+ # Merge the contrasted RGB part with the original Alpha channel
118
+ contrasted_rgb.putalpha(alpha_channel)
119
+ contrast_image = contrasted_rgb
120
+ else:
121
+ # For other modes (like RGB, L, etc.), apply autocontrast
122
+ # directly
123
+ contrast_image = ImageOps.autocontrast(
124
+ original_image, cutoff=args.cutoff
125
+ )
126
+
127
+ save_image(args, contrast_image, Path(image_path_str), "contrast")
@@ -23,7 +23,7 @@ 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
27
 
28
28
  log = logging.getLogger(__name__)
29
29
 
@@ -45,12 +45,12 @@ def build_subparser(subparsers) -> None:
45
45
  halftone_parser.set_defaults(func=run)
46
46
 
47
47
  halftone_parser.add_argument(
48
- dest="image_filenames",
48
+ dest="image_paths",
49
49
  help="set the image filename",
50
50
  nargs="+",
51
51
  type=str,
52
52
  default=None,
53
- metavar="IMAGE_FILENAMES",
53
+ metavar="IMAGE_PATHS",
54
54
  )
55
55
 
56
56
  halftone_parser.add_argument(
@@ -111,28 +111,29 @@ def run(args: argparse.Namespace) -> None:
111
111
  """
112
112
  log.debug(args)
113
113
 
114
- for image_filename_str in args.image_filenames:
115
- image_filename = Path(image_filename_str)
116
- original_image = Image.open(image_filename)
117
- halftone_image = create_halftone_image(
118
- original_image, args.cells, args.grayscale
119
- )
120
-
121
- if args.before_after:
122
- save_gif_image(
123
- args,
124
- image_filename,
125
- original_image,
126
- halftone_image,
127
- "halftone",
114
+ for image_path_str in args.image_paths:
115
+ image_filename = Path(image_path_str)
116
+ with load_image(image_filename) as original_image:
117
+ halftone_image = create_halftone_image(
118
+ original_image, args.cells, args.grayscale
128
119
  )
129
- else:
130
- save_image(args, halftone_image, image_filename, "halftone")
120
+
121
+ if args.before_after:
122
+ save_gif_image(
123
+ args,
124
+ image_filename,
125
+ original_image,
126
+ halftone_image,
127
+ "halftone",
128
+ )
129
+ else:
130
+ save_image(args, halftone_image, image_filename, "halftone")
131
131
 
132
132
 
133
133
  def _draw_halftone_dot(
134
134
  draw: ImageDraw.ImageDraw,
135
135
  source_image: Image.Image,
136
+ brightness_map: Image.Image,
136
137
  cell: HalftoneCell,
137
138
  grayscale: bool,
138
139
  fill_color_dot: int,
@@ -150,21 +151,19 @@ def _draw_halftone_dot(
150
151
  x = max(0, x)
151
152
  y = max(0, y)
152
153
 
153
- # get pixel value (brightness or color) from the source image using clamped
154
- # coordinates
154
+ # Get brightness from the pre-calculated map (L mode)
155
+ brightness = brightness_map.getpixel((x, y))
156
+
157
+ # Get pixel value (color) from the source image
155
158
  pixel_value = source_image.getpixel((x, y))
156
159
 
157
160
  if grayscale:
158
- brightness = pixel_value
159
- dot_fill = fill_color_dot # Use white for grayscale dots
161
+ # In grayscale mode, source_image is L mode, pixel_value is brightness
162
+ # dot_fill is the fixed color (255 for white dot on black background)
163
+ dot_fill = fill_color_dot
160
164
  else:
161
- # calculate brightness (luminance) from the RGB color
162
- brightness = int(
163
- 0.299 * pixel_value[0]
164
- + 0.587 * pixel_value[1]
165
- + 0.114 * pixel_value[2]
166
- )
167
- dot_fill = pixel_value # Use original color for color dots
165
+ # In color mode, dot_fill is the original color
166
+ dot_fill = pixel_value
168
167
 
169
168
  # calculate dot radius relative to cell size based on brightness max radius
170
169
  # is half the cell size
@@ -203,8 +202,11 @@ def create_halftone_image(
203
202
  fill_color_black: int | tuple[int, int, int]
204
203
  fill_color_dot_for_grayscale: int
205
204
 
205
+ # Always create a grayscale version for brightness calculation (dot size)
206
+ brightness_map = original_image.convert("L")
207
+
206
208
  if grayscale:
207
- source_image = original_image.convert("L")
209
+ source_image = brightness_map
208
210
  output_mode = "L"
209
211
  fill_color_black = 0
210
212
  fill_color_dot_for_grayscale = 255
@@ -229,6 +231,7 @@ def create_halftone_image(
229
231
  _draw_halftone_dot(
230
232
  draw,
231
233
  source_image,
234
+ brightness_map,
232
235
  cell,
233
236
  grayscale,
234
237
  fill_color_dot_for_grayscale,
@@ -18,10 +18,11 @@
18
18
  import argparse
19
19
  import logging
20
20
  from pathlib import Path
21
+ from contextlib import ExitStack
21
22
 
22
23
  from PIL import Image
23
24
 
24
- from fotolab import save_image
25
+ from fotolab import load_image, save_image
25
26
 
26
27
  log = logging.getLogger(__name__)
27
28
 
@@ -35,12 +36,12 @@ def build_subparser(subparsers) -> None:
35
36
  montage_parser.set_defaults(func=run)
36
37
 
37
38
  montage_parser.add_argument(
38
- dest="image_filenames",
39
+ dest="image_paths",
39
40
  help="set the image filenames",
40
41
  nargs="+",
41
42
  type=str,
42
43
  default=None,
43
- metavar="IMAGE_FILENAMES",
44
+ metavar="IMAGE_PATHS",
44
45
  )
45
46
 
46
47
  montage_parser.add_argument(
@@ -72,22 +73,23 @@ def run(args: argparse.Namespace) -> None:
72
73
  """
73
74
  log.debug(args)
74
75
  images = []
75
- for image_filename_str in args.image_filenames:
76
- image_filename = Path(image_filename_str)
77
- images.append(Image.open(image_filename))
76
+ with ExitStack() as stack:
77
+ for image_path_str in args.image_paths:
78
+ image_filename = Path(image_path_str)
79
+ images.append(stack.enter_context(load_image(image_filename)))
78
80
 
79
- if len(images) < 2:
80
- raise ValueError("at least two images is required for montage")
81
+ if len(images) < 2:
82
+ raise ValueError("at least two images is required for montage")
81
83
 
82
- total_width = sum(img.width for img in images)
83
- total_height = max(img.height for img in images)
84
+ total_width = sum(img.width for img in images)
85
+ total_height = max(img.height for img in images)
84
86
 
85
- montaged_image = Image.new("RGB", (total_width, total_height))
87
+ montaged_image = Image.new("RGB", (total_width, total_height))
86
88
 
87
- x_offset = 0
88
- for image in images:
89
- montaged_image.paste(image, (x_offset, 0))
90
- x_offset += image.width
89
+ x_offset = 0
90
+ for image in images:
91
+ montaged_image.paste(image, (x_offset, 0))
92
+ x_offset += image.width
91
93
 
92
- output_image_filename = Path(args.image_filenames[0])
93
- save_image(args, montaged_image, output_image_filename, "montage")
94
+ output_image_filename = Path(args.image_paths[0])
95
+ save_image(args, montaged_image, output_image_filename, "montage")
@@ -18,12 +18,11 @@
18
18
  import argparse
19
19
  import logging
20
20
  import math
21
- import sys
22
21
  from pathlib import Path
23
22
 
24
23
  from PIL import Image, ImageColor
25
24
 
26
- from fotolab import save_image
25
+ from fotolab import load_image, save_image
27
26
 
28
27
  log = logging.getLogger(__name__)
29
28
 
@@ -38,12 +37,12 @@ def build_subparser(subparsers) -> None:
38
37
  resize_parser.set_defaults(func=run)
39
38
 
40
39
  resize_parser.add_argument(
41
- dest="image_filenames",
40
+ dest="image_paths",
42
41
  help="set the image filename",
43
42
  nargs="+",
44
43
  type=str,
45
44
  default=None,
46
- metavar="IMAGE_FILENAMES",
45
+ metavar="IMAGE_PATHS",
47
46
  )
48
47
 
49
48
  resize_parser.add_argument(
@@ -65,48 +64,28 @@ def build_subparser(subparsers) -> None:
65
64
  ),
66
65
  )
67
66
 
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
- )
67
+ # Define width and height arguments as optional with defaults.
68
+ # The conditional logic (required/mutually exclusive) is now handled in the
69
+ # run function.
70
+ resize_parser.add_argument(
71
+ "-W",
72
+ "--width",
73
+ dest="width",
74
+ help="set the width of the image (default: '%(default)s')",
75
+ type=int,
76
+ default=DEFAULT_WIDTH,
77
+ metavar="WIDTH",
78
+ )
79
+
80
+ resize_parser.add_argument(
81
+ "-H",
82
+ "--height",
83
+ dest="height",
84
+ help="set the height of the image (default: '%(default)s')",
85
+ type=int,
86
+ default=DEFAULT_HEIGHT,
87
+ metavar="HEIGHT",
88
+ )
110
89
 
111
90
  resize_parser.add_argument(
112
91
  "-op",
@@ -137,13 +116,31 @@ def run(args: argparse.Namespace) -> None:
137
116
  """
138
117
  log.debug(args)
139
118
 
140
- for image_filepath in [Path(f) for f in args.image_filenames]:
141
- original_image = Image.open(image_filepath)
142
- if args.canvas:
143
- resized_image = _resize_image_onto_canvas(original_image, args)
144
- else:
145
- resized_image = _resize_image(original_image, args)
146
- save_image(args, resized_image, image_filepath, "resize")
119
+ width_provided = args.width != DEFAULT_WIDTH
120
+ height_provided = args.height != DEFAULT_HEIGHT
121
+
122
+ if args.canvas:
123
+ # Canvas mode: Both width and height are required
124
+ if not (width_provided and height_provided):
125
+ raise SystemExit(
126
+ "error: argument -W/--width and -H/--height are required when "
127
+ "using --canvas"
128
+ )
129
+ else:
130
+ # Resize mode: Width and height are mutually exclusive
131
+ if width_provided and height_provided:
132
+ raise SystemExit(
133
+ "error: argument -W/--width and -H/--height are mutually "
134
+ "exclusive when not using --canvas"
135
+ )
136
+
137
+ for image_filepath in [Path(f) for f in args.image_paths]:
138
+ with load_image(image_filepath) as original_image:
139
+ if args.canvas:
140
+ resized_image = _resize_image_onto_canvas(original_image, args)
141
+ else:
142
+ resized_image = _resize_image(original_image, args)
143
+ save_image(args, resized_image, image_filepath, "resize")
147
144
 
148
145
 
149
146
  def _resize_image_onto_canvas(original_image, args):
@@ -176,18 +173,20 @@ def _calc_new_image_dimension(image, args) -> tuple:
176
173
 
177
174
  original_aspect_ratio = old_width / old_height
178
175
 
179
- if new_width != DEFAULT_WIDTH and new_height == DEFAULT_HEIGHT:
180
- # user provided width, calculate height to maintain aspect ratio
181
- new_height = math.ceil(new_width / original_aspect_ratio)
182
- log.debug("new height calculated based on width: %d", new_height)
183
- elif new_height != DEFAULT_HEIGHT and new_width == DEFAULT_WIDTH:
184
- # user provided height, calculate width to maintain aspect ratio
185
- new_width = math.ceil(new_height * original_aspect_ratio)
186
- log.debug("new width calculated based on height: %d", new_width)
187
-
188
- # if both are default, no calculation needed, use defaults
189
- # due to argparse's mutually exclusive group, it's not possible for both
190
- # new_width and new_height to be non-default when --canvas is False
176
+ match (new_width != DEFAULT_WIDTH, new_height != DEFAULT_HEIGHT):
177
+ case (True, False):
178
+ # user provided width, calculate height to maintain aspect ratio
179
+ new_height = math.ceil(new_width / original_aspect_ratio)
180
+ log.debug("new height calculated based on width: %d", new_height)
181
+ case (False, True):
182
+ # user provided height, calculate width to maintain aspect ratio
183
+ new_width = math.ceil(new_height * original_aspect_ratio)
184
+ log.debug("new width calculated based on height: %d", new_width)
185
+ case _:
186
+ # if both are default, no calculation needed, use defaults.
187
+ # The case where both are non-default is disallowed by argparse
188
+ # when --canvas is False, so we do nothing here.
189
+ pass
191
190
 
192
191
  log.debug("new image dimension: %d x %d", new_width, new_height)
193
192
  return (new_width, new_height)
@@ -21,7 +21,7 @@ from pathlib import Path
21
21
 
22
22
  from PIL import Image
23
23
 
24
- from fotolab import save_image
24
+ from fotolab import load_image, save_image
25
25
 
26
26
  log = logging.getLogger(__name__)
27
27
 
@@ -33,12 +33,12 @@ def build_subparser(subparsers) -> None:
33
33
  rotate_parser.set_defaults(func=run)
34
34
 
35
35
  rotate_parser.add_argument(
36
- dest="image_filenames",
36
+ dest="image_paths",
37
37
  help="set the image filenames",
38
38
  nargs="+",
39
39
  type=str,
40
40
  default=None,
41
- metavar="IMAGE_FILENAMES",
41
+ metavar="IMAGE_PATHS",
42
42
  )
43
43
 
44
44
  rotate_parser.add_argument(
@@ -88,14 +88,14 @@ def run(args: argparse.Namespace) -> None:
88
88
  rotation = -args.rotation if args.clockwise else args.rotation
89
89
  log.debug(f"Rotation angle: {rotation} degrees")
90
90
 
91
- for image_filename_str in args.image_filenames:
92
- image_filename = Path(image_filename_str)
91
+ for image_path_str in args.image_paths:
92
+ image_filename = Path(image_path_str)
93
93
  log.debug(f"Processing image: {image_filename}")
94
- original_image = Image.open(image_filename)
95
- log.debug(f"Original image size: {original_image.size}")
96
- rotated_image = original_image.rotate(
97
- rotation, expand=True, resample=Image.Resampling.BICUBIC
98
- )
99
- log.debug(f"Rotated image size: {rotated_image.size}")
100
- save_image(args, rotated_image, image_filename, "rotate")
101
- log.debug(f"Image saved: {image_filename}")
94
+ with load_image(image_filename) as original_image:
95
+ log.debug(f"Original image size: {original_image.size}")
96
+ rotated_image = original_image.rotate(
97
+ rotation, expand=True, resample=Image.Resampling.BICUBIC
98
+ )
99
+ log.debug(f"Rotated image size: {rotated_image.size}")
100
+ save_image(args, rotated_image, image_filename, "rotate")
101
+ log.debug(f"Image saved: {image_filename}")
@@ -19,9 +19,9 @@ import argparse
19
19
  import logging
20
20
  from pathlib import Path
21
21
 
22
- from PIL import Image, ImageFilter
22
+ from PIL import ImageFilter
23
23
 
24
- from fotolab import save_gif_image, save_image
24
+ from fotolab import load_image, save_gif_image, save_image
25
25
 
26
26
  log = logging.getLogger(__name__)
27
27
 
@@ -33,12 +33,12 @@ def build_subparser(subparsers) -> None:
33
33
  sharpen_parser.set_defaults(func=run)
34
34
 
35
35
  sharpen_parser.add_argument(
36
- dest="image_filenames",
36
+ dest="image_paths",
37
37
  help="set the image filenames",
38
38
  nargs="+",
39
39
  type=str,
40
40
  default=None,
41
- metavar="IMAGE_FILENAMES",
41
+ metavar="IMAGE_PATHS",
42
42
  )
43
43
 
44
44
  sharpen_parser.add_argument(
@@ -115,20 +115,22 @@ def run(args: argparse.Namespace) -> None:
115
115
  """
116
116
  log.debug(args)
117
117
 
118
- for image_filename in args.image_filenames:
119
- original_image = Image.open(image_filename)
120
- sharpen_image = original_image.filter(
121
- ImageFilter.UnsharpMask(
122
- args.radius, percent=args.percent, threshold=args.threshold
118
+ for image_path_str in args.image_paths:
119
+ with load_image(Path(image_path_str)) as original_image:
120
+ sharpen_image = original_image.filter(
121
+ ImageFilter.UnsharpMask(
122
+ args.radius, percent=args.percent, threshold=args.threshold
123
+ )
123
124
  )
124
- )
125
- if args.before_after:
126
- save_gif_image(
127
- args,
128
- Path(image_filename),
129
- original_image,
130
- sharpen_image,
131
- "sharpen",
132
- )
133
- else:
134
- save_image(args, sharpen_image, Path(image_filename), "sharpen")
125
+ if args.before_after:
126
+ save_gif_image(
127
+ args,
128
+ Path(image_path_str),
129
+ original_image,
130
+ sharpen_image,
131
+ "sharpen",
132
+ )
133
+ else:
134
+ save_image(
135
+ args, sharpen_image, Path(image_path_str), "sharpen"
136
+ )
@@ -23,7 +23,7 @@ from typing import Tuple
23
23
 
24
24
  from PIL import Image, ImageColor, ImageDraw, ImageFont, ImageSequence
25
25
 
26
- from fotolab import save_image
26
+ from fotolab import load_image, save_image
27
27
  from fotolab.subcommands.info import get_formatted_camera_info
28
28
 
29
29
  log: logging.Logger = logging.getLogger(__name__)
@@ -43,12 +43,12 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
43
43
  watermark_parser.set_defaults(func=run)
44
44
 
45
45
  watermark_parser.add_argument(
46
- dest="image_filenames",
46
+ dest="image_paths",
47
47
  help="set the image filenames",
48
48
  nargs="+",
49
49
  type=str,
50
50
  default=None,
51
- metavar="IMAGE_FILENAMES",
51
+ metavar="IMAGE_PATHS",
52
52
  )
53
53
 
54
54
  watermark_parser.add_argument(
@@ -190,26 +190,28 @@ def run(args: argparse.Namespace) -> None:
190
190
  """
191
191
  log.debug(args)
192
192
 
193
- for image_filename in args.image_filenames:
193
+ for image_path_str in args.image_paths:
194
194
  try:
195
- image: Image.Image = Image.open(image_filename)
195
+ with load_image(Path(image_path_str)) as image:
196
+ if image.format == "GIF":
197
+ watermark_gif_image(image, image_path_str, args)
198
+ else:
199
+ watermarked_image: Image.Image = watermark_non_gif_image(
200
+ image, args
201
+ )
202
+ save_image(
203
+ args,
204
+ watermarked_image,
205
+ Path(image_path_str),
206
+ "watermark",
207
+ )
196
208
  except FileNotFoundError:
197
- log.error("Image file not found: %s", image_filename)
209
+ log.error("Image file not found: %s", image_path_str)
198
210
  continue
199
211
  except Exception as e:
200
- log.error("Could not open image %s: %s", image_filename, e)
212
+ log.error("Could not open image %s: %s", image_path_str, e)
201
213
  continue
202
214
 
203
- if image.format == "GIF":
204
- watermark_gif_image(image, image_filename, args)
205
- else:
206
- watermarked_image: Image.Image = watermark_non_gif_image(
207
- image, args
208
- )
209
- save_image(
210
- args, watermarked_image, Path(image_filename), "watermark"
211
- )
212
-
213
215
 
214
216
  def watermark_gif_image(
215
217
  original_image: Image.Image, output_filename: str, args: argparse.Namespace
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fotolab
3
- Version: 0.34.0
3
+ Version: 0.34.2
4
4
  Summary: A console program to manipulate photos.
5
5
  Author-email: Kian-Meng Ang <kianmeng@cpan.org>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -112,10 +112,11 @@ fotolab animate -h
112
112
  usage: fotolab animate [-h] [-f FORMAT] [-d DURATION] [-l LOOP] [-op]
113
113
  [--webp-quality QUALITY] [--webp-lossless]
114
114
  [--webp-method METHOD] [-od OUTPUT_DIR]
115
- IMAGE_FILENAMES [IMAGE_FILENAMES ...]
115
+ [-of OUTPUT_FILENAME]
116
+ IMAGE_PATHS [IMAGE_PATHS ...]
116
117
 
117
118
  positional arguments:
118
- IMAGE_FILENAMES set the image filenames
119
+ IMAGE_PATHS set the image filenames
119
120
 
120
121
  options:
121
122
  -h, --help show this help message and exit
@@ -133,6 +134,8 @@ options:
133
134
  default: '4')
134
135
  -od, --output-dir OUTPUT_DIR
135
136
  set default output folder (default: 'output')
137
+ -of, --output-filename OUTPUT_FILENAME
138
+ set output filename (default: 'None')
136
139
  ```
137
140
 
138
141
  <!--help-animate !-->
@@ -146,13 +149,17 @@ fotolab auto -h
146
149
  <!--help-auto !-->
147
150
 
148
151
  ```console
149
- usage: fotolab auto [-h] IMAGE_FILENAMES [IMAGE_FILENAMES ...]
152
+ usage: fotolab auto [-h] [-t TITLE] [-w WATERMARK_TEXT]
153
+ IMAGE_PATHS [IMAGE_PATHS ...]
150
154
 
151
155
  positional arguments:
152
- IMAGE_FILENAMES set the image filename
156
+ IMAGE_PATHS set the image filename
153
157
 
154
158
  options:
155
- -h, --help show this help message and exit
159
+ -h, --help show this help message and exit
160
+ -t, --title TITLE set the tile (default: 'None')
161
+ -w, --watermark WATERMARK_TEXT
162
+ set the watermark (default: 'kianmeng.org')
156
163
  ```
157
164
 
158
165
  <!--help-auto !-->
@@ -168,10 +175,10 @@ fotolab border -h
168
175
  ```console
169
176
  usage: fotolab border [-h] [-c COLOR] [-w WIDTH] [-wt WIDTH] [-wr WIDTH]
170
177
  [-wb WIDTH] [-wl WIDTH] [-op] [-od OUTPUT_DIR]
171
- IMAGE_FILENAMES [IMAGE_FILENAMES ...]
178
+ IMAGE_PATHS [IMAGE_PATHS ...]
172
179
 
173
180
  positional arguments:
174
- IMAGE_FILENAMES set the image filenames
181
+ IMAGE_PATHS set the image filenames
175
182
 
176
183
  options:
177
184
  -h, --help show this help message and exit
@@ -204,10 +211,10 @@ fotolab contrast -h
204
211
 
205
212
  ```console
206
213
  usage: fotolab contrast [-h] [-c CUTOFF] [-op] [-od OUTPUT_DIR]
207
- IMAGE_FILENAMES [IMAGE_FILENAMES ...]
214
+ IMAGE_PATHS [IMAGE_PATHS ...]
208
215
 
209
216
  positional arguments:
210
- IMAGE_FILENAMES set the image filename
217
+ IMAGE_PATHS set the image filename
211
218
 
212
219
  options:
213
220
  -h, --help show this help message and exit
@@ -231,10 +238,10 @@ fotolab halftone -h
231
238
 
232
239
  ```console
233
240
  usage: fotolab halftone [-h] [-ba] [-op] [-od OUTPUT_DIR] [-c CELLS] [-g]
234
- IMAGE_FILENAMES [IMAGE_FILENAMES ...]
241
+ IMAGE_PATHS [IMAGE_PATHS ...]
235
242
 
236
243
  positional arguments:
237
- IMAGE_FILENAMES set the image filename
244
+ IMAGE_PATHS set the image filename
238
245
 
239
246
  options:
240
247
  -h, --help show this help message and exit
@@ -283,10 +290,10 @@ fotolab rotate -h
283
290
 
284
291
  ```console
285
292
  usage: fotolab rotate [-h] [-r ROTATION] [-cw] [-op] [-od OUTPUT_DIR]
286
- IMAGE_FILENAMES [IMAGE_FILENAMES ...]
293
+ IMAGE_PATHS [IMAGE_PATHS ...]
287
294
 
288
295
  positional arguments:
289
- IMAGE_FILENAMES set the image filenames
296
+ IMAGE_PATHS set the image filenames
290
297
 
291
298
  options:
292
299
  -h, --help show this help message and exit
@@ -311,10 +318,10 @@ fotolab montage -h
311
318
 
312
319
  ```console
313
320
  usage: fotolab montage [-h] [-op] [-od OUTPUT_DIR]
314
- IMAGE_FILENAMES [IMAGE_FILENAMES ...]
321
+ IMAGE_PATHS [IMAGE_PATHS ...]
315
322
 
316
323
  positional arguments:
317
- IMAGE_FILENAMES set the image filenames
324
+ IMAGE_PATHS set the image filenames
318
325
 
319
326
  options:
320
327
  -h, --help show this help message and exit
@@ -337,10 +344,10 @@ fotolab resize -h
337
344
  ```console
338
345
  usage: fotolab resize [-h] [-c] [-l CANVAS_COLOR] [-W WIDTH | -H HEIGHT] [-op]
339
346
  [-od OUTPUT_DIR]
340
- IMAGE_FILENAMES [IMAGE_FILENAMES ...]
347
+ IMAGE_PATHS [IMAGE_PATHS ...]
341
348
 
342
349
  positional arguments:
343
- IMAGE_FILENAMES set the image filename
350
+ IMAGE_PATHS set the image filename
344
351
 
345
352
  options:
346
353
  -h, --help show this help message and exit
@@ -369,10 +376,10 @@ fotolab sharpen -h
369
376
  ```console
370
377
  usage: fotolab sharpen [-h] [-r RADIUS] [-p PERCENT] [-t THRESHOLD] [-ba]
371
378
  [-op] [-od OUTPUT_DIR]
372
- IMAGE_FILENAMES [IMAGE_FILENAMES ...]
379
+ IMAGE_PATHS [IMAGE_PATHS ...]
373
380
 
374
381
  positional arguments:
375
- IMAGE_FILENAMES set the image filenames
382
+ IMAGE_PATHS set the image filenames
376
383
 
377
384
  options:
378
385
  -h, --help show this help message and exit
@@ -408,10 +415,10 @@ usage: fotolab watermark [-h] [-t WATERMARK_TEXT]
408
415
  [-a ALPHA_VALUE] [--camera]
409
416
  [-l | --lowercase | --no-lowercase] [-op]
410
417
  [-od OUTPUT_DIR]
411
- IMAGE_FILENAMES [IMAGE_FILENAMES ...]
418
+ IMAGE_PATHS [IMAGE_PATHS ...]
412
419
 
413
420
  positional arguments:
414
- IMAGE_FILENAMES set the image filenames
421
+ IMAGE_PATHS set the image filenames
415
422
 
416
423
  options:
417
424
  -h, --help show this help message and exit
@@ -0,0 +1,22 @@
1
+ fotolab/__init__.py,sha256=xYHsuP-sNzx6xCzWpierNFJfW1qKEzDd54-UYsHgX5o,3481
2
+ fotolab/__main__.py,sha256=Wk11t_zKs3wJ-okM3yv7Mtj1pRwQZu3NCmpteBwn4Hs,810
3
+ fotolab/cli.py,sha256=Ebx6RQM5Dr8PsIqQ4NOySxnGMzGGI0U7CTawYfHROlk,4499
4
+ fotolab/subcommands/__init__.py,sha256=l3DlIaJ3u3jGjnC1H1yV8LZ_nPqOLJ6gikD4BCaMAQ0,1129
5
+ fotolab/subcommands/animate.py,sha256=4e3PSFxxw4tQFMMMMmJKdDyuon_ZkX4AI3Am2K-Afmw,5857
6
+ fotolab/subcommands/auto.py,sha256=j34xiZlbl8qFpW8kFr3Z-J0Xk08Qiz8Stn1O9I2y1io,3572
7
+ fotolab/subcommands/border.py,sha256=3cIFrHyixV2iE25v-O6heUBsWrCmkBnZvHW7iI0qTo8,4743
8
+ fotolab/subcommands/contrast.py,sha256=6MkjUny4I7gtsFyXJfzKzDVLoY-b9YvHr3ykXPKL7ww,3802
9
+ fotolab/subcommands/env.py,sha256=QoxRvzZKgmoHTUxDV4QYhdChCpMWs5TbXFY_qIpIQpE,1469
10
+ fotolab/subcommands/halftone.py,sha256=hJERzqFsFAwOgPi1eg0MyxW0yt2CzrM3DcN1iDOGdOI,6965
11
+ fotolab/subcommands/info.py,sha256=H3voMi67cKoHT2Mu4RUNQBPdb_MspetPjhOvy-YyNnE,3563
12
+ fotolab/subcommands/montage.py,sha256=Z5hGjgR8V0OiWFg4BB--QIYw2ZGbocQMfR_cPmzrJn4,2751
13
+ fotolab/subcommands/resize.py,sha256=Uv2X_-Bpt0y-ns93t3-EVWQtZfMPLu2HriScCQGYifU,5998
14
+ fotolab/subcommands/rotate.py,sha256=9uSI724K-XspuJOTQX21tesPm3osYyn4AR9mKWtsQTI,2972
15
+ fotolab/subcommands/sharpen.py,sha256=3_r6SZPYZodS1_4yNlFUb-5uVPkOgWGKBw1-6e06iF4,3698
16
+ fotolab/subcommands/watermark.py,sha256=JelYFSZ0_RgHQLEG82r3Jzkw_nX8OSZ1wuMC-zmA0Hs,11499
17
+ fotolab-0.34.2.dist-info/licenses/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
18
+ fotolab-0.34.2.dist-info/METADATA,sha256=bfaZqujVzur_wywXdQBMHwZShlcyS8NVWe8N0WUzt38,15101
19
+ fotolab-0.34.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ fotolab-0.34.2.dist-info/entry_points.txt,sha256=0e1go9plFpqj5FP-OpV2acxTAx3ViI59PMXuhejvgcQ,45
21
+ fotolab-0.34.2.dist-info/top_level.txt,sha256=XUJ3gdpsbjohoZCLdVlbQrxAUDkbQg7WwGQG2DaN0t4,8
22
+ fotolab-0.34.2.dist-info/RECORD,,
@@ -1,22 +0,0 @@
1
- fotolab/__init__.py,sha256=0UZ6OZFCRkc2Fwc4LT1UtE9JdK3uq0uL8fSSVSOuVyo,3161
2
- fotolab/__main__.py,sha256=Wk11t_zKs3wJ-okM3yv7Mtj1pRwQZu3NCmpteBwn4Hs,810
3
- fotolab/cli.py,sha256=Ebx6RQM5Dr8PsIqQ4NOySxnGMzGGI0U7CTawYfHROlk,4499
4
- fotolab/subcommands/__init__.py,sha256=l3DlIaJ3u3jGjnC1H1yV8LZ_nPqOLJ6gikD4BCaMAQ0,1129
5
- fotolab/subcommands/animate.py,sha256=pyoMfRtE-07EhFni2yS8xEoRV9up5T4VcF4Qw8me4Hs,5879
6
- fotolab/subcommands/auto.py,sha256=gkpZ15JwzSlQPYSqGSeeGu0SLKps3A2cEn1dYpyQEyk,3584
7
- fotolab/subcommands/border.py,sha256=TSr1rqAPWe1ZThkZan_WiMQdSTuua2YPXg6fYcvFitQ,4715
8
- fotolab/subcommands/contrast.py,sha256=yoIcJbqz3S0kzMOD39MVIcLMj2GTtVN3uPI8wsoHysQ,3711
9
- fotolab/subcommands/env.py,sha256=QoxRvzZKgmoHTUxDV4QYhdChCpMWs5TbXFY_qIpIQpE,1469
10
- fotolab/subcommands/halftone.py,sha256=a0rZWE6elXhMSCyE-w_FKEMoDGK9Afs9oF3yT09ZhcA,6768
11
- fotolab/subcommands/info.py,sha256=H3voMi67cKoHT2Mu4RUNQBPdb_MspetPjhOvy-YyNnE,3563
12
- fotolab/subcommands/montage.py,sha256=ax-TG5FixBRFHPGkVTq8kOtRL900K6nujeBYHZ0yBAE,2622
13
- fotolab/subcommands/resize.py,sha256=d1-_xoTB8Vf62bm7H_9MfXrMaJhlzLiUbePgTdYzaKs,5816
14
- fotolab/subcommands/rotate.py,sha256=-yZ7pNW7PDOPBWbKPDut9z8bBe1NocfVBV_6V6tqq-4,2945
15
- fotolab/subcommands/sharpen.py,sha256=vGBJHHHUaQ2_SHS-E1mHfNVIfnnNYcCk5ritajWTik8,3594
16
- fotolab/subcommands/watermark.py,sha256=BkUa1tH86Ii_u03ypj9Tm0u9v7s9y9Jiz3aLpjmFDb8,11355
17
- fotolab-0.34.0.dist-info/licenses/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
18
- fotolab-0.34.0.dist-info/METADATA,sha256=AFHeSlX8rf-ueN9HHsJ9nBzfu_Nw2UoeGSJaqsFDBrc,14816
19
- fotolab-0.34.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- fotolab-0.34.0.dist-info/entry_points.txt,sha256=0e1go9plFpqj5FP-OpV2acxTAx3ViI59PMXuhejvgcQ,45
21
- fotolab-0.34.0.dist-info/top_level.txt,sha256=XUJ3gdpsbjohoZCLdVlbQrxAUDkbQg7WwGQG2DaN0t4,8
22
- fotolab-0.34.0.dist-info/RECORD,,