fotolab 0.21.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 +67 -0
- fotolab/__main__.py +22 -0
- fotolab/animate.py +114 -0
- fotolab/auto.py +83 -0
- fotolab/border.py +139 -0
- fotolab/cli.py +180 -0
- fotolab/contrast.py +77 -0
- fotolab/env.py +52 -0
- fotolab/info.py +103 -0
- fotolab/montage.py +71 -0
- fotolab/resize.py +176 -0
- fotolab/rotate.py +61 -0
- fotolab/sharpen.py +98 -0
- fotolab/watermark.py +262 -0
- fotolab-0.21.1.dist-info/LICENSE.md +616 -0
- fotolab-0.21.1.dist-info/METADATA +399 -0
- fotolab-0.21.1.dist-info/RECORD +19 -0
- fotolab-0.21.1.dist-info/WHEEL +4 -0
- fotolab-0.21.1.dist-info/entry_points.txt +3 -0
fotolab/watermark.py
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
# Copyright (C) 2024 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
|
+
"""Watermark subcommand."""
|
17
|
+
|
18
|
+
import argparse
|
19
|
+
import logging
|
20
|
+
import math
|
21
|
+
|
22
|
+
from PIL import Image, ImageColor, ImageDraw, ImageFont
|
23
|
+
|
24
|
+
from fotolab import save_image
|
25
|
+
from fotolab.info import extract_exif_tags
|
26
|
+
|
27
|
+
log = logging.getLogger(__name__)
|
28
|
+
|
29
|
+
FONT_SIZE_ASPECT_RATIO = 12 / 600
|
30
|
+
FONT_PADDING_ASPECT_RATIO = 15 / 600
|
31
|
+
FONT_OUTLINE_WIDTH_ASPECT_RATIO = 2 / 600
|
32
|
+
POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right"]
|
33
|
+
|
34
|
+
|
35
|
+
def build_subparser(subparsers) -> None:
|
36
|
+
"""Build the subparser."""
|
37
|
+
watermark_parser = subparsers.add_parser(
|
38
|
+
"watermark", help="watermark an image"
|
39
|
+
)
|
40
|
+
|
41
|
+
watermark_parser.set_defaults(func=run)
|
42
|
+
|
43
|
+
watermark_parser.add_argument(
|
44
|
+
dest="image_filenames",
|
45
|
+
help="set the image filenames",
|
46
|
+
nargs="+",
|
47
|
+
type=str,
|
48
|
+
default=None,
|
49
|
+
metavar="IMAGE_FILENAMES",
|
50
|
+
)
|
51
|
+
|
52
|
+
watermark_parser.add_argument(
|
53
|
+
"-t",
|
54
|
+
"--text",
|
55
|
+
dest="text",
|
56
|
+
help="set the watermark text (default: '%(default)s')",
|
57
|
+
type=str,
|
58
|
+
default="kianmeng.org",
|
59
|
+
metavar="WATERMARK_TEXT",
|
60
|
+
)
|
61
|
+
|
62
|
+
watermark_parser.add_argument(
|
63
|
+
"-p",
|
64
|
+
"--position",
|
65
|
+
dest="position",
|
66
|
+
choices=POSITIONS,
|
67
|
+
help="set position of the watermark text (default: '%(default)s')",
|
68
|
+
default="bottom-left",
|
69
|
+
)
|
70
|
+
|
71
|
+
watermark_parser.add_argument(
|
72
|
+
"-pd",
|
73
|
+
"--padding",
|
74
|
+
dest="padding",
|
75
|
+
type=int,
|
76
|
+
default=15,
|
77
|
+
help=(
|
78
|
+
"set the padding of the watermark text relative to the image "
|
79
|
+
"(default: '%(default)s')"
|
80
|
+
),
|
81
|
+
metavar="PADDING",
|
82
|
+
)
|
83
|
+
|
84
|
+
watermark_parser.add_argument(
|
85
|
+
"-fs",
|
86
|
+
"--font-size",
|
87
|
+
dest="font_size",
|
88
|
+
type=int,
|
89
|
+
default=12,
|
90
|
+
help="set the font size of watermark text (default: '%(default)s')",
|
91
|
+
metavar="FONT_SIZE",
|
92
|
+
)
|
93
|
+
|
94
|
+
watermark_parser.add_argument(
|
95
|
+
"-fc",
|
96
|
+
"--font-color",
|
97
|
+
dest="font_color",
|
98
|
+
type=str,
|
99
|
+
default="white",
|
100
|
+
help="set the font color of watermark text (default: '%(default)s')",
|
101
|
+
metavar="FONT_COLOR",
|
102
|
+
)
|
103
|
+
|
104
|
+
watermark_parser.add_argument(
|
105
|
+
"-ow",
|
106
|
+
"--outline-width",
|
107
|
+
dest="outline_width",
|
108
|
+
type=int,
|
109
|
+
default=2,
|
110
|
+
help=(
|
111
|
+
"set the outline width of the watermark text "
|
112
|
+
"(default: '%(default)s')"
|
113
|
+
),
|
114
|
+
metavar="OUTLINE_WIDTH",
|
115
|
+
)
|
116
|
+
|
117
|
+
watermark_parser.add_argument(
|
118
|
+
"-oc",
|
119
|
+
"--outline-color",
|
120
|
+
dest="outline_color",
|
121
|
+
type=str,
|
122
|
+
default="black",
|
123
|
+
help=(
|
124
|
+
"set the outline color of the watermark text "
|
125
|
+
"(default: '%(default)s')"
|
126
|
+
),
|
127
|
+
metavar="OUTLINE_COLOR",
|
128
|
+
)
|
129
|
+
|
130
|
+
watermark_parser.add_argument(
|
131
|
+
"--camera",
|
132
|
+
default=False,
|
133
|
+
action="store_true",
|
134
|
+
dest="camera",
|
135
|
+
help="use camera metadata as watermark",
|
136
|
+
)
|
137
|
+
|
138
|
+
watermark_parser.add_argument(
|
139
|
+
"-l",
|
140
|
+
"--lowercase",
|
141
|
+
default=True,
|
142
|
+
action="store_true",
|
143
|
+
dest="lowercase",
|
144
|
+
help="lowercase the watermark text",
|
145
|
+
)
|
146
|
+
|
147
|
+
|
148
|
+
def run(args: argparse.Namespace) -> None:
|
149
|
+
"""Run watermark subcommand.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
args (argparse.Namespace): Config from command line arguments
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
None
|
156
|
+
"""
|
157
|
+
log.debug(args)
|
158
|
+
|
159
|
+
for image_filename in args.image_filenames:
|
160
|
+
watermarked_image = watermark_image(image_filename, args)
|
161
|
+
save_image(args, watermarked_image, image_filename, "watermark")
|
162
|
+
|
163
|
+
|
164
|
+
def watermark_image(image_filename, args):
|
165
|
+
"""Watermark the image."""
|
166
|
+
original_image = Image.open(image_filename)
|
167
|
+
watermarked_image = original_image.copy()
|
168
|
+
|
169
|
+
draw = ImageDraw.Draw(watermarked_image)
|
170
|
+
|
171
|
+
font = ImageFont.load_default(calc_font_size(original_image, args))
|
172
|
+
log.debug("default font: %s", " ".join(font.getname()))
|
173
|
+
|
174
|
+
text = args.text
|
175
|
+
if args.camera and camera_metadata(image_filename):
|
176
|
+
text = camera_metadata(image_filename)
|
177
|
+
|
178
|
+
if args.lowercase:
|
179
|
+
text = text.lower()
|
180
|
+
|
181
|
+
(left, top, right, bottom) = draw.textbbox(xy=(0, 0), text=text, font=font)
|
182
|
+
text_width = right - left
|
183
|
+
text_height = bottom - top
|
184
|
+
(position_x, position_y) = calc_position(
|
185
|
+
watermarked_image,
|
186
|
+
text_width,
|
187
|
+
text_height,
|
188
|
+
args.position,
|
189
|
+
calc_padding(original_image, args),
|
190
|
+
)
|
191
|
+
|
192
|
+
draw.text(
|
193
|
+
(position_x, position_y),
|
194
|
+
text,
|
195
|
+
font=font,
|
196
|
+
fill=(*ImageColor.getrgb(args.font_color), 128),
|
197
|
+
stroke_width=calc_font_outline_width(original_image, args),
|
198
|
+
stroke_fill=(*ImageColor.getrgb(args.outline_color), 128),
|
199
|
+
)
|
200
|
+
return watermarked_image
|
201
|
+
|
202
|
+
|
203
|
+
def camera_metadata(image_filename):
|
204
|
+
"""Extract camera and model metadata."""
|
205
|
+
exif_tags = extract_exif_tags(image_filename)
|
206
|
+
metadata = f'{exif_tags["Make"]} {exif_tags["Model"]}'
|
207
|
+
return metadata.strip()
|
208
|
+
|
209
|
+
|
210
|
+
def calc_font_size(image, args) -> int:
|
211
|
+
"""Calculate the font size based on the width of the image."""
|
212
|
+
width, _height = image.size
|
213
|
+
new_font_size = args.font_size
|
214
|
+
if width > 600:
|
215
|
+
new_font_size = math.floor(FONT_SIZE_ASPECT_RATIO * width)
|
216
|
+
|
217
|
+
log.debug("new font size: %d", new_font_size)
|
218
|
+
return new_font_size
|
219
|
+
|
220
|
+
|
221
|
+
def calc_font_outline_width(image, args) -> int:
|
222
|
+
"""Calculate the font padding based on the width of the image."""
|
223
|
+
width, _height = image.size
|
224
|
+
new_font_outline_width = args.outline_width
|
225
|
+
if width > 600:
|
226
|
+
new_font_outline_width = math.floor(
|
227
|
+
FONT_OUTLINE_WIDTH_ASPECT_RATIO * width
|
228
|
+
)
|
229
|
+
|
230
|
+
log.debug("new font outline width: %d", new_font_outline_width)
|
231
|
+
return new_font_outline_width
|
232
|
+
|
233
|
+
|
234
|
+
def calc_padding(image, args) -> int:
|
235
|
+
"""Calculate the font padding based on the width of the image."""
|
236
|
+
width, _height = image.size
|
237
|
+
new_padding = args.padding
|
238
|
+
if width > 600:
|
239
|
+
new_padding = math.floor(FONT_PADDING_ASPECT_RATIO * width)
|
240
|
+
|
241
|
+
log.debug("new padding: %d", new_padding)
|
242
|
+
return new_padding
|
243
|
+
|
244
|
+
|
245
|
+
def calc_position(image, text_width, text_height, position, padding) -> tuple:
|
246
|
+
"""Calculate the boundary coordinates of the watermark text."""
|
247
|
+
(position_x, position_y) = (0, 0)
|
248
|
+
|
249
|
+
if position == "top-left":
|
250
|
+
position_x = 0 + padding
|
251
|
+
position_y = 0 + padding
|
252
|
+
elif position == "top-right":
|
253
|
+
position_x = image.width - text_width - padding
|
254
|
+
position_y = 0 + padding
|
255
|
+
elif position == "bottom-left":
|
256
|
+
position_x = 0 + padding
|
257
|
+
position_y = image.height - text_height - padding
|
258
|
+
elif position == "bottom-right":
|
259
|
+
position_x = image.width - text_width - padding
|
260
|
+
position_y = image.height - text_height - padding
|
261
|
+
|
262
|
+
return (position_x, position_y)
|