fotolab 0.23.0__tar.gz → 0.25.0__tar.gz
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-0.23.0 → fotolab-0.25.0}/.pre-commit-config.yaml +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/CHANGELOG.md +13 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/PKG-INFO +2 -2
- {fotolab-0.23.0 → fotolab-0.25.0}/README.md +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/__init__.py +2 -2
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/cli.py +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/__init__.py +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/animate.py +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/auto.py +2 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/border.py +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/contrast.py +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/env.py +4 -4
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/info.py +11 -10
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/montage.py +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/resize.py +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/rotate.py +1 -1
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/sharpen.py +40 -3
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/watermark.py +67 -8
- {fotolab-0.23.0 → fotolab-0.25.0}/.coveragerc +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/.gitignore +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/.python-version +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/CONTRIBUTING.md +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/LICENSE.md +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/Pipfile +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/Pipfile.lock +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/docs/Makefile +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/docs/make.bat +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/CHANGELOG.md +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/CONTRIBUTING.md +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/LICENSE.md +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/README.md +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/_static/logo.jpg +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/conf.py +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/index.rst +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/__main__.py +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/noxfile.py +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/pyproject.toml +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/tests/__init__.py +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/tests/conftest.py +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/tests/test_env.py +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/tests/test_help_flag.py +0 -0
- {fotolab-0.23.0 → fotolab-0.25.0}/tests/test_quiet_flag.py +0 -0
@@ -7,6 +7,19 @@ and this project adheres to [0-based versioning](https://0ver.org/).
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## v0.25.0 (2025-01-05)
|
11
|
+
|
12
|
+
- Add `--before-after` or `-ba` flag to `sharpen` command
|
13
|
+
- Bump `pre-commit` hook and deps
|
14
|
+
- Bump copyright years
|
15
|
+
|
16
|
+
## v0.24.0 (2024-12-29)
|
17
|
+
|
18
|
+
- Bump `pre-commit` hook
|
19
|
+
- Pass image instead of filename when process `info` and `watermark` subcommand
|
20
|
+
- Refactor print result of `env` subcommand
|
21
|
+
- Support watermarking `gif` image
|
22
|
+
|
10
23
|
## v0.23.0 (2024-12-22)
|
11
24
|
|
12
25
|
- Add `-cw` or `--clockwise` option to `rotate` subcommand
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fotolab
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.25.0
|
4
4
|
Summary: A console program that manipulate images.
|
5
5
|
Keywords: photography,photo
|
6
6
|
Author-email: Kian-Meng Ang <kianmeng@cpan.org>
|
@@ -385,7 +385,7 @@ options:
|
|
385
385
|
|
386
386
|
## Copyright and License
|
387
387
|
|
388
|
-
Copyright (C) 2024 Kian-Meng Ang
|
388
|
+
Copyright (C) 2024,2025 Kian-Meng Ang
|
389
389
|
|
390
390
|
This program is free software: you can redistribute it and/or modify it under
|
391
391
|
the terms of the GNU Affero General Public License as published by the Free
|
@@ -362,7 +362,7 @@ options:
|
|
362
362
|
|
363
363
|
## Copyright and License
|
364
364
|
|
365
|
-
Copyright (C) 2024 Kian-Meng Ang
|
365
|
+
Copyright (C) 2024,2025 Kian-Meng Ang
|
366
366
|
|
367
367
|
This program is free software: you can redistribute it and/or modify it under
|
368
368
|
the terms of the GNU Affero General Public License as published by the Free
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -21,7 +21,7 @@ import subprocess
|
|
21
21
|
import sys
|
22
22
|
from pathlib import Path
|
23
23
|
|
24
|
-
__version__ = "0.
|
24
|
+
__version__ = "0.25.0"
|
25
25
|
|
26
26
|
log = logging.getLogger(__name__)
|
27
27
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -70,6 +70,7 @@ def run(args: argparse.Namespace) -> None:
|
|
70
70
|
"camera": False,
|
71
71
|
"canvas": False,
|
72
72
|
"lowercase": False,
|
73
|
+
"before_after": False,
|
73
74
|
}
|
74
75
|
combined_args = argparse.Namespace(**vars(args), **extra_args)
|
75
76
|
combined_args.overwrite = True
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -44,9 +44,9 @@ def run(_args: argparse.Namespace) -> None:
|
|
44
44
|
None
|
45
45
|
"""
|
46
46
|
sys_version = sys.version.replace("\n", "")
|
47
|
-
|
47
|
+
env = [
|
48
48
|
f"fotolab: {__version__}",
|
49
49
|
f"python: {sys_version}",
|
50
50
|
f"platform: {platform.platform()}",
|
51
|
-
|
52
|
-
)
|
51
|
+
]
|
52
|
+
print(*env, sep="\n")
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -73,18 +73,20 @@ def run(args: argparse.Namespace) -> None:
|
|
73
73
|
None
|
74
74
|
"""
|
75
75
|
log.debug(args)
|
76
|
+
|
76
77
|
info = []
|
78
|
+
image = Image.open(args.image_filename)
|
77
79
|
|
78
80
|
if args.camera:
|
79
|
-
info.append(camera_metadata(
|
81
|
+
info.append(camera_metadata(image))
|
80
82
|
|
81
83
|
if args.datetime:
|
82
|
-
info.append(datetime(
|
84
|
+
info.append(datetime(image))
|
83
85
|
|
84
86
|
if info:
|
85
87
|
print("\n".join(info))
|
86
88
|
else:
|
87
|
-
exif_tags = extract_exif_tags(
|
89
|
+
exif_tags = extract_exif_tags(image)
|
88
90
|
if exif_tags:
|
89
91
|
tag_name_width = max(map(len, exif_tags))
|
90
92
|
for tag_name, tag_value in exif_tags.items():
|
@@ -93,9 +95,8 @@ def run(args: argparse.Namespace) -> None:
|
|
93
95
|
print("No metadata found!")
|
94
96
|
|
95
97
|
|
96
|
-
def extract_exif_tags(
|
98
|
+
def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
|
97
99
|
"""Extract Exif metadata from image."""
|
98
|
-
image = Image.open(image_filename)
|
99
100
|
exif = image._getexif()
|
100
101
|
log.debug(exif)
|
101
102
|
|
@@ -112,14 +113,14 @@ def extract_exif_tags(image_filename: str, sort: bool = False) -> dict:
|
|
112
113
|
return filtered_info
|
113
114
|
|
114
115
|
|
115
|
-
def datetime(
|
116
|
+
def datetime(image: Image.Image):
|
116
117
|
"""Extract datetime metadata."""
|
117
|
-
exif_tags = extract_exif_tags(
|
118
|
+
exif_tags = extract_exif_tags(image)
|
118
119
|
return exif_tags["DateTime"]
|
119
120
|
|
120
121
|
|
121
|
-
def camera_metadata(
|
122
|
+
def camera_metadata(image: Image.Image):
|
122
123
|
"""Extract camera and model metadata."""
|
123
|
-
exif_tags = extract_exif_tags(
|
124
|
+
exif_tags = extract_exif_tags(image)
|
124
125
|
metadata = f'{exif_tags["Make"]} {exif_tags["Model"]}'
|
125
126
|
return metadata.strip()
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -17,10 +17,11 @@
|
|
17
17
|
|
18
18
|
import argparse
|
19
19
|
import logging
|
20
|
+
from pathlib import Path
|
20
21
|
|
21
22
|
from PIL import Image, ImageFilter
|
22
23
|
|
23
|
-
from fotolab import save_image
|
24
|
+
from fotolab import _open_image, save_image
|
24
25
|
|
25
26
|
log = logging.getLogger(__name__)
|
26
27
|
|
@@ -76,6 +77,15 @@ def build_subparser(subparsers) -> None:
|
|
76
77
|
metavar="THRESHOLD",
|
77
78
|
)
|
78
79
|
|
80
|
+
sharpen_parser.add_argument(
|
81
|
+
"-ba",
|
82
|
+
"--before-after",
|
83
|
+
default=False,
|
84
|
+
action="store_true",
|
85
|
+
dest="before_after",
|
86
|
+
help="generate a GIF showing before and after changes",
|
87
|
+
)
|
88
|
+
|
79
89
|
|
80
90
|
def run(args: argparse.Namespace) -> None:
|
81
91
|
"""Run sharpen subcommand.
|
@@ -95,4 +105,31 @@ def run(args: argparse.Namespace) -> None:
|
|
95
105
|
args.radius, percent=args.percent, threshold=args.threshold
|
96
106
|
)
|
97
107
|
)
|
98
|
-
|
108
|
+
if args.before_after:
|
109
|
+
save_gif_image(args, image_filename, original_image, sharpen_image)
|
110
|
+
else:
|
111
|
+
save_image(args, sharpen_image, image_filename, "sharpen")
|
112
|
+
|
113
|
+
|
114
|
+
def save_gif_image(args, image_filename, original_image, sharpen_image):
|
115
|
+
"""Save the original and sharpen image."""
|
116
|
+
image_file = Path(image_filename)
|
117
|
+
new_filename = Path(
|
118
|
+
args.output_dir,
|
119
|
+
image_file.with_name(f"sharpen_gif_{image_file.stem}.gif"),
|
120
|
+
)
|
121
|
+
new_filename.parent.mkdir(parents=True, exist_ok=True)
|
122
|
+
|
123
|
+
log.info("sharpen gif image: %s", new_filename)
|
124
|
+
original_image.save(
|
125
|
+
new_filename,
|
126
|
+
format="gif",
|
127
|
+
append_images=[sharpen_image],
|
128
|
+
save_all=True,
|
129
|
+
duration=2500,
|
130
|
+
loop=0,
|
131
|
+
optimize=True,
|
132
|
+
)
|
133
|
+
|
134
|
+
if args.open:
|
135
|
+
_open_image(new_filename)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -19,7 +19,7 @@ import argparse
|
|
19
19
|
import logging
|
20
20
|
import math
|
21
21
|
|
22
|
-
from PIL import Image, ImageColor, ImageDraw, ImageFont
|
22
|
+
from PIL import Image, ImageColor, ImageDraw, ImageFont, ImageSequence
|
23
23
|
|
24
24
|
from fotolab import save_image
|
25
25
|
from fotolab.subcommands.info import camera_metadata
|
@@ -157,13 +157,72 @@ def run(args: argparse.Namespace) -> None:
|
|
157
157
|
log.debug(args)
|
158
158
|
|
159
159
|
for image_filename in args.image_filenames:
|
160
|
-
|
161
|
-
|
160
|
+
image = Image.open(image_filename)
|
161
|
+
if image.format == "GIF":
|
162
|
+
watermark_gif_image(image, args)
|
163
|
+
else:
|
164
|
+
watermarked_image = watermark_non_gif_image(image, args)
|
165
|
+
save_image(args, watermarked_image, image_filename, "watermark")
|
162
166
|
|
163
167
|
|
164
|
-
def
|
168
|
+
def watermark_gif_image(original_image: Image.Image, args: argparse.Namespace):
|
169
|
+
"""Watermark the image."""
|
170
|
+
watermarked_image = original_image.copy()
|
171
|
+
|
172
|
+
frames = []
|
173
|
+
|
174
|
+
for frame in ImageSequence.Iterator(original_image):
|
175
|
+
frame = frame.convert("RGBA")
|
176
|
+
draw = ImageDraw.Draw(frame)
|
177
|
+
|
178
|
+
font = ImageFont.load_default(calc_font_size(original_image, args))
|
179
|
+
log.debug("default font: %s", " ".join(font.getname()))
|
180
|
+
|
181
|
+
text = args.text
|
182
|
+
if args.camera and camera_metadata(original_image):
|
183
|
+
text = camera_metadata(original_image)
|
184
|
+
|
185
|
+
if args.lowercase:
|
186
|
+
text = text.lower()
|
187
|
+
|
188
|
+
(left, top, right, bottom) = draw.textbbox(
|
189
|
+
xy=(0, 0), text=text, font=font
|
190
|
+
)
|
191
|
+
text_width = right - left
|
192
|
+
text_height = bottom - top
|
193
|
+
(position_x, position_y) = calc_position(
|
194
|
+
watermarked_image,
|
195
|
+
text_width,
|
196
|
+
text_height,
|
197
|
+
args.position,
|
198
|
+
calc_padding(original_image, args),
|
199
|
+
)
|
200
|
+
|
201
|
+
draw.text(
|
202
|
+
(position_x, position_y),
|
203
|
+
text,
|
204
|
+
font=font,
|
205
|
+
fill=(*ImageColor.getrgb(args.font_color), 128),
|
206
|
+
stroke_width=calc_font_outline_width(original_image, args),
|
207
|
+
stroke_fill=(*ImageColor.getrgb(args.outline_color), 128),
|
208
|
+
)
|
209
|
+
frames.append(frame)
|
210
|
+
|
211
|
+
frames[0].save(
|
212
|
+
"foo.gif",
|
213
|
+
format="GIF",
|
214
|
+
append_images=frames[1:],
|
215
|
+
save_all=True,
|
216
|
+
duration=original_image.info.get("duration", 100),
|
217
|
+
loop=original_image.info.get("loop", 0),
|
218
|
+
disposal=original_image.info.get("disposal", 2),
|
219
|
+
)
|
220
|
+
|
221
|
+
|
222
|
+
def watermark_non_gif_image(
|
223
|
+
original_image: Image.Image, args: argparse.Namespace
|
224
|
+
):
|
165
225
|
"""Watermark the image."""
|
166
|
-
original_image = Image.open(image_filename)
|
167
226
|
watermarked_image = original_image.copy()
|
168
227
|
|
169
228
|
draw = ImageDraw.Draw(watermarked_image)
|
@@ -172,8 +231,8 @@ def watermark_image(image_filename, args):
|
|
172
231
|
log.debug("default font: %s", " ".join(font.getname()))
|
173
232
|
|
174
233
|
text = args.text
|
175
|
-
if args.camera and camera_metadata(
|
176
|
-
text = camera_metadata(
|
234
|
+
if args.camera and camera_metadata(original_image):
|
235
|
+
text = camera_metadata(original_image)
|
177
236
|
|
178
237
|
if args.lowercase:
|
179
238
|
text = text.lower()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|