fotolab 0.28.6__tar.gz → 0.29.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 (45) hide show
  1. {fotolab-0.28.6 → fotolab-0.29.1}/CHANGELOG.md +17 -0
  2. {fotolab-0.28.6 → fotolab-0.29.1}/PKG-INFO +4 -4
  3. {fotolab-0.28.6 → fotolab-0.29.1}/Pipfile.lock +1 -1
  4. {fotolab-0.28.6 → fotolab-0.29.1}/README.md +3 -3
  5. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/__init__.py +1 -1
  6. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/cli.py +15 -6
  7. fotolab-0.29.1/fotolab/subcommands/halftone.py +229 -0
  8. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/info.py +35 -17
  9. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/watermark.py +11 -3
  10. fotolab-0.28.6/fotolab/subcommands/halftone.py +0 -145
  11. {fotolab-0.28.6 → fotolab-0.29.1}/.coveragerc +0 -0
  12. {fotolab-0.28.6 → fotolab-0.29.1}/.gitignore +0 -0
  13. {fotolab-0.28.6 → fotolab-0.29.1}/.pre-commit-config.yaml +0 -0
  14. {fotolab-0.28.6 → fotolab-0.29.1}/.python-version +0 -0
  15. {fotolab-0.28.6 → fotolab-0.29.1}/CONTRIBUTING.md +0 -0
  16. {fotolab-0.28.6 → fotolab-0.29.1}/LICENSE.md +0 -0
  17. {fotolab-0.28.6 → fotolab-0.29.1}/Pipfile +0 -0
  18. {fotolab-0.28.6 → fotolab-0.29.1}/docs/Makefile +0 -0
  19. {fotolab-0.28.6 → fotolab-0.29.1}/docs/make.bat +0 -0
  20. {fotolab-0.28.6 → fotolab-0.29.1}/docs/source/CHANGELOG.md +0 -0
  21. {fotolab-0.28.6 → fotolab-0.29.1}/docs/source/CONTRIBUTING.md +0 -0
  22. {fotolab-0.28.6 → fotolab-0.29.1}/docs/source/LICENSE.md +0 -0
  23. {fotolab-0.28.6 → fotolab-0.29.1}/docs/source/README.md +0 -0
  24. {fotolab-0.28.6 → fotolab-0.29.1}/docs/source/_static/logo.jpg +0 -0
  25. {fotolab-0.28.6 → fotolab-0.29.1}/docs/source/conf.py +0 -0
  26. {fotolab-0.28.6 → fotolab-0.29.1}/docs/source/index.rst +0 -0
  27. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/__main__.py +0 -0
  28. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/__init__.py +0 -0
  29. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/animate.py +0 -0
  30. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/auto.py +0 -0
  31. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/border.py +0 -0
  32. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/contrast.py +0 -0
  33. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/env.py +0 -0
  34. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/montage.py +0 -0
  35. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/resize.py +0 -0
  36. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/rotate.py +0 -0
  37. {fotolab-0.28.6 → fotolab-0.29.1}/fotolab/subcommands/sharpen.py +0 -0
  38. {fotolab-0.28.6 → fotolab-0.29.1}/generate +0 -0
  39. {fotolab-0.28.6 → fotolab-0.29.1}/noxfile.py +0 -0
  40. {fotolab-0.28.6 → fotolab-0.29.1}/pyproject.toml +0 -0
  41. {fotolab-0.28.6 → fotolab-0.29.1}/tests/__init__.py +0 -0
  42. {fotolab-0.28.6 → fotolab-0.29.1}/tests/conftest.py +0 -0
  43. {fotolab-0.28.6 → fotolab-0.29.1}/tests/test_env_subcommand.py +0 -0
  44. {fotolab-0.28.6 → fotolab-0.29.1}/tests/test_help_flag.py +0 -0
  45. {fotolab-0.28.6 → fotolab-0.29.1}/tests/test_quiet_flag.py +0 -0
@@ -7,6 +7,23 @@ and this project adheres to [0-based versioning](https://0ver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.29.1 (2025-05-04)
11
+
12
+ - Improve camera metadata handling in watermark subcommand.
13
+ - Refactor halftone dot drawing into a separate function for clarity
14
+ - Refactor halftone dot drawing to use NamedTuple for cell data
15
+ - Refactor info subcommand to handle specific metadata requests.
16
+ - Update help message in readme
17
+
18
+ ## v0.29.0 (2025-04-27)
19
+
20
+ - Add `cells` argument to halftone subcommand and function
21
+ - Add grayscale option to halftone subcommand for grayscale conversion
22
+ - Bump deps
23
+ - Calculate dot radius relative to cell size based on brightness.
24
+ - Refactor CLI to handle missing subcommand execution function gracefully
25
+ - Require a subcommand to be specified and remove redundant checks
26
+
10
27
  ## v0.28.6 (2025-04-20)
11
28
 
12
29
  - Add missing typehints for `watermark` subcommand
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fotolab
3
- Version: 0.28.6
3
+ Version: 0.29.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>
@@ -75,7 +75,7 @@ positional arguments:
75
75
  animate animate an image
76
76
  auto auto adjust (resize, contrast, and watermark) a photo
77
77
  border add border to image
78
- contrast contrast an image
78
+ contrast contrast an image.
79
79
  env print environment information for bug reporting
80
80
  halftone halftone an image
81
81
  info info an image
@@ -200,8 +200,8 @@ positional arguments:
200
200
 
201
201
  options:
202
202
  -h, --help show this help message and exit
203
- -c, --cutoff CUTOFF set the percentage of lightest or darkest pixels to
204
- discard from histogram (default: '1')
203
+ -c, --cutoff CUTOFF set the percentage (0-50) of lightest or darkest
204
+ pixels to discard from histogram (default: '1.0')
205
205
  -op, --open open the image using default program (default:
206
206
  'False')
207
207
  -od, --output-dir OUTPUT_DIR
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_meta": {
3
3
  "hash": {
4
- "sha256": "0ae04620e58fea48a693d99901d3c9f3a1992033a0a3b8eac3306e6ca34f65bb"
4
+ "sha256": "52a8d63c6f19800a1bd1586e2773e76ce235140a359f64eb68e0dbb72a2dd138"
5
5
  },
6
6
  "pipfile-spec": 6,
7
7
  "requires": {
@@ -52,7 +52,7 @@ positional arguments:
52
52
  animate animate an image
53
53
  auto auto adjust (resize, contrast, and watermark) a photo
54
54
  border add border to image
55
- contrast contrast an image
55
+ contrast contrast an image.
56
56
  env print environment information for bug reporting
57
57
  halftone halftone an image
58
58
  info info an image
@@ -177,8 +177,8 @@ positional arguments:
177
177
 
178
178
  options:
179
179
  -h, --help show this help message and exit
180
- -c, --cutoff CUTOFF set the percentage of lightest or darkest pixels to
181
- discard from histogram (default: '1')
180
+ -c, --cutoff CUTOFF set the percentage (0-50) of lightest or darkest
181
+ pixels to discard from histogram (default: '1.0')
182
182
  -op, --open open the image using default program (default:
183
183
  'False')
184
184
  -od, --output-dir OUTPUT_DIR
@@ -24,7 +24,7 @@ from pathlib import Path
24
24
 
25
25
  from PIL import Image
26
26
 
27
- __version__ = "0.28.6"
27
+ __version__ = "0.29.1"
28
28
 
29
29
  log = logging.getLogger(__name__)
30
30
 
@@ -115,6 +115,7 @@ def build_parser() -> argparse.ArgumentParser:
115
115
  subparsers = parser.add_subparsers(
116
116
  help="sub-command help",
117
117
  dest="command",
118
+ required=True,
118
119
  )
119
120
  fotolab.subcommands.build_subparser(subparsers)
120
121
 
@@ -128,16 +129,24 @@ def main(args: Optional[Sequence[str]] = None) -> None:
128
129
 
129
130
  try:
130
131
  parser = build_parser()
131
- if len(args) == 0:
132
- parser.print_help(sys.stderr)
133
- return
134
-
135
132
  parsed_args = parser.parse_args(args)
136
133
  setup_logging(parsed_args)
137
134
 
138
- if hasattr(parsed_args, "func"):
135
+ if parsed_args.command is not None:
139
136
  log.debug(parsed_args)
140
- parsed_args.func(parsed_args)
137
+ # Ensure the function attribute exists (set by set_defaults in
138
+ # subcommands)
139
+ if hasattr(parsed_args, "func"):
140
+ parsed_args.func(parsed_args)
141
+ else:
142
+ # This case should ideally not happen if subcommands are set up
143
+ # correctly
144
+ log.error(
145
+ "subcommand '%s' is missing its execution function.",
146
+ parsed_args.command
147
+ )
148
+ parser.print_help(sys.stderr)
149
+ raise SystemExit(1)
141
150
  else:
142
151
  parser.print_help(sys.stderr)
143
152
 
@@ -0,0 +1,229 @@
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
+ """Halftone subcommand."""
17
+
18
+ import argparse
19
+ import logging
20
+ import math
21
+ from typing import NamedTuple
22
+
23
+ from PIL import Image, ImageDraw
24
+
25
+ from fotolab import save_gif_image, save_image
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+
30
+ class HalftoneCell(NamedTuple):
31
+ """Represents a cell in the halftone grid."""
32
+
33
+ col: int
34
+ row: int
35
+ cellsize: float
36
+
37
+
38
+ def build_subparser(subparsers) -> None:
39
+ """Build the subparser."""
40
+ halftone_parser = subparsers.add_parser(
41
+ "halftone", help="halftone an image"
42
+ )
43
+
44
+ halftone_parser.set_defaults(func=run)
45
+
46
+ halftone_parser.add_argument(
47
+ dest="image_filenames",
48
+ help="set the image filename",
49
+ nargs="+",
50
+ type=str,
51
+ default=None,
52
+ metavar="IMAGE_FILENAMES",
53
+ )
54
+
55
+ halftone_parser.add_argument(
56
+ "-ba",
57
+ "--before-after",
58
+ default=False,
59
+ action="store_true",
60
+ dest="before_after",
61
+ help="generate a GIF showing before and after changes",
62
+ )
63
+
64
+ halftone_parser.add_argument(
65
+ "-op",
66
+ "--open",
67
+ default=False,
68
+ action="store_true",
69
+ dest="open",
70
+ help="open the image using default program (default: '%(default)s')",
71
+ )
72
+
73
+ halftone_parser.add_argument(
74
+ "-od",
75
+ "--output-dir",
76
+ dest="output_dir",
77
+ default="output",
78
+ help="set default output folder (default: '%(default)s')",
79
+ )
80
+
81
+ halftone_parser.add_argument(
82
+ "-c",
83
+ "--cells",
84
+ dest="cells",
85
+ type=int,
86
+ default=50,
87
+ help="set number of cells across the image width (default: %(default)s)",
88
+ )
89
+
90
+ halftone_parser.add_argument(
91
+ "-g",
92
+ "--grayscale",
93
+ default=False,
94
+ action="store_true",
95
+ dest="grayscale",
96
+ help="convert image to grayscale before applying halftone",
97
+ )
98
+
99
+
100
+ def run(args: argparse.Namespace) -> None:
101
+ """Run halftone subcommand.
102
+
103
+ Args:
104
+ args (argparse.Namespace): Config from command line arguments
105
+
106
+ Returns:
107
+ None
108
+ """
109
+ log.debug(args)
110
+
111
+ for image_filename in args.image_filenames:
112
+ original_image = Image.open(image_filename)
113
+ halftone_image = create_halftone_image(
114
+ original_image, args.cells, args.grayscale
115
+ )
116
+
117
+ if args.before_after:
118
+ save_gif_image(
119
+ args,
120
+ image_filename,
121
+ original_image,
122
+ halftone_image,
123
+ "halftone",
124
+ )
125
+ else:
126
+ save_image(args, halftone_image, image_filename, "halftone")
127
+
128
+
129
+ def _draw_halftone_dot(
130
+ draw: ImageDraw.ImageDraw,
131
+ source_image: Image.Image,
132
+ cell: HalftoneCell,
133
+ grayscale: bool,
134
+ fill_color_dot,
135
+ ) -> None:
136
+ """Calculate properties and draw a single halftone dot."""
137
+ # Calculate center point of current cell
138
+ img_width, img_height = source_image.size
139
+
140
+ # Calculate center point of current cell and clamp to image bounds
141
+ x = min(int(cell.col * cell.cellsize + cell.cellsize / 2), img_width - 1)
142
+ y = min(int(cell.row * cell.cellsize + cell.cellsize / 2), img_height - 1)
143
+
144
+ # Ensure coordinates are non-negative (shouldn't happen with current logic, but safe)
145
+ x = max(0, x)
146
+ y = max(0, y)
147
+
148
+ # Get pixel value (brightness or color) from the source image using clamped coordinates
149
+ pixel_value = source_image.getpixel((x, y))
150
+
151
+ if grayscale:
152
+ brightness = pixel_value
153
+ dot_fill = fill_color_dot # Use white for grayscale dots
154
+ else:
155
+ # Calculate brightness (luminance) from the RGB color
156
+ brightness = int(
157
+ 0.299 * pixel_value[0]
158
+ + 0.587 * pixel_value[1]
159
+ + 0.114 * pixel_value[2]
160
+ )
161
+ dot_fill = pixel_value # Use original color for color dots
162
+
163
+ # Calculate dot radius relative to cell size based on brightness
164
+ # Max radius is half the cell size. Scale by brightness (0-255).
165
+ dot_radius = (brightness / 255.0) * (cell.cellsize / 2)
166
+
167
+ # Draw the dot
168
+ draw.ellipse(
169
+ [
170
+ x - dot_radius,
171
+ y - dot_radius,
172
+ x + dot_radius,
173
+ y + dot_radius,
174
+ ],
175
+ fill=dot_fill,
176
+ )
177
+
178
+
179
+ def create_halftone_image(
180
+ original_image: Image.Image, cell_count: int, grayscale: bool = False
181
+ ) -> Image.Image:
182
+ """Create a halftone version of the input image.
183
+
184
+ Modified from the circular halftone effect processing.py example from
185
+ https://tabreturn.github.io/code/processing/python/2019/02/09/processing.py_in_ten_lessons-6.3-_halftones.html
186
+
187
+ Args:
188
+ original_image: The source image to convert
189
+ cell_count: Number of cells across the width
190
+ grayscale: Whether to convert to grayscale first (default: False)
191
+
192
+ Returns:
193
+ Image.Image: The halftone converted image
194
+ """
195
+ if grayscale:
196
+ source_image = original_image.convert("L")
197
+ output_mode = "L"
198
+ fill_color_black = 0
199
+ fill_color_dot = 255
200
+ else:
201
+ source_image = original_image.convert("RGB")
202
+ output_mode = "RGB"
203
+ fill_color_black = (0, 0, 0)
204
+ fill_color_dot = None # Will be set inside the loop for color images
205
+
206
+ width, height = original_image.size
207
+
208
+ # Create a new image for the output, initialized to black
209
+ halftone_image = Image.new(output_mode, (width, height), fill_color_black)
210
+ draw = ImageDraw.Draw(halftone_image)
211
+
212
+ cellsize = width / cell_count
213
+ rowtotal = math.ceil(height / cellsize)
214
+
215
+ # Determine the fill color for dots once if grayscale
216
+ grayscale_fill_color_dot = fill_color_dot if grayscale else None
217
+
218
+ for row in range(rowtotal):
219
+ for col in range(cell_count):
220
+ cell = HalftoneCell(col=col, row=row, cellsize=cellsize)
221
+ _draw_halftone_dot(
222
+ draw,
223
+ source_image,
224
+ cell,
225
+ grayscale,
226
+ grayscale_fill_color_dot,
227
+ )
228
+
229
+ return halftone_image
@@ -74,25 +74,41 @@ def run(args: argparse.Namespace) -> None:
74
74
  """
75
75
  log.debug(args)
76
76
 
77
- info = []
77
+ # TODO: Add error handling for file open
78
+ # TODO: Use context manager `with Image.open(...)`
78
79
  image = Image.open(args.image_filename)
79
80
 
81
+ exif_tags = extract_exif_tags(image, args.sort)
82
+
83
+ if not exif_tags:
84
+ print("No metadata found!")
85
+ # Close the image if opened outside a 'with' block
86
+ image.close()
87
+ return
88
+
89
+ output_info = []
90
+ specific_info_requested = False
91
+
80
92
  if args.camera:
81
- info.append(camera_metadata(image))
93
+ specific_info_requested = True
94
+ # TODO: Add error handling for missing keys
95
+ output_info.append(camera_metadata(exif_tags))
82
96
 
83
97
  if args.datetime:
84
- info.append(datetime(image))
98
+ specific_info_requested = True
99
+ # TODO: Add error handling for missing keys
100
+ output_info.append(datetime(exif_tags))
85
101
 
86
- if info:
87
- print("\n".join(info))
102
+ if specific_info_requested:
103
+ print("\n".join(output_info))
88
104
  else:
89
- exif_tags = extract_exif_tags(image)
90
- if exif_tags:
91
- tag_name_width = max(map(len, exif_tags))
92
- for tag_name, tag_value in exif_tags.items():
93
- print(f"{tag_name:<{tag_name_width}}: {tag_value}")
94
- else:
95
- print("No metadata found!")
105
+ # Print all tags if no specific info was requested
106
+ tag_name_width = max(map(len, exif_tags))
107
+ for tag_name, tag_value in exif_tags.items():
108
+ print(f"{tag_name:<{tag_name_width}}: {tag_value}")
109
+
110
+ # Close the image if opened outside a 'with' block
111
+ image.close()
96
112
 
97
113
 
98
114
  def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
@@ -117,14 +133,16 @@ def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
117
133
  return filtered_info
118
134
 
119
135
 
120
- def datetime(image: Image.Image):
136
+ def datetime(exif_tags: dict):
121
137
  """Extract datetime metadata."""
122
- exif_tags = extract_exif_tags(image)
138
+ # TODO: Add error handling for missing key
123
139
  return exif_tags["DateTime"]
124
140
 
125
141
 
126
- def camera_metadata(image: Image.Image):
142
+ def camera_metadata(exif_tags: dict):
127
143
  """Extract camera and model metadata."""
128
- exif_tags = extract_exif_tags(image)
129
- metadata = f'{exif_tags["Make"]} {exif_tags["Model"]}'
144
+ # TODO: Add error handling for missing keys
145
+ make = exif_tags.get("Make", "")
146
+ model = exif_tags.get("Model", "")
147
+ metadata = f"{make} {model}"
130
148
  return metadata.strip()
@@ -281,9 +281,17 @@ def watermark_image(
281
281
 
282
282
  def prepare_text(args: argparse.Namespace, image: Image.Image) -> str:
283
283
  """Prepare the watermark text."""
284
- text: str = args.text
285
- if args.camera and camera_metadata(image):
286
- text = camera_metadata(image)
284
+ text: str = args.text # Default text
285
+ 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
288
+ text = metadata_text
289
+ else:
290
+ log.warning(
291
+ "Camera metadata requested but not found or empty; "
292
+ "falling back to default text: '%s'",
293
+ args.text,
294
+ )
287
295
 
288
296
  if args.lowercase:
289
297
  text = text.lower()
@@ -1,145 +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
- """Halftone subcommand."""
17
-
18
- import argparse
19
- import logging
20
- import math
21
-
22
- from PIL import Image, ImageDraw
23
-
24
- from fotolab import save_gif_image, save_image
25
-
26
- log = logging.getLogger(__name__)
27
-
28
-
29
- def build_subparser(subparsers) -> None:
30
- """Build the subparser."""
31
- halftone_parser = subparsers.add_parser(
32
- "halftone", help="halftone an image"
33
- )
34
-
35
- halftone_parser.set_defaults(func=run)
36
-
37
- halftone_parser.add_argument(
38
- dest="image_filenames",
39
- help="set the image filename",
40
- nargs="+",
41
- type=str,
42
- default=None,
43
- metavar="IMAGE_FILENAMES",
44
- )
45
-
46
- halftone_parser.add_argument(
47
- "-ba",
48
- "--before-after",
49
- default=False,
50
- action="store_true",
51
- dest="before_after",
52
- help="generate a GIF showing before and after changes",
53
- )
54
-
55
- halftone_parser.add_argument(
56
- "-op",
57
- "--open",
58
- default=False,
59
- action="store_true",
60
- dest="open",
61
- help="open the image using default program (default: '%(default)s')",
62
- )
63
-
64
- halftone_parser.add_argument(
65
- "-od",
66
- "--output-dir",
67
- dest="output_dir",
68
- default="output",
69
- help="set default output folder (default: '%(default)s')",
70
- )
71
-
72
-
73
- def run(args: argparse.Namespace) -> None:
74
- """Run halftone subcommand.
75
-
76
- Args:
77
- args (argparse.Namespace): Config from command line arguments
78
-
79
- Returns:
80
- None
81
- """
82
- log.debug(args)
83
-
84
- for image_filename in args.image_filenames:
85
- original_image = Image.open(image_filename)
86
- halftone_image = create_halftone_image(original_image)
87
-
88
- if args.before_after:
89
- save_gif_image(
90
- args,
91
- image_filename,
92
- original_image,
93
- halftone_image,
94
- "halftone",
95
- )
96
- else:
97
- save_image(args, halftone_image, image_filename, "halftone")
98
-
99
-
100
- def create_halftone_image(
101
- original_image: Image.Image, cell_count: int = 50
102
- ) -> Image.Image:
103
- """Create a halftone version of the input image.
104
-
105
- Modified from the circular halftone effect processing.py example from
106
- https://tabreturn.github.io/code/processing/python/2019/02/09/processing.py_in_ten_lessons-6.3-_halftones.html
107
-
108
- Args:
109
- original_image: The source image to convert
110
- cell_count: Number of cells across the width (default: 50)
111
-
112
- Returns:
113
- Image.Image: The halftone converted image
114
- """
115
- grayscale_image = original_image.convert("L")
116
- width, height = original_image.size
117
-
118
- halftone_image = Image.new("L", (width, height), "black")
119
- draw = ImageDraw.Draw(halftone_image)
120
-
121
- cellsize = width / cell_count
122
- rowtotal = math.ceil(height / cellsize)
123
-
124
- for row in range(rowtotal):
125
- for col in range(cell_count):
126
- # Calculate center point of current cell
127
- x = int(col * cellsize + cellsize / 2)
128
- y = int(row * cellsize + cellsize / 2)
129
-
130
- # Get brightness and calculate dot size
131
- brightness = grayscale_image.getpixel((x, y))
132
- dot_size = 10 * brightness / 200
133
-
134
- # Draw the dot
135
- draw.ellipse(
136
- [
137
- x - dot_size / 2,
138
- y - dot_size / 2,
139
- x + dot_size / 2,
140
- y + dot_size / 2,
141
- ],
142
- fill=255,
143
- )
144
-
145
- return halftone_image
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