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 +1 -1
- fotolab/subcommands/halftone.py +70 -32
- fotolab/subcommands/info.py +35 -17
- fotolab/subcommands/watermark.py +11 -3
- {fotolab-0.29.0.dist-info → fotolab-0.29.1.dist-info}/METADATA +4 -4
- {fotolab-0.29.0.dist-info → fotolab-0.29.1.dist-info}/RECORD +9 -9
- {fotolab-0.29.0.dist-info → fotolab-0.29.1.dist-info}/LICENSE.md +0 -0
- {fotolab-0.29.0.dist-info → fotolab-0.29.1.dist-info}/WHEEL +0 -0
- {fotolab-0.29.0.dist-info → fotolab-0.29.1.dist-info}/entry_points.txt +0 -0
fotolab/__init__.py
CHANGED
fotolab/subcommands/halftone.py
CHANGED
@@ -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
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
fotolab/subcommands/info.py
CHANGED
@@ -74,25 +74,41 @@ def run(args: argparse.Namespace) -> None:
|
|
74
74
|
"""
|
75
75
|
log.debug(args)
|
76
76
|
|
77
|
-
|
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
|
-
|
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
|
-
|
98
|
+
specific_info_requested = True
|
99
|
+
# TODO: Add error handling for missing keys
|
100
|
+
output_info.append(datetime(exif_tags))
|
85
101
|
|
86
|
-
if
|
87
|
-
print("\n".join(
|
102
|
+
if specific_info_requested:
|
103
|
+
print("\n".join(output_info))
|
88
104
|
else:
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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(
|
136
|
+
def datetime(exif_tags: dict):
|
121
137
|
"""Extract datetime metadata."""
|
122
|
-
|
138
|
+
# TODO: Add error handling for missing key
|
123
139
|
return exif_tags["DateTime"]
|
124
140
|
|
125
141
|
|
126
|
-
def camera_metadata(
|
142
|
+
def camera_metadata(exif_tags: dict):
|
127
143
|
"""Extract camera and model metadata."""
|
128
|
-
|
129
|
-
|
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()
|
fotolab/subcommands/watermark.py
CHANGED
@@ -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
|
286
|
-
|
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.
|
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
|
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=
|
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=
|
11
|
-
fotolab/subcommands/info.py,sha256=
|
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=
|
17
|
-
fotolab-0.29.
|
18
|
-
fotolab-0.29.
|
19
|
-
fotolab-0.29.
|
20
|
-
fotolab-0.29.
|
21
|
-
fotolab-0.29.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|