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 +1 -1
- fotolab/subcommands/halftone.py +72 -32
- fotolab/subcommands/info.py +25 -20
- fotolab/subcommands/watermark.py +11 -3
- {fotolab-0.29.0.dist-info → fotolab-0.29.2.dist-info}/METADATA +4 -4
- {fotolab-0.29.0.dist-info → fotolab-0.29.2.dist-info}/RECORD +9 -9
- {fotolab-0.29.0.dist-info → fotolab-0.29.2.dist-info}/LICENSE.md +0 -0
- {fotolab-0.29.0.dist-info → fotolab-0.29.2.dist-info}/WHEEL +0 -0
- {fotolab-0.29.0.dist-info → fotolab-0.29.2.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,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
|
-
|
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,
|
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
|
fotolab/subcommands/info.py
CHANGED
@@ -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
|
-
|
78
|
-
|
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
|
-
|
87
|
+
specific_info_requested = True
|
88
|
+
output_info.append(camera_metadata(exif_tags))
|
82
89
|
|
83
90
|
if args.datetime:
|
84
|
-
|
91
|
+
specific_info_requested = True
|
92
|
+
output_info.append(datetime(exif_tags))
|
85
93
|
|
86
|
-
if
|
87
|
-
print("\n".join(
|
94
|
+
if specific_info_requested:
|
95
|
+
print("\n".join(output_info))
|
88
96
|
else:
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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(
|
125
|
+
def datetime(exif_tags: dict):
|
121
126
|
"""Extract datetime metadata."""
|
122
|
-
exif_tags
|
123
|
-
return exif_tags["DateTime"]
|
127
|
+
return exif_tags.get("DateTime", "Not available")
|
124
128
|
|
125
129
|
|
126
|
-
def camera_metadata(
|
130
|
+
def camera_metadata(exif_tags: dict):
|
127
131
|
"""Extract camera and model metadata."""
|
128
|
-
|
129
|
-
|
132
|
+
make = exif_tags.get("Make", "")
|
133
|
+
model = exif_tags.get("Model", "")
|
134
|
+
metadata = f"{make} {model}"
|
130
135
|
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.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
|
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=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=
|
11
|
-
fotolab/subcommands/info.py,sha256=
|
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=
|
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.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,,
|
File without changes
|
File without changes
|
File without changes
|