fotolab 0.29.0__py3-none-any.whl → 0.29.2__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.2"
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,58 @@ 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,
145
+ # but safe)
146
+ x = max(0, x)
147
+ y = max(0, y)
148
+
149
+ # Get pixel value (brightness or color) from the source image using clamped
150
+ # coordinates
151
+ pixel_value = source_image.getpixel((x, y))
152
+
153
+ if grayscale:
154
+ brightness = pixel_value
155
+ dot_fill = fill_color_dot # Use white for grayscale dots
156
+ else:
157
+ # Calculate brightness (luminance) from the RGB color
158
+ brightness = int(
159
+ 0.299 * pixel_value[0]
160
+ + 0.587 * pixel_value[1]
161
+ + 0.114 * pixel_value[2]
162
+ )
163
+ dot_fill = pixel_value # Use original color for color dots
164
+
165
+ # Calculate dot radius relative to cell size based on brightness Max radius
166
+ # is half the cell size. Scale by brightness (0-255).
167
+ dot_radius = (brightness / 255.0) * (cell.cellsize / 2)
168
+
169
+ # Draw the dot
170
+ draw.ellipse(
171
+ [
172
+ x - dot_radius,
173
+ y - dot_radius,
174
+ x + dot_radius,
175
+ y + dot_radius,
176
+ ],
177
+ fill=dot_fill,
178
+ )
179
+
180
+
120
181
  def create_halftone_image(
121
182
  original_image: Image.Image, cell_count: int, grayscale: bool = False
122
183
  ) -> Image.Image:
@@ -142,6 +203,7 @@ def create_halftone_image(
142
203
  source_image = original_image.convert("RGB")
143
204
  output_mode = "RGB"
144
205
  fill_color_black = (0, 0, 0)
206
+ fill_color_dot = None # Will be set inside the loop for color images
145
207
 
146
208
  width, height = original_image.size
147
209
 
@@ -152,40 +214,18 @@ def create_halftone_image(
152
214
  cellsize = width / cell_count
153
215
  rowtotal = math.ceil(height / cellsize)
154
216
 
217
+ # Determine the fill color for dots once if grayscale
218
+ grayscale_fill_color_dot = fill_color_dot if grayscale else None
219
+
155
220
  for row in range(rowtotal):
156
221
  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,
222
+ cell = HalftoneCell(col=col, row=row, cellsize=cellsize)
223
+ _draw_halftone_dot(
224
+ draw,
225
+ source_image,
226
+ cell,
227
+ grayscale,
228
+ grayscale_fill_color_dot,
189
229
  )
190
230
 
191
231
  return halftone_image
@@ -33,7 +33,6 @@ def build_subparser(subparsers) -> None:
33
33
  dest="image_filename",
34
34
  help="set the image filename",
35
35
  type=str,
36
- default=None,
37
36
  metavar="IMAGE_FILENAME",
38
37
  )
39
38
 
@@ -74,25 +73,31 @@ def run(args: argparse.Namespace) -> None:
74
73
  """
75
74
  log.debug(args)
76
75
 
77
- info = []
78
- image = Image.open(args.image_filename)
76
+ with Image.open(args.image_filename) as image:
77
+ exif_tags = extract_exif_tags(image, args.sort)
78
+
79
+ if not exif_tags:
80
+ print("No metadata found!")
81
+ return
82
+
83
+ output_info = []
84
+ specific_info_requested = False
79
85
 
80
86
  if args.camera:
81
- info.append(camera_metadata(image))
87
+ specific_info_requested = True
88
+ output_info.append(camera_metadata(exif_tags))
82
89
 
83
90
  if args.datetime:
84
- info.append(datetime(image))
91
+ specific_info_requested = True
92
+ output_info.append(datetime(exif_tags))
85
93
 
86
- if info:
87
- print("\n".join(info))
94
+ if specific_info_requested:
95
+ print("\n".join(output_info))
88
96
  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!")
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}")
96
101
 
97
102
 
98
103
  def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
@@ -117,14 +122,14 @@ def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
117
122
  return filtered_info
118
123
 
119
124
 
120
- def datetime(image: Image.Image):
125
+ def datetime(exif_tags: dict):
121
126
  """Extract datetime metadata."""
122
- exif_tags = extract_exif_tags(image)
123
- return exif_tags["DateTime"]
127
+ return exif_tags.get("DateTime", "Not available")
124
128
 
125
129
 
126
- def camera_metadata(image: Image.Image):
130
+ def camera_metadata(exif_tags: dict):
127
131
  """Extract camera and model metadata."""
128
- exif_tags = extract_exif_tags(image)
129
- metadata = f'{exif_tags["Make"]} {exif_tags["Model"]}'
132
+ make = exif_tags.get("Make", "")
133
+ model = exif_tags.get("Model", "")
134
+ metadata = f"{make} {model}"
130
135
  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.2
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=x3KFWNlsAU6oD0mfatPwnN0mtrqpZrMk6ndICOBKR8o,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=lt6RV0OuZkGs1LigTC1EcCCY42CocPFHWaJTDjJ5LFM,6693
11
+ fotolab/subcommands/info.py,sha256=vie578cEOesAyHohm0xCWwZBqXpblYJtdHGSyBgxsoU,3581
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.2.dist-info/entry_points.txt,sha256=mvw7AY_yZkIyjAxPtHNed9X99NZeLnMxEeAfEJUbrCM,44
18
+ fotolab-0.29.2.dist-info/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
19
+ fotolab-0.29.2.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
20
+ fotolab-0.29.2.dist-info/METADATA,sha256=GOyjpDfTCDNXKoRm2_ZB6fsc55dx5Ktfae4tFgtu8VI,12980
21
+ fotolab-0.29.2.dist-info/RECORD,,