fotolab 0.34.1__tar.gz → 0.34.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {fotolab-0.34.1/src/fotolab.egg-info → fotolab-0.34.2}/PKG-INFO +1 -1
  2. {fotolab-0.34.1 → fotolab-0.34.2}/pyproject.toml +1 -1
  3. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/__init__.py +12 -0
  4. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/animate.py +3 -4
  5. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/border.py +11 -11
  6. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/contrast.py +24 -24
  7. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/halftone.py +15 -15
  8. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/montage.py +17 -15
  9. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/resize.py +61 -62
  10. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/rotate.py +9 -9
  11. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/sharpen.py +19 -17
  12. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/watermark.py +14 -12
  13. {fotolab-0.34.1 → fotolab-0.34.2/src/fotolab.egg-info}/PKG-INFO +1 -1
  14. fotolab-0.34.2/tests/test_resize_subcommand.py +63 -0
  15. fotolab-0.34.1/tests/test_resize_subcommand.py +0 -20
  16. {fotolab-0.34.1 → fotolab-0.34.2}/LICENSE.md +0 -0
  17. {fotolab-0.34.1 → fotolab-0.34.2}/README.md +0 -0
  18. {fotolab-0.34.1 → fotolab-0.34.2}/setup.cfg +0 -0
  19. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/__main__.py +0 -0
  20. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/cli.py +0 -0
  21. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/__init__.py +0 -0
  22. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/auto.py +0 -0
  23. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/env.py +0 -0
  24. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab/subcommands/info.py +0 -0
  25. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab.egg-info/SOURCES.txt +0 -0
  26. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab.egg-info/dependency_links.txt +0 -0
  27. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab.egg-info/entry_points.txt +0 -0
  28. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab.egg-info/requires.txt +0 -0
  29. {fotolab-0.34.1 → fotolab-0.34.2}/src/fotolab.egg-info/top_level.txt +0 -0
  30. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_animate_subcommand.py +0 -0
  31. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_auto_subcommand.py +0 -0
  32. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_border_subcommand.py +0 -0
  33. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_contrast_subcommand.py +0 -0
  34. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_env_subcommand.py +0 -0
  35. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_halftone_subcommand.py +0 -0
  36. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_help_flag.py +0 -0
  37. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_info_subcommand.py +0 -0
  38. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_montage_subcommand.py +0 -0
  39. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_quiet_flag.py +0 -0
  40. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_rotate_subcommand.py +0 -0
  41. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_sharpen_subcommand.py +0 -0
  42. {fotolab-0.34.1 → fotolab-0.34.2}/tests/test_watermark_subcommand.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fotolab
3
- Version: 0.34.1
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
@@ -7,7 +7,7 @@ where = ["src"]
7
7
 
8
8
  [project]
9
9
  name = "fotolab"
10
- version = "0.34.1"
10
+ version = "0.34.2"
11
11
  description = "A console program to manipulate photos."
12
12
  authors = [{name = "Kian-Meng Ang", email = "kianmeng@cpan.org"}]
13
13
  requires-python = ">=3.10"
@@ -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
 
@@ -164,10 +163,10 @@ def run(args: argparse.Namespace) -> None:
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:
@@ -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
 
@@ -136,15 +136,15 @@ def run(args: argparse.Namespace) -> None:
136
136
  log.debug(args)
137
137
 
138
138
  for image_filepath in [Path(f) for f in args.image_paths]:
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
- )
146
-
147
- save_image(args, bordered_image, image_filepath, "border")
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
+
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
 
@@ -103,25 +103,25 @@ def run(args: argparse.Namespace) -> None:
103
103
  log.debug(args)
104
104
 
105
105
  for image_path_str in args.image_paths:
106
- original_image = Image.open(image_path_str)
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_path_str), "contrast")
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
 
@@ -113,21 +113,21 @@ def run(args: argparse.Namespace) -> None:
113
113
 
114
114
  for image_path_str in args.image_paths:
115
115
  image_filename = Path(image_path_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",
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(
@@ -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
 
@@ -72,22 +73,23 @@ def run(args: argparse.Namespace) -> None:
72
73
  """
73
74
  log.debug(args)
74
75
  images = []
75
- for image_path_str in args.image_paths:
76
- image_filename = Path(image_path_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_paths[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
 
@@ -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
 
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
+
140
137
  for image_filepath in [Path(f) for f in args.image_paths]:
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")
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
 
@@ -91,11 +91,11 @@ def run(args: argparse.Namespace) -> None:
91
91
  for image_path_str in args.image_paths:
92
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
 
@@ -116,19 +116,21 @@ def run(args: argparse.Namespace) -> None:
116
116
  log.debug(args)
117
117
 
118
118
  for image_path_str in args.image_paths:
119
- original_image = Image.open(image_path_str)
120
- sharpen_image = original_image.filter(
121
- ImageFilter.UnsharpMask(
122
- args.radius, percent=args.percent, threshold=args.threshold
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_path_str),
129
- original_image,
130
- sharpen_image,
131
- "sharpen",
132
- )
133
- else:
134
- save_image(args, sharpen_image, Path(image_path_str), "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__)
@@ -192,7 +192,19 @@ def run(args: argparse.Namespace) -> None:
192
192
 
193
193
  for image_path_str in args.image_paths:
194
194
  try:
195
- image: Image.Image = Image.open(image_path_str)
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
209
  log.error("Image file not found: %s", image_path_str)
198
210
  continue
@@ -200,16 +212,6 @@ def run(args: argparse.Namespace) -> None:
200
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_path_str, 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_path_str), "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.1
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
@@ -0,0 +1,63 @@
1
+ # Copyright (C) 2024,2025 Kian-Meng Ang
2
+
3
+ # This program is free software: you can redistribute it and/or modify it under
4
+ # the terms of the GNU Affero General Public License as published by the Free
5
+ # Software Foundation, either version 3 of the License, or (at your option) any
6
+ # later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but WITHOUT
9
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11
+ # details.
12
+ #
13
+ # You should have received a copy of the GNU Affero General Public License
14
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+
17
+ def test_resize_subcommand(cli_runner, image_file):
18
+ img_path = image_file("sample.png")
19
+ ret = cli_runner("resize", str(img_path), "--width", "200")
20
+ assert ret.returncode == 0
21
+
22
+
23
+ def test_resize_subcommand_mutually_exclusive_failure(cli_runner, image_file):
24
+ img_path = image_file("sample.png")
25
+ ret = cli_runner(
26
+ "resize", str(img_path), "--width", "200", "--height", "200"
27
+ )
28
+ assert ret.returncode != 0
29
+ assert (
30
+ "error: argument -W/--width and -H/--height are mutually exclusive when not using --canvas"
31
+ in ret.stderr
32
+ )
33
+
34
+
35
+ def test_resize_subcommand_canvas_required_failure(cli_runner, image_file):
36
+ img_path = image_file("sample.png")
37
+ ret = cli_runner("resize", str(img_path), "--canvas", "--width", "200")
38
+ assert ret.returncode != 0
39
+ assert (
40
+ "error: argument -W/--width and -H/--height are required when using --canvas"
41
+ in ret.stderr
42
+ )
43
+
44
+ ret = cli_runner("resize", str(img_path), "--canvas", "--height", "200")
45
+ assert ret.returncode != 0
46
+ assert (
47
+ "error: argument -W/--width and -H/--height are required when using --canvas"
48
+ in ret.stderr
49
+ )
50
+
51
+
52
+ def test_resize_subcommand_canvas_success(cli_runner, image_file):
53
+ img_path = image_file("sample.png")
54
+ ret = cli_runner(
55
+ "resize",
56
+ str(img_path),
57
+ "--canvas",
58
+ "--width",
59
+ "200",
60
+ "--height",
61
+ "200",
62
+ )
63
+ assert ret.returncode == 0
@@ -1,20 +0,0 @@
1
- # Copyright (C) 2024,2025 Kian-Meng Ang
2
-
3
- # This program is free software: you can redistribute it and/or modify it under
4
- # the terms of the GNU Affero General Public License as published by the Free
5
- # Software Foundation, either version 3 of the License, or (at your option) any
6
- # later version.
7
- #
8
- # This program is distributed in the hope that it will be useful, but WITHOUT
9
- # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
- # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11
- # details.
12
- #
13
- # You should have received a copy of the GNU Affero General Public License
14
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
-
16
-
17
- def test_resize_subcommand(cli_runner, image_file):
18
- img_path = image_file("sample.png")
19
- ret = cli_runner("resize", str(img_path), "--width", "200")
20
- assert ret.returncode == 0
File without changes
File without changes
File without changes
File without changes