fotolab 0.30.0__tar.gz → 0.31.1__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 (56) hide show
  1. {fotolab-0.30.0 → fotolab-0.31.1}/.gitignore +1 -0
  2. {fotolab-0.30.0 → fotolab-0.31.1}/CHANGELOG.md +30 -12
  3. {fotolab-0.30.0 → fotolab-0.31.1}/PKG-INFO +7 -2
  4. {fotolab-0.30.0 → fotolab-0.31.1}/README.md +6 -1
  5. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/__init__.py +7 -8
  6. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/animate.py +13 -7
  7. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/auto.py +1 -0
  8. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/border.py +8 -8
  9. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/info.py +16 -22
  10. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/watermark.py +41 -10
  11. {fotolab-0.30.0 → fotolab-0.31.1}/tests/conftest.py +11 -0
  12. fotolab-0.31.1/tests/test_animate_subcommand.py +7 -0
  13. fotolab-0.31.1/tests/test_auto_subcommand.py +7 -0
  14. fotolab-0.31.1/tests/test_border_subcommand.py +6 -0
  15. fotolab-0.31.1/tests/test_contrast_subcommand.py +6 -0
  16. fotolab-0.31.1/tests/test_env_subcommand.py +23 -0
  17. fotolab-0.31.1/tests/test_halftone_subcommand.py +6 -0
  18. fotolab-0.31.1/tests/test_info_subcommand.py +6 -0
  19. fotolab-0.31.1/tests/test_montage_subcommand.py +6 -0
  20. fotolab-0.31.1/tests/test_resize_subcommand.py +6 -0
  21. fotolab-0.31.1/tests/test_rotate_subcommand.py +6 -0
  22. fotolab-0.31.1/tests/test_sharpen_subcommand.py +6 -0
  23. fotolab-0.31.1/tests/test_watermark_subcommand.py +6 -0
  24. fotolab-0.30.0/tests/test_env_subcommand.py +0 -10
  25. {fotolab-0.30.0 → fotolab-0.31.1}/.coveragerc +0 -0
  26. {fotolab-0.30.0 → fotolab-0.31.1}/.pre-commit-config.yaml +0 -0
  27. {fotolab-0.30.0 → fotolab-0.31.1}/.python-version +0 -0
  28. {fotolab-0.30.0 → fotolab-0.31.1}/CONTRIBUTING.md +0 -0
  29. {fotolab-0.30.0 → fotolab-0.31.1}/LICENSE.md +0 -0
  30. {fotolab-0.30.0 → fotolab-0.31.1}/Pipfile +0 -0
  31. {fotolab-0.30.0 → fotolab-0.31.1}/Pipfile.lock +0 -0
  32. {fotolab-0.30.0 → fotolab-0.31.1}/docs/Makefile +0 -0
  33. {fotolab-0.30.0 → fotolab-0.31.1}/docs/make.bat +0 -0
  34. {fotolab-0.30.0 → fotolab-0.31.1}/docs/source/CHANGELOG.md +0 -0
  35. {fotolab-0.30.0 → fotolab-0.31.1}/docs/source/CONTRIBUTING.md +0 -0
  36. {fotolab-0.30.0 → fotolab-0.31.1}/docs/source/LICENSE.md +0 -0
  37. {fotolab-0.30.0 → fotolab-0.31.1}/docs/source/README.md +0 -0
  38. {fotolab-0.30.0 → fotolab-0.31.1}/docs/source/_static/logo.jpg +0 -0
  39. {fotolab-0.30.0 → fotolab-0.31.1}/docs/source/conf.py +0 -0
  40. {fotolab-0.30.0 → fotolab-0.31.1}/docs/source/index.rst +0 -0
  41. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/__main__.py +0 -0
  42. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/cli.py +0 -0
  43. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/__init__.py +0 -0
  44. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/contrast.py +0 -0
  45. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/env.py +0 -0
  46. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/halftone.py +0 -0
  47. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/montage.py +0 -0
  48. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/resize.py +0 -0
  49. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/rotate.py +0 -0
  50. {fotolab-0.30.0 → fotolab-0.31.1}/fotolab/subcommands/sharpen.py +0 -0
  51. {fotolab-0.30.0 → fotolab-0.31.1}/generate +0 -0
  52. {fotolab-0.30.0 → fotolab-0.31.1}/noxfile.py +0 -0
  53. {fotolab-0.30.0 → fotolab-0.31.1}/pyproject.toml +0 -0
  54. {fotolab-0.30.0 → fotolab-0.31.1}/tests/__init__.py +0 -0
  55. {fotolab-0.30.0 → fotolab-0.31.1}/tests/test_help_flag.py +0 -0
  56. {fotolab-0.30.0 → fotolab-0.31.1}/tests/test_quiet_flag.py +0 -0
@@ -162,5 +162,6 @@ cython_debug/
162
162
  # heatmaps
163
163
  *.png
164
164
  .aider*
165
+ .opencode
165
166
  TODO
166
167
  output
@@ -7,6 +7,24 @@ and this project adheres to [0-based versioning](https://0ver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.31.1 (2025-06-01)
11
+
12
+ - Code format
13
+ - Fix incorrect function name due to refactoring
14
+ - Fix missing option in `auto` subcommand
15
+ - Refactor `info` subcommand and improve EXIF extraction
16
+ - Remove unused type
17
+ - Update help message in readme
18
+
19
+ ## v0.31.0 (2025-05-25)
20
+
21
+ - Add basic tests for several CLI subcommands
22
+ - Add test for auto subcommand and image fixture
23
+ - Add watermark transparency option and improve error handling
24
+ - Fix markdown in changelog
25
+ - Refine `env` subcommand test assertions
26
+ - Standardize border dimension return as a tuple
27
+
10
28
  ## v0.30.0 (2025-05-18)
11
29
 
12
30
  - Add WEBP `quality`, `lossless`, and `method` args
@@ -14,14 +32,14 @@ and this project adheres to [0-based versioning](https://0ver.org/).
14
32
  - Close image files after creating animation
15
33
  - Update help message generated by latest Python to readme
16
34
  - Update help message in readme
17
- - Validate duration for animate command
35
+ - Validate duration for `animate` subcommand
18
36
 
19
37
  ## v0.29.2 (2025-05-11)
20
38
 
21
39
  - Bump deps
22
40
  - Code format
23
41
  - Improve info subcommand by adding error handling for missing keys
24
- - Remove default value for image filename argument in info subcommand
42
+ - Remove default value for image filename argument in `info` subcommand
25
43
  - Use context manager to open image and remove redundant image close.
26
44
 
27
45
  ## v0.29.1 (2025-05-04)
@@ -34,8 +52,8 @@ and this project adheres to [0-based versioning](https://0ver.org/).
34
52
 
35
53
  ## v0.29.0 (2025-04-27)
36
54
 
37
- - Add `cells` argument to halftone subcommand and function
38
- - Add grayscale option to halftone subcommand for grayscale conversion
55
+ - Add `cells` argument to `halftone` subcommand and function
56
+ - Add `grayscale` option to `halftone` subcommand for grayscale conversion
39
57
  - Bump deps
40
58
  - Calculate dot radius relative to cell size based on brightness.
41
59
  - Refactor CLI to handle missing subcommand execution function gracefully
@@ -51,22 +69,22 @@ and this project adheres to [0-based versioning](https://0ver.org/).
51
69
  ## v0.28.5 (2025-04-13)
52
70
 
53
71
  - Group build and publish package commands together
54
- - Improve contrast subcommand and add pylint disable
55
- - Prompt to publish package in release nox job
56
- - Resolve pylint raised issue
57
- - Validate cutoff value is between 0 and 50 in contrast subcommand
72
+ - Improve `contrast` subcommand and add `pylint` disable
73
+ - Prompt to publish package in release `nox` job
74
+ - Resolve `pylint` raised issue
75
+ - Validate `cutoff` value is between 0 and 50 in `contrast` subcommand
58
76
 
59
77
  ## v0.28.4 (2025-04-06)
60
78
 
61
79
  - Build the release after release `nox` job
62
80
  - Bump `pre-commit` for `flake8`
63
81
  - Fix incorrect git options
64
- - Install flit as deps for dev environment
82
+ - Install `flit` as deps for dev environment
65
83
  - Update help message in readme
66
84
 
67
85
  ## v0.28.3 (2025-03-30)
68
86
 
69
- - Bump `pre-commit` hook for validate-project
87
+ - Bump `pre-commit` hook for `validate-project`
70
88
  - Commit changes after bump release
71
89
  - Remove exception handling as it's handled in parent calling function
72
90
  - Resolve W0718: Catching too general exception Exception
@@ -76,7 +94,7 @@ and this project adheres to [0-based versioning](https://0ver.org/).
76
94
  ## v0.28.2 (2025-03-23)
77
95
 
78
96
  - Accept width in integer instead of string
79
- - Bump pre-commit hook for validate-project
97
+ - Bump `pre-commit` hook for `validate-project`
80
98
  - Handle file not found and other exceptions when opening/processing images
81
99
  - Remove all unnecessary exceptions handling
82
100
  - Use Union for border width return type to allow int or tuple
@@ -97,7 +115,7 @@ and this project adheres to [0-based versioning](https://0ver.org/).
97
115
  - Refactor getting output filename when saving image
98
116
  - Remove `--output-dir` and `--open` args from main cli
99
117
  - Update help message in readme
100
- - Use single `save_gif_image` function in sharpen and halftone subcommand
118
+ - Use single `save_gif_image` function in sharpen and `halftone` subcommand
101
119
 
102
120
  ## v0.27.2 (2025-03-02)
103
121
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fotolab
3
- Version: 0.30.0
3
+ Version: 0.31.1
4
4
  Summary: A console program that manipulate images.
5
5
  Keywords: photography,photo
6
6
  Author-email: Kian-Meng Ang <kianmeng@cpan.org>
@@ -371,7 +371,8 @@ fotolab watermark -h
371
371
  usage: fotolab watermark [-h] [-t WATERMARK_TEXT]
372
372
  [-p {top-left,top-right,bottom-left,bottom-right}]
373
373
  [-pd PADDING] [-fs FONT_SIZE] [-fc FONT_COLOR]
374
- [-ow OUTLINE_WIDTH] [-oc OUTLINE_COLOR] [--camera]
374
+ [-ow OUTLINE_WIDTH] [-oc OUTLINE_COLOR]
375
+ [-a ALPHA_VALUE] [--camera]
375
376
  [-l | --lowercase | --no-lowercase] [-op]
376
377
  [-od OUTPUT_DIR]
377
378
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
@@ -400,6 +401,10 @@ options:
400
401
  -oc, --outline-color OUTLINE_COLOR
401
402
  set the outline color of the watermark text (default:
402
403
  'black')
404
+ -a, --alpha ALPHA_VALUE
405
+ set the transparency of the watermark text (0-255,
406
+ where 0 is fully transparent and 255 is fully opaque;
407
+ default: '128')
403
408
  --camera use camera metadata as watermark
404
409
  -l, --lowercase, --no-lowercase
405
410
  lowercase the watermark text
@@ -348,7 +348,8 @@ fotolab watermark -h
348
348
  usage: fotolab watermark [-h] [-t WATERMARK_TEXT]
349
349
  [-p {top-left,top-right,bottom-left,bottom-right}]
350
350
  [-pd PADDING] [-fs FONT_SIZE] [-fc FONT_COLOR]
351
- [-ow OUTLINE_WIDTH] [-oc OUTLINE_COLOR] [--camera]
351
+ [-ow OUTLINE_WIDTH] [-oc OUTLINE_COLOR]
352
+ [-a ALPHA_VALUE] [--camera]
352
353
  [-l | --lowercase | --no-lowercase] [-op]
353
354
  [-od OUTPUT_DIR]
354
355
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
@@ -377,6 +378,10 @@ options:
377
378
  -oc, --outline-color OUTLINE_COLOR
378
379
  set the outline color of the watermark text (default:
379
380
  'black')
381
+ -a, --alpha ALPHA_VALUE
382
+ set the transparency of the watermark text (0-255,
383
+ where 0 is fully transparent and 255 is fully opaque;
384
+ default: '128')
380
385
  --camera use camera metadata as watermark
381
386
  -l, --lowercase, --no-lowercase
382
387
  lowercase the watermark text
@@ -24,7 +24,7 @@ from pathlib import Path
24
24
 
25
25
  from PIL import Image
26
26
 
27
- __version__ = "0.30.0"
27
+ __version__ = "0.31.1"
28
28
 
29
29
  log = logging.getLogger(__name__)
30
30
 
@@ -59,15 +59,14 @@ def save_gif_image(
59
59
  _open_image(new_filename)
60
60
 
61
61
 
62
- def save_image(args, new_image, output_filename, subcommand):
62
+ def save_image(
63
+ args: argparse.Namespace,
64
+ new_image: Image.Image,
65
+ output_filename: str,
66
+ subcommand: str,
67
+ ) -> None:
63
68
  """Save image after image operation.
64
69
 
65
- Args:
66
- args (argparse.Namespace): Config from command line arguments
67
- new_image(PIL.Image.Image): Modified image
68
- output_filename(str): Save filename image
69
- subcommand(str): Subcommand used to call this function
70
-
71
70
  Returns:
72
71
  None
73
72
  """
@@ -73,7 +73,10 @@ def build_subparser(subparsers) -> None:
73
73
  dest="duration",
74
74
  type=_validate_duration,
75
75
  default=2500,
76
- help="set the duration in milliseconds (must be a positive integer, default: '%(default)s')",
76
+ help=(
77
+ "set the duration in milliseconds "
78
+ "(must be a positive integer, default: '%(default)s')"
79
+ ),
77
80
  metavar="DURATION",
78
81
  )
79
82
 
@@ -120,7 +123,10 @@ def build_subparser(subparsers) -> None:
120
123
  type=int,
121
124
  default=4,
122
125
  choices=range(0, 7),
123
- help="set WEBP encoding method (0=fast, 6=slow/best, default: '%(default)s')",
126
+ help=(
127
+ "set WEBP encoding method "
128
+ "(0=fast, 6=slow/best, default: '%(default)s')"
129
+ ),
124
130
  metavar="METHOD",
125
131
  )
126
132
 
@@ -170,17 +176,17 @@ def run(args: argparse.Namespace) -> None:
170
176
  "save_all": True,
171
177
  "duration": args.duration,
172
178
  "loop": args.loop,
173
- "optimize": True, # General optimization, good for GIF
179
+ "optimize": True,
174
180
  }
175
181
 
182
+ # Pillow's WEBP save doesn't use a general 'optimize' like GIF.
183
+ # Specific WEBP params like 'method' and 'quality' control this. We
184
+ # can remove 'optimize' if it causes issues or is ignored for WEBP.
185
+ # For now, let's keep it, Pillow might handle it or ignore it.
176
186
  if args.format == "webp":
177
187
  save_kwargs["quality"] = args.webp_quality
178
188
  save_kwargs["lossless"] = args.webp_lossless
179
189
  save_kwargs["method"] = args.webp_method
180
- # Pillow's WEBP save doesn't use a general 'optimize' like GIF.
181
- # Specific WEBP params like 'method' and 'quality' control this.
182
- # We can remove 'optimize' if it causes issues or is ignored for WEBP.
183
- # For now, let's keep it, Pillow might handle it or ignore it.
184
190
 
185
191
  main_frame.save(new_filename, **save_kwargs)
186
192
  finally:
@@ -71,6 +71,7 @@ def run(args: argparse.Namespace) -> None:
71
71
  "canvas": False,
72
72
  "lowercase": False,
73
73
  "before_after": False,
74
+ "alpha": 128,
74
75
  }
75
76
  combined_args = argparse.Namespace(**vars(args), **extra_args)
76
77
  combined_args.overwrite = True
@@ -17,7 +17,7 @@
17
17
 
18
18
  import argparse
19
19
  import logging
20
- from typing import Tuple, Union
20
+ from typing import Tuple
21
21
 
22
22
  from PIL import Image, ImageColor, ImageOps
23
23
 
@@ -26,7 +26,7 @@ from fotolab import save_image
26
26
  log = logging.getLogger(__name__)
27
27
 
28
28
 
29
- def build_subparser(subparsers) -> None:
29
+ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
30
30
  """Build the subparser."""
31
31
  border_parser = subparsers.add_parser("border", help="add border to image")
32
32
 
@@ -150,17 +150,16 @@ def run(args: argparse.Namespace) -> None:
150
150
 
151
151
  def get_border(
152
152
  args: argparse.Namespace,
153
- ) -> Union[Tuple[int, int, int, int], int]:
153
+ ) -> Tuple[int, int, int, int]:
154
154
  """Calculate the border dimensions.
155
155
 
156
156
  Args:
157
157
  args (argparse.Namespace): Command line arguments
158
158
 
159
159
  Returns:
160
- Union[Tuple[int, int, int, int], int]: Border dimensions in pixels.
161
- If individual widths are specified, returns a tuple of (left, top,
162
- right, bottom) widths. Otherwise, returns a uniform width for all
163
- sides.
160
+ Tuple[int, int, int, int]: Border dimensions in pixels as (left, top,
161
+ right, bottom) widths. If individual widths are not specified,
162
+ a uniform width is returned for all sides.
164
163
  """
165
164
  if any(
166
165
  [
@@ -176,4 +175,5 @@ def get_border(
176
175
  args.width_right,
177
176
  args.width_bottom,
178
177
  )
179
- return args.width
178
+ # If no individual widths are specified, use the general width for all sides
179
+ return (args.width, args.width, args.width, args.width)
@@ -81,31 +81,25 @@ def run(args: argparse.Namespace) -> None:
81
81
  return
82
82
 
83
83
  output_info = []
84
- specific_info_requested = False
85
84
 
86
- if args.camera:
87
- specific_info_requested = True
88
- output_info.append(camera_metadata(exif_tags))
85
+ if args.camera:
86
+ output_info.append(get_formatted_camera_info(exif_tags))
89
87
 
90
- if args.datetime:
91
- specific_info_requested = True
92
- output_info.append(datetime(exif_tags))
88
+ if args.datetime:
89
+ output_info.append(get_formatted_datetime_info(exif_tags))
93
90
 
94
- if specific_info_requested:
95
- print("\n".join(output_info))
96
- else:
97
- # Print all tags if no specific info was requested
98
- tag_name_width = max(map(len, exif_tags))
99
- for tag_name, tag_value in exif_tags.items():
100
- print(f"{tag_name:<{tag_name_width}}: {tag_value}")
91
+ if output_info: # Check if any specific info was added
92
+ print("\n".join(output_info))
93
+ else:
94
+ # Print all tags if no specific info was requested
95
+ tag_name_width = max(map(len, exif_tags))
96
+ for tag_name, tag_value in exif_tags.items():
97
+ print(f"{tag_name:<{tag_name_width}}: {tag_value}")
101
98
 
102
99
 
103
100
  def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
104
101
  """Extract Exif metadata from image."""
105
- try:
106
- exif = image._getexif()
107
- except AttributeError:
108
- exif = None
102
+ exif = image.getexif()
109
103
 
110
104
  log.debug(exif)
111
105
 
@@ -122,13 +116,13 @@ def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
122
116
  return filtered_info
123
117
 
124
118
 
125
- def datetime(exif_tags: dict):
126
- """Extract datetime metadata."""
119
+ def get_formatted_datetime_info(exif_tags: dict):
120
+ """Extract and format datetime metadata."""
127
121
  return exif_tags.get("DateTime", "Not available")
128
122
 
129
123
 
130
- def camera_metadata(exif_tags: dict):
131
- """Extract camera and model metadata."""
124
+ def get_formatted_camera_info(exif_tags: dict):
125
+ """Extract and format camera make and model metadata."""
132
126
  make = exif_tags.get("Make", "")
133
127
  model = exif_tags.get("Model", "")
134
128
  metadata = f"{make} {model}"
@@ -24,7 +24,7 @@ from typing import Tuple
24
24
  from PIL import Image, ImageColor, ImageDraw, ImageFont, ImageSequence
25
25
 
26
26
  from fotolab import save_image
27
- from fotolab.subcommands.info import camera_metadata
27
+ from fotolab.subcommands.info import get_formatted_camera_info
28
28
 
29
29
  log: logging.Logger = logging.getLogger(__name__)
30
30
 
@@ -129,6 +129,21 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
129
129
  metavar="OUTLINE_COLOR",
130
130
  )
131
131
 
132
+ watermark_parser.add_argument(
133
+ "-a",
134
+ "--alpha",
135
+ dest="alpha",
136
+ type=int,
137
+ default=128,
138
+ choices=range(0, 256),
139
+ metavar="ALPHA_VALUE",
140
+ help=(
141
+ "set the transparency of the watermark text (0-255, "
142
+ "where 0 is fully transparent and 255 is fully opaque; "
143
+ "default: '%(default)s')"
144
+ ),
145
+ )
146
+
132
147
  watermark_parser.add_argument(
133
148
  "--camera",
134
149
  default=False,
@@ -176,7 +191,15 @@ def run(args: argparse.Namespace) -> None:
176
191
  log.debug(args)
177
192
 
178
193
  for image_filename in args.image_filenames:
179
- image: Image.Image = Image.open(image_filename)
194
+ try:
195
+ image: Image.Image = Image.open(image_filename)
196
+ except FileNotFoundError:
197
+ log.error("Image file not found: %s", image_filename)
198
+ continue
199
+ except Exception as e:
200
+ log.error("Could not open image %s: %s", image_filename, e)
201
+ continue
202
+
180
203
  if image.format == "GIF":
181
204
  watermark_gif_image(image, image_filename, args)
182
205
  else:
@@ -202,7 +225,7 @@ def watermark_gif_image(
202
225
  frames: list[Image.Image] = []
203
226
  for frame in ImageSequence.Iterator(original_image):
204
227
  watermarked_frame: Image.Image = watermark_image(
205
- args, frame.convert("RGBA")
228
+ args, frame.convert("RGBA"), args.alpha
206
229
  )
207
230
  frames.append(watermarked_frame)
208
231
 
@@ -241,11 +264,11 @@ def watermark_non_gif_image(
241
264
  Returns:
242
265
  Image.Image: The watermarked image
243
266
  """
244
- return watermark_image(args, original_image)
267
+ return watermark_image(args, original_image, args.alpha)
245
268
 
246
269
 
247
270
  def watermark_image(
248
- args: argparse.Namespace, original_image: Image.Image
271
+ args: argparse.Namespace, original_image: Image.Image, alpha: int
249
272
  ) -> Image.Image:
250
273
  """Watermark an image."""
251
274
  watermarked_image: Image.Image = original_image.copy()
@@ -268,23 +291,31 @@ def watermark_image(
268
291
  calc_padding(original_image, args),
269
292
  )
270
293
 
294
+ try:
295
+ font_fill_color = ImageColor.getrgb(args.font_color)
296
+ stroke_fill_color = ImageColor.getrgb(args.outline_color)
297
+ except ValueError:
298
+ log.error("Invalid font or outline color specified. Using defaults.")
299
+ font_fill_color = ImageColor.getrgb("white")
300
+ stroke_fill_color = ImageColor.getrgb("black")
301
+
271
302
  draw.text(
272
303
  (position_x, position_y),
273
304
  text,
274
305
  font=font,
275
- fill=(*ImageColor.getrgb(args.font_color), 128),
306
+ fill=(*font_fill_color, alpha),
276
307
  stroke_width=calc_font_outline_width(original_image, args),
277
- stroke_fill=(*ImageColor.getrgb(args.outline_color), 128),
308
+ stroke_fill=(*stroke_fill_color, alpha),
278
309
  )
279
310
  return watermarked_image
280
311
 
281
312
 
282
313
  def prepare_text(args: argparse.Namespace, image: Image.Image) -> str:
283
314
  """Prepare the watermark text."""
284
- text: str = args.text # Default text
315
+ text = args.text
285
316
  if args.camera:
286
- metadata_text: str | None = camera_metadata(image)
287
- if metadata_text: # Use metadata only if it's not None or empty
317
+ metadata_text = get_formatted_camera_info(image)
318
+ if metadata_text:
288
319
  text = metadata_text
289
320
  else:
290
321
  log.warning(
@@ -27,6 +27,17 @@ def fixture_csv_file(tmpdir):
27
27
  return csv_file
28
28
 
29
29
 
30
+ @pytest.fixture(autouse=True, name="image_file")
31
+ def fixture_image_file(tmpdir):
32
+ def image_file(filename):
33
+ src = Path(FIXTURE_PATH, filename)
34
+ des = Path(tmpdir, src.name)
35
+ copyfile(src, des)
36
+ return des
37
+
38
+ return image_file
39
+
40
+
30
41
  @pytest.fixture(autouse=True, name="cli_runner")
31
42
  def fixture_cli_runner(tmpdir):
32
43
  def cli_runner(*args, **kwargs):
@@ -0,0 +1,7 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_animate_subcommand(cli_runner, image_file):
4
+ img_path1 = image_file("sample.png")
5
+ img_path2 = image_file("sample.png")
6
+ ret = cli_runner('animate', str(img_path1), str(img_path2))
7
+ assert ret.returncode == 0
@@ -0,0 +1,7 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_auto_subcommand(cli_runner, image_file):
4
+ """Test auto subcommand."""
5
+ img_path = image_file("sample.png")
6
+ ret = cli_runner("auto", str(img_path))
7
+ assert ret.returncode == 0
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_border_subcommand(cli_runner, image_file):
4
+ img_path = image_file("sample.png")
5
+ ret = cli_runner('border', str(img_path), '--width', '10')
6
+ assert ret.returncode == 0
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_contrast_subcommand(cli_runner, image_file):
4
+ img_path = image_file("sample.png")
5
+ ret = cli_runner('contrast', str(img_path))
6
+ assert ret.returncode == 0
@@ -0,0 +1,23 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ import platform
4
+ import sys
5
+
6
+ from fotolab import __version__
7
+
8
+
9
+ def test_env_output(cli_runner):
10
+ ret = cli_runner("env")
11
+
12
+ actual_sys_version = sys.version.replace("\n", "")
13
+ actual_platform = platform.platform()
14
+
15
+ expected_output = (
16
+ f"fotolab: {__version__}\n"
17
+ f"python: {actual_sys_version}\n"
18
+ f"platform: {actual_platform}\n"
19
+ )
20
+
21
+ assert ret.stdout == expected_output
22
+ assert ret.stderr == ""
23
+ assert ret.returncode == 0
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_halftone_subcommand(cli_runner, image_file):
4
+ img_path = image_file("sample.png")
5
+ ret = cli_runner('halftone', str(img_path))
6
+ assert ret.returncode == 0
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_info_subcommand(cli_runner, image_file):
4
+ img_path = image_file("sample.png")
5
+ ret = cli_runner('info', str(img_path))
6
+ assert ret.returncode == 0
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_montage_subcommand(cli_runner, image_file):
4
+ img_path = image_file("sample.png")
5
+ ret = cli_runner('montage', str(img_path), str(img_path))
6
+ assert ret.returncode == 0
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_resize_subcommand(cli_runner, image_file):
4
+ img_path = image_file("sample.png")
5
+ ret = cli_runner('resize', str(img_path), '--width', '200')
6
+ assert ret.returncode == 0
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_rotate_subcommand(cli_runner, image_file):
4
+ img_path = image_file("sample.png")
5
+ ret = cli_runner('rotate', str(img_path))
6
+ assert ret.returncode == 0
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_sharpen_subcommand(cli_runner, image_file):
4
+ img_path = image_file("sample.png")
5
+ ret = cli_runner('sharpen', str(img_path))
6
+ assert ret.returncode == 0
@@ -0,0 +1,6 @@
1
+ # pylint: disable=C0114,C0116
2
+
3
+ def test_watermark_subcommand(cli_runner, image_file):
4
+ img_path = image_file("sample.png")
5
+ ret = cli_runner('watermark', str(img_path), '--text', 'Test')
6
+ assert ret.returncode == 0
@@ -1,10 +0,0 @@
1
- # pylint: disable=C0114,C0116
2
-
3
- from fotolab import __version__
4
-
5
-
6
- def test_env_output(cli_runner):
7
- ret = cli_runner("env")
8
- assert f"fotolab: {__version__}" in ret.stdout
9
- assert "python: " in ret.stdout
10
- assert "platform: " in ret.stdout
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes