fotolab 0.26.2__tar.gz → 0.26.3__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 (43) hide show
  1. {fotolab-0.26.2 → fotolab-0.26.3}/.pre-commit-config.yaml +1 -1
  2. {fotolab-0.26.2 → fotolab-0.26.3}/CHANGELOG.md +6 -0
  3. {fotolab-0.26.2 → fotolab-0.26.3}/PKG-INFO +1 -1
  4. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/__init__.py +1 -1
  5. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/border.py +17 -7
  6. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/halftone.py +45 -28
  7. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/watermark.py +22 -46
  8. {fotolab-0.26.2 → fotolab-0.26.3}/.coveragerc +0 -0
  9. {fotolab-0.26.2 → fotolab-0.26.3}/.gitignore +0 -0
  10. {fotolab-0.26.2 → fotolab-0.26.3}/.python-version +0 -0
  11. {fotolab-0.26.2 → fotolab-0.26.3}/CONTRIBUTING.md +0 -0
  12. {fotolab-0.26.2 → fotolab-0.26.3}/LICENSE.md +0 -0
  13. {fotolab-0.26.2 → fotolab-0.26.3}/Pipfile +0 -0
  14. {fotolab-0.26.2 → fotolab-0.26.3}/Pipfile.lock +0 -0
  15. {fotolab-0.26.2 → fotolab-0.26.3}/README.md +0 -0
  16. {fotolab-0.26.2 → fotolab-0.26.3}/docs/Makefile +0 -0
  17. {fotolab-0.26.2 → fotolab-0.26.3}/docs/make.bat +0 -0
  18. {fotolab-0.26.2 → fotolab-0.26.3}/docs/source/CHANGELOG.md +0 -0
  19. {fotolab-0.26.2 → fotolab-0.26.3}/docs/source/CONTRIBUTING.md +0 -0
  20. {fotolab-0.26.2 → fotolab-0.26.3}/docs/source/LICENSE.md +0 -0
  21. {fotolab-0.26.2 → fotolab-0.26.3}/docs/source/README.md +0 -0
  22. {fotolab-0.26.2 → fotolab-0.26.3}/docs/source/_static/logo.jpg +0 -0
  23. {fotolab-0.26.2 → fotolab-0.26.3}/docs/source/conf.py +0 -0
  24. {fotolab-0.26.2 → fotolab-0.26.3}/docs/source/index.rst +0 -0
  25. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/__main__.py +0 -0
  26. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/cli.py +0 -0
  27. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/__init__.py +0 -0
  28. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/animate.py +0 -0
  29. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/auto.py +0 -0
  30. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/contrast.py +0 -0
  31. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/env.py +0 -0
  32. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/info.py +0 -0
  33. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/montage.py +0 -0
  34. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/resize.py +0 -0
  35. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/rotate.py +0 -0
  36. {fotolab-0.26.2 → fotolab-0.26.3}/fotolab/subcommands/sharpen.py +0 -0
  37. {fotolab-0.26.2 → fotolab-0.26.3}/noxfile.py +0 -0
  38. {fotolab-0.26.2 → fotolab-0.26.3}/pyproject.toml +0 -0
  39. {fotolab-0.26.2 → fotolab-0.26.3}/tests/__init__.py +0 -0
  40. {fotolab-0.26.2 → fotolab-0.26.3}/tests/conftest.py +0 -0
  41. {fotolab-0.26.2 → fotolab-0.26.3}/tests/test_env_subcommand.py +0 -0
  42. {fotolab-0.26.2 → fotolab-0.26.3}/tests/test_help_flag.py +0 -0
  43. {fotolab-0.26.2 → fotolab-0.26.3}/tests/test_quiet_flag.py +0 -0
@@ -107,7 +107,7 @@ repos:
107
107
  - --disable=R0801,W0212
108
108
 
109
109
  - repo: https://github.com/pre-commit/mirrors-mypy
110
- rev: v1.14.1
110
+ rev: v1.15.0
111
111
  hooks:
112
112
  - id: mypy
113
113
  exclude: docs/
@@ -7,6 +7,12 @@ and this project adheres to [0-based versioning](https://0ver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.26.3 (2025-02-09)
11
+
12
+ - Bump `pre-commit` hooks
13
+ - Refactor text generation or saving image for `watermark` subcommand
14
+ - Refactor `halftone` and `border` subcommand
15
+
10
16
  ## v0.26.2 (2025-02-02)
11
17
 
12
18
  - Add missing changelog item
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fotolab
3
- Version: 0.26.2
3
+ Version: 0.26.3
4
4
  Summary: A console program that manipulate images.
5
5
  Keywords: photography,photo
6
6
  Author-email: Kian-Meng Ang <kianmeng@cpan.org>
@@ -20,7 +20,7 @@ import subprocess
20
20
  import sys
21
21
  from pathlib import Path
22
22
 
23
- __version__ = "0.26.2"
23
+ __version__ = "0.26.3"
24
24
 
25
25
  log = logging.getLogger(__name__)
26
26
 
@@ -125,13 +125,23 @@ def run(args: argparse.Namespace) -> None:
125
125
  save_image(args, bordered_image, image_filename, "border")
126
126
 
127
127
 
128
- def get_border(args: argparse.Namespace) -> tuple:
129
- """Calculate the border."""
130
- if (
131
- args.width_left
132
- or args.width_top
133
- or args.width_right
134
- or args.width_bottom
128
+ def get_border(args: argparse.Namespace) -> tuple[int, int, int, int]:
129
+ """Calculate the border dimensions.
130
+
131
+ Args:
132
+ args (argparse.Namespace): Command line arguments
133
+
134
+ Returns:
135
+ tuple[int, int, int, int]: Border dimensions in pixesl. If individual
136
+ widths are not specified, returns a uniform width for all sides.
137
+ """
138
+ if any(
139
+ [
140
+ args.width_left,
141
+ args.width_top,
142
+ args.width_right,
143
+ args.width_bottom,
144
+ ]
135
145
  ):
136
146
  return (
137
147
  int(args.width_left),
@@ -67,45 +67,62 @@ def run(args: argparse.Namespace) -> None:
67
67
 
68
68
  for image_filename in args.image_filenames:
69
69
  original_image = Image.open(image_filename)
70
- grayscale_image = original_image.convert("L")
71
- width, height = original_image.size
70
+ halftone_image = create_halftone_image(original_image)
71
+
72
+ if args.before_after:
73
+ save_gif_image(
74
+ args, image_filename, original_image, halftone_image
75
+ )
76
+ else:
77
+ save_image(args, halftone_image, image_filename, "halftone")
72
78
 
73
- halftone_image = Image.new("L", (width, height), "black")
74
- draw = ImageDraw.Draw(halftone_image)
75
79
 
76
- # modified from the circular halftone effect processing.py example from
77
- # https://tabreturn.github.io/code/processing/python/2019/02/09/processing.py_in_ten_lessons-6.3-_halftones.html
78
- coltotal = 50
79
- cellsize = width / coltotal
80
- rowtotal = math.ceil(height / cellsize)
80
+ def create_halftone_image(
81
+ original_image: Image.Image, cell_count: int = 50
82
+ ) -> Image.Image:
83
+ """Create a halftone version of the input image.
81
84
 
82
- col = 0
83
- row = 0
85
+ Modified from the circular halftone effect processing.py example from
86
+ https://tabreturn.github.io/code/processing/python/2019/02/09/processing.py_in_ten_lessons-6.3-_halftones.html
84
87
 
85
- for _ in range(int(coltotal * rowtotal)):
86
- x = int(col * cellsize)
87
- y = int(row * cellsize)
88
- col += 1
88
+ Args:
89
+ original_image: The source image to convert
90
+ cell_count: Number of cells across the width (default: 50)
89
91
 
90
- if col >= coltotal:
91
- col = 0
92
- row += 1
92
+ Returns:
93
+ Image.Image: The halftone converted image
94
+ """
95
+ grayscale_image = original_image.convert("L")
96
+ width, height = original_image.size
93
97
 
94
- x = int(x + cellsize / 2)
95
- y = int(y + cellsize / 2)
98
+ halftone_image = Image.new("L", (width, height), "black")
99
+ draw = ImageDraw.Draw(halftone_image)
96
100
 
101
+ cellsize = width / cell_count
102
+ rowtotal = math.ceil(height / cellsize)
103
+
104
+ for row in range(rowtotal):
105
+ for col in range(cell_count):
106
+ # Calculate center point of current cell
107
+ x = int(col * cellsize + cellsize / 2)
108
+ y = int(row * cellsize + cellsize / 2)
109
+
110
+ # Get brightness and calculate dot size
97
111
  brightness = grayscale_image.getpixel((x, y))
98
- amp = 10 * brightness / 200
112
+ dot_size = 10 * brightness / 200
113
+
114
+ # Draw the dot
99
115
  draw.ellipse(
100
- [x - amp / 2, y - amp / 2, x + amp / 2, y + amp / 2], fill=255
116
+ [
117
+ x - dot_size / 2,
118
+ y - dot_size / 2,
119
+ x + dot_size / 2,
120
+ y + dot_size / 2,
121
+ ],
122
+ fill=255,
101
123
  )
102
124
 
103
- if args.before_after:
104
- save_gif_image(
105
- args, image_filename, original_image, halftone_image
106
- )
107
- else:
108
- save_image(args, halftone_image, image_filename, "halftone")
125
+ return halftone_image
109
126
 
110
127
 
111
128
  def save_gif_image(args, image_filename, original_image, sharpen_image):
@@ -167,46 +167,10 @@ def run(args: argparse.Namespace) -> None:
167
167
 
168
168
  def watermark_gif_image(original_image: Image.Image, args: argparse.Namespace):
169
169
  """Watermark the image."""
170
- watermarked_image = original_image.copy()
171
-
172
170
  frames = []
173
-
174
171
  for frame in ImageSequence.Iterator(original_image):
175
- frame = frame.convert("RGBA")
176
- draw = ImageDraw.Draw(frame)
177
-
178
- font = ImageFont.load_default(calc_font_size(original_image, args))
179
- log.debug("default font: %s", " ".join(font.getname()))
180
-
181
- text = args.text
182
- if args.camera and camera_metadata(original_image):
183
- text = camera_metadata(original_image)
184
-
185
- if args.lowercase:
186
- text = text.lower()
187
-
188
- (left, top, right, bottom) = draw.textbbox(
189
- xy=(0, 0), text=text, font=font
190
- )
191
- text_width = right - left
192
- text_height = bottom - top
193
- (position_x, position_y) = calc_position(
194
- watermarked_image,
195
- text_width,
196
- text_height,
197
- args.position,
198
- calc_padding(original_image, args),
199
- )
200
-
201
- draw.text(
202
- (position_x, position_y),
203
- text,
204
- font=font,
205
- fill=(*ImageColor.getrgb(args.font_color), 128),
206
- stroke_width=calc_font_outline_width(original_image, args),
207
- stroke_fill=(*ImageColor.getrgb(args.outline_color), 128),
208
- )
209
- frames.append(frame)
172
+ watermarked_frame = watermark_image(args, frame.convert("RGBA"))
173
+ frames.append(watermarked_frame)
210
174
 
211
175
  frames[0].save(
212
176
  "foo.gif",
@@ -223,20 +187,20 @@ def watermark_non_gif_image(
223
187
  original_image: Image.Image, args: argparse.Namespace
224
188
  ):
225
189
  """Watermark the image."""
226
- watermarked_image = original_image.copy()
190
+ return watermark_image(args, original_image)
191
+
227
192
 
193
+ def watermark_image(
194
+ args: argparse.Namespace, original_image: Image.Image
195
+ ) -> Image.Image:
196
+ """Watermark an image."""
197
+ watermarked_image = original_image.copy()
228
198
  draw = ImageDraw.Draw(watermarked_image)
229
199
 
230
200
  font = ImageFont.load_default(calc_font_size(original_image, args))
231
201
  log.debug("default font: %s", " ".join(font.getname()))
232
202
 
233
- text = args.text
234
- if args.camera and camera_metadata(original_image):
235
- text = camera_metadata(original_image)
236
-
237
- if args.lowercase:
238
- text = text.lower()
239
-
203
+ text = prepare_text(args, original_image)
240
204
  (left, top, right, bottom) = draw.textbbox(xy=(0, 0), text=text, font=font)
241
205
  text_width = right - left
242
206
  text_height = bottom - top
@@ -259,6 +223,18 @@ def watermark_non_gif_image(
259
223
  return watermarked_image
260
224
 
261
225
 
226
+ def prepare_text(args, image) -> str:
227
+ """Prepare the watermark text."""
228
+ text = args.text
229
+ if args.camera and camera_metadata(image):
230
+ text = camera_metadata(image)
231
+
232
+ if args.lowercase:
233
+ text = text.lower()
234
+
235
+ return text
236
+
237
+
262
238
  def calc_font_size(image, args) -> int:
263
239
  """Calculate the font size based on the width of the image."""
264
240
  width, _height = image.size
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
File without changes
File without changes