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.
Files changed (42) hide show
  1. {fotolab-0.23.0 → fotolab-0.25.0}/.pre-commit-config.yaml +1 -1
  2. {fotolab-0.23.0 → fotolab-0.25.0}/CHANGELOG.md +13 -0
  3. {fotolab-0.23.0 → fotolab-0.25.0}/PKG-INFO +2 -2
  4. {fotolab-0.23.0 → fotolab-0.25.0}/README.md +1 -1
  5. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/__init__.py +2 -2
  6. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/cli.py +1 -1
  7. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/__init__.py +1 -1
  8. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/animate.py +1 -1
  9. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/auto.py +2 -1
  10. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/border.py +1 -1
  11. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/contrast.py +1 -1
  12. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/env.py +4 -4
  13. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/info.py +11 -10
  14. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/montage.py +1 -1
  15. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/resize.py +1 -1
  16. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/rotate.py +1 -1
  17. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/sharpen.py +40 -3
  18. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/subcommands/watermark.py +67 -8
  19. {fotolab-0.23.0 → fotolab-0.25.0}/.coveragerc +0 -0
  20. {fotolab-0.23.0 → fotolab-0.25.0}/.gitignore +0 -0
  21. {fotolab-0.23.0 → fotolab-0.25.0}/.python-version +0 -0
  22. {fotolab-0.23.0 → fotolab-0.25.0}/CONTRIBUTING.md +0 -0
  23. {fotolab-0.23.0 → fotolab-0.25.0}/LICENSE.md +0 -0
  24. {fotolab-0.23.0 → fotolab-0.25.0}/Pipfile +0 -0
  25. {fotolab-0.23.0 → fotolab-0.25.0}/Pipfile.lock +0 -0
  26. {fotolab-0.23.0 → fotolab-0.25.0}/docs/Makefile +0 -0
  27. {fotolab-0.23.0 → fotolab-0.25.0}/docs/make.bat +0 -0
  28. {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/CHANGELOG.md +0 -0
  29. {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/CONTRIBUTING.md +0 -0
  30. {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/LICENSE.md +0 -0
  31. {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/README.md +0 -0
  32. {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/_static/logo.jpg +0 -0
  33. {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/conf.py +0 -0
  34. {fotolab-0.23.0 → fotolab-0.25.0}/docs/source/index.rst +0 -0
  35. {fotolab-0.23.0 → fotolab-0.25.0}/fotolab/__main__.py +0 -0
  36. {fotolab-0.23.0 → fotolab-0.25.0}/noxfile.py +0 -0
  37. {fotolab-0.23.0 → fotolab-0.25.0}/pyproject.toml +0 -0
  38. {fotolab-0.23.0 → fotolab-0.25.0}/tests/__init__.py +0 -0
  39. {fotolab-0.23.0 → fotolab-0.25.0}/tests/conftest.py +0 -0
  40. {fotolab-0.23.0 → fotolab-0.25.0}/tests/test_env.py +0 -0
  41. {fotolab-0.23.0 → fotolab-0.25.0}/tests/test_help_flag.py +0 -0
  42. {fotolab-0.23.0 → fotolab-0.25.0}/tests/test_quiet_flag.py +0 -0
@@ -107,7 +107,7 @@ repos:
107
107
  - --disable=R0801,W0212
108
108
 
109
109
  - repo: https://github.com/pre-commit/mirrors-mypy
110
- rev: v1.13.0
110
+ rev: v1.14.1
111
111
  hooks:
112
112
  - id: mypy
113
113
  exclude: docs/
@@ -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.23.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.23.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
4
4
  # it under the terms of the GNU General Public License as published by
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
- print(
47
+ env = [
48
48
  f"fotolab: {__version__}",
49
49
  f"python: {sys_version}",
50
50
  f"platform: {platform.platform()}",
51
- sep="\n",
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(args.image_filename))
81
+ info.append(camera_metadata(image))
80
82
 
81
83
  if args.datetime:
82
- info.append(datetime(args.image_filename))
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(args.image_filename)
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(image_filename: str, sort: bool = False) -> dict:
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(image_filename):
116
+ def datetime(image: Image.Image):
116
117
  """Extract datetime metadata."""
117
- exif_tags = extract_exif_tags(image_filename)
118
+ exif_tags = extract_exif_tags(image)
118
119
  return exif_tags["DateTime"]
119
120
 
120
121
 
121
- def camera_metadata(image_filename):
122
+ def camera_metadata(image: Image.Image):
122
123
  """Extract camera and model metadata."""
123
- exif_tags = extract_exif_tags(image_filename)
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
@@ -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
@@ -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
@@ -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
- save_image(args, sharpen_image, image_filename, "sharpen")
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
- watermarked_image = watermark_image(image_filename, args)
161
- save_image(args, watermarked_image, image_filename, "watermark")
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 watermark_image(image_filename, args):
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(image_filename):
176
- text = camera_metadata(image_filename)
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