fotolab 0.29.0__py3-none-any.whl → 0.29.1__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
@@ -24,7 +24,7 @@ from pathlib import Path
24
24
 
25
25
  from PIL import Image
26
26
 
27
- __version__ = "0.29.0"
27
+ __version__ = "0.29.1"
28
28
 
29
29
  log = logging.getLogger(__name__)
30
30
 
@@ -18,6 +18,7 @@
18
18
  import argparse
19
19
  import logging
20
20
  import math
21
+ from typing import NamedTuple
21
22
 
22
23
  from PIL import Image, ImageDraw
23
24
 
@@ -26,6 +27,14 @@ from fotolab import save_gif_image, save_image
26
27
  log = logging.getLogger(__name__)
27
28
 
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
+
29
38
  def build_subparser(subparsers) -> None:
30
39
  """Build the subparser."""
31
40
  halftone_parser = subparsers.add_parser(
@@ -117,6 +126,56 @@ def run(args: argparse.Namespace) -> None:
117
126
  save_image(args, halftone_image, image_filename, "halftone")
118
127
 
119
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
+
120
179
  def create_halftone_image(
121
180
  original_image: Image.Image, cell_count: int, grayscale: bool = False
122
181
  ) -> Image.Image:
@@ -142,6 +201,7 @@ def create_halftone_image(
142
201
  source_image = original_image.convert("RGB")
143
202
  output_mode = "RGB"
144
203
  fill_color_black = (0, 0, 0)
204
+ fill_color_dot = None # Will be set inside the loop for color images
145
205
 
146
206
  width, height = original_image.size
147
207
 
@@ -152,40 +212,18 @@ def create_halftone_image(
152
212
  cellsize = width / cell_count
153
213
  rowtotal = math.ceil(height / cellsize)
154
214
 
215
+ # Determine the fill color for dots once if grayscale
216
+ grayscale_fill_color_dot = fill_color_dot if grayscale else None
217
+
155
218
  for row in range(rowtotal):
156
219
  for col in range(cell_count):
157
- # Calculate center point of current cell
158
- x = int(col * cellsize + cellsize / 2)
159
- y = int(row * cellsize + cellsize / 2)
160
-
161
- # Get pixel value (brightness or color) from the source image
162
- pixel_value = source_image.getpixel((x, y))
163
-
164
- if grayscale:
165
- brightness = pixel_value
166
- dot_fill = fill_color_dot # Use white for grayscale dots
167
- else:
168
- # Calculate brightness (luminance) from the RGB color
169
- brightness = int(
170
- 0.299 * pixel_value[0]
171
- + 0.587 * pixel_value[1]
172
- + 0.114 * pixel_value[2]
173
- )
174
- dot_fill = pixel_value # Use original color for color dots
175
-
176
- # Calculate dot radius relative to cell size based on brightness
177
- # Max radius is half the cell size. Scale by brightness (0-255).
178
- dot_radius = (brightness / 255.0) * (cellsize / 2)
179
-
180
- # Draw the dot
181
- draw.ellipse(
182
- [
183
- x - dot_radius,
184
- y - dot_radius,
185
- x + dot_radius,
186
- y + dot_radius,
187
- ],
188
- fill=dot_fill,
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,
189
227
  )
190
228
 
191
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fotolab
3
- Version: 0.29.0
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,4 +1,4 @@
1
- fotolab/__init__.py,sha256=E76E5fEYHIL3PDbEllLqBALGLmzPYpgtdk4B1-ujU_g,3262
1
+ fotolab/__init__.py,sha256=KwMFpmP-DDvkiBIx6zr9XGWSONGzZo5Yr0aHKjYUmlg,3262
2
2
  fotolab/__main__.py,sha256=aboOURPs_snOXTEWYR0q8oq1UTY9e-NxCd1j33V0wHI,833
3
3
  fotolab/cli.py,sha256=NH_u73SJhzwDJSRqpi1I4dc8wBfcqbDxNne3cio163A,4411
4
4
  fotolab/subcommands/__init__.py,sha256=l3DlIaJ3u3jGjnC1H1yV8LZ_nPqOLJ6gikD4BCaMAQ0,1129
@@ -7,15 +7,15 @@ fotolab/subcommands/auto.py,sha256=ia-xegV1Z4HvYsbKgmTzf1NfNFdTDPWfZe7vQ1_90Ik,2
7
7
  fotolab/subcommands/border.py,sha256=BS3BHytdWiNumxdKulKYK-WigbsKtPxECdvInUhUjSQ,4608
8
8
  fotolab/subcommands/contrast.py,sha256=ZHTvAJhRYZjNTrkHRZq3O6wba3LS7QjH1BfiGf4ZvGY,3005
9
9
  fotolab/subcommands/env.py,sha256=QoxRvzZKgmoHTUxDV4QYhdChCpMWs5TbXFY_qIpIQpE,1469
10
- fotolab/subcommands/halftone.py,sha256=dtqmyZyGC8huzSGRCRgp8t11m51hFC15kvgfEJBm_0c,5714
11
- fotolab/subcommands/info.py,sha256=Qp-Zu7Xp1ptLK211hOkFM5oxZWGTrlNBqCpbx2IjL9Y,3371
10
+ fotolab/subcommands/halftone.py,sha256=YOGtdl6N3aC4E47ac1rGzHU0HczVsujKNIBMyiRf2-c,6681
11
+ fotolab/subcommands/info.py,sha256=EYuU06-rrnZZMY0XKwaIiqLypbd6x9KmCydiB1UhtTU,4010
12
12
  fotolab/subcommands/montage.py,sha256=d_3EcyRSFS8fKkczlHO8IRP-mrKhQUtkQndjfd0MKsc,2566
13
13
  fotolab/subcommands/resize.py,sha256=UOb2rg_5ArRj0gxPTDOww_ix3tqJRC0W5b_S-Lt1G4w,5444
14
14
  fotolab/subcommands/rotate.py,sha256=uBFjHyjiBSQLtrtH1p9myODIHUDr1gkL4PpU-6Y1Ofo,2575
15
15
  fotolab/subcommands/sharpen.py,sha256=YNho2IPbc-lPvSy3Bsjehc2JOEy27LPqFSGRULs9MyY,3492
16
- fotolab/subcommands/watermark.py,sha256=iEuek0BM2PJlGMLq8VZhHxaQ8BInuN2_mvssA7Dhgy8,10032
17
- fotolab-0.29.0.dist-info/entry_points.txt,sha256=mvw7AY_yZkIyjAxPtHNed9X99NZeLnMxEeAfEJUbrCM,44
18
- fotolab-0.29.0.dist-info/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
19
- fotolab-0.29.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
20
- fotolab-0.29.0.dist-info/METADATA,sha256=h3twSy_UW9ri5GZ0_Xyh0s2IYH6AdVCAll-TuKYc-Xg,12970
21
- fotolab-0.29.0.dist-info/RECORD,,
16
+ fotolab/subcommands/watermark.py,sha256=qRGUp1Lc22fZSJDFRqQGPiz8RSB293ebvOVTdsDLUE4,10351
17
+ fotolab-0.29.1.dist-info/entry_points.txt,sha256=mvw7AY_yZkIyjAxPtHNed9X99NZeLnMxEeAfEJUbrCM,44
18
+ fotolab-0.29.1.dist-info/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
19
+ fotolab-0.29.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
20
+ fotolab-0.29.1.dist-info/METADATA,sha256=0jUgjFRshumTtyFXLKe6_sQVnpyGLNez-Fl-KB5vHMk,12980
21
+ fotolab-0.29.1.dist-info/RECORD,,