fotolab 0.29.1__tar.gz → 0.30.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 (45) hide show
  1. {fotolab-0.29.1 → fotolab-0.30.0}/.pre-commit-config.yaml +3 -3
  2. {fotolab-0.29.1 → fotolab-0.30.0}/CHANGELOG.md +17 -0
  3. {fotolab-0.29.1 → fotolab-0.30.0}/PKG-INFO +10 -3
  4. {fotolab-0.29.1 → fotolab-0.30.0}/Pipfile.lock +0 -4
  5. {fotolab-0.29.1 → fotolab-0.30.0}/README.md +9 -2
  6. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/__init__.py +1 -1
  7. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/cli.py +4 -4
  8. fotolab-0.30.0/fotolab/subcommands/animate.py +193 -0
  9. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/contrast.py +3 -1
  10. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/halftone.py +6 -4
  11. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/info.py +7 -20
  12. {fotolab-0.29.1 → fotolab-0.30.0}/noxfile.py +1 -0
  13. fotolab-0.29.1/fotolab/subcommands/animate.py +0 -131
  14. {fotolab-0.29.1 → fotolab-0.30.0}/.coveragerc +0 -0
  15. {fotolab-0.29.1 → fotolab-0.30.0}/.gitignore +0 -0
  16. {fotolab-0.29.1 → fotolab-0.30.0}/.python-version +0 -0
  17. {fotolab-0.29.1 → fotolab-0.30.0}/CONTRIBUTING.md +0 -0
  18. {fotolab-0.29.1 → fotolab-0.30.0}/LICENSE.md +0 -0
  19. {fotolab-0.29.1 → fotolab-0.30.0}/Pipfile +0 -0
  20. {fotolab-0.29.1 → fotolab-0.30.0}/docs/Makefile +0 -0
  21. {fotolab-0.29.1 → fotolab-0.30.0}/docs/make.bat +0 -0
  22. {fotolab-0.29.1 → fotolab-0.30.0}/docs/source/CHANGELOG.md +0 -0
  23. {fotolab-0.29.1 → fotolab-0.30.0}/docs/source/CONTRIBUTING.md +0 -0
  24. {fotolab-0.29.1 → fotolab-0.30.0}/docs/source/LICENSE.md +0 -0
  25. {fotolab-0.29.1 → fotolab-0.30.0}/docs/source/README.md +0 -0
  26. {fotolab-0.29.1 → fotolab-0.30.0}/docs/source/_static/logo.jpg +0 -0
  27. {fotolab-0.29.1 → fotolab-0.30.0}/docs/source/conf.py +0 -0
  28. {fotolab-0.29.1 → fotolab-0.30.0}/docs/source/index.rst +0 -0
  29. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/__main__.py +0 -0
  30. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/__init__.py +0 -0
  31. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/auto.py +0 -0
  32. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/border.py +0 -0
  33. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/env.py +0 -0
  34. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/montage.py +0 -0
  35. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/resize.py +0 -0
  36. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/rotate.py +0 -0
  37. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/sharpen.py +0 -0
  38. {fotolab-0.29.1 → fotolab-0.30.0}/fotolab/subcommands/watermark.py +0 -0
  39. {fotolab-0.29.1 → fotolab-0.30.0}/generate +0 -0
  40. {fotolab-0.29.1 → fotolab-0.30.0}/pyproject.toml +0 -0
  41. {fotolab-0.29.1 → fotolab-0.30.0}/tests/__init__.py +0 -0
  42. {fotolab-0.29.1 → fotolab-0.30.0}/tests/conftest.py +0 -0
  43. {fotolab-0.29.1 → fotolab-0.30.0}/tests/test_env_subcommand.py +0 -0
  44. {fotolab-0.29.1 → fotolab-0.30.0}/tests/test_help_flag.py +0 -0
  45. {fotolab-0.29.1 → fotolab-0.30.0}/tests/test_quiet_flag.py +0 -0
@@ -48,7 +48,7 @@ repos:
48
48
  rev: 25.1.0
49
49
  hooks:
50
50
  - id: black
51
- language_version: python3.12
51
+ language_version: python3.13
52
52
  args:
53
53
  - --line-length=79
54
54
  - --target-version=py38
@@ -74,13 +74,13 @@ repos:
74
74
  - --remove-all-unused-imports
75
75
  language: python
76
76
  files: \.py$
77
- language_version: python3.12
77
+ language_version: python3.13
78
78
 
79
79
  - repo: https://github.com/PyCQA/flake8
80
80
  rev: 7.2.0
81
81
  hooks:
82
82
  - id: flake8
83
- language_version: python3.12
83
+ language_version: python3.13
84
84
  additional_dependencies:
85
85
  - flake8-docstrings
86
86
  - flake8-pytest-style
@@ -7,6 +7,23 @@ and this project adheres to [0-based versioning](https://0ver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.30.0 (2025-05-18)
11
+
12
+ - Add WEBP `quality`, `lossless`, and `method` args
13
+ - Bump Python version for `pre-commit` check
14
+ - Close image files after creating animation
15
+ - Update help message generated by latest Python to readme
16
+ - Update help message in readme
17
+ - Validate duration for animate command
18
+
19
+ ## v0.29.2 (2025-05-11)
20
+
21
+ - Bump deps
22
+ - Code format
23
+ - Improve info subcommand by adding error handling for missing keys
24
+ - Remove default value for image filename argument in info subcommand
25
+ - Use context manager to open image and remove redundant image close.
26
+
10
27
  ## v0.29.1 (2025-05-04)
11
28
 
12
29
  - Improve camera metadata handling in watermark subcommand.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fotolab
3
- Version: 0.29.1
3
+ Version: 0.30.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>
@@ -106,7 +106,8 @@ fotolab animate -h
106
106
 
107
107
  ```console
108
108
  usage: fotolab animate [-h] [-f FORMAT] [-d DURATION] [-l LOOP] [-op]
109
- [-od OUTPUT_DIR]
109
+ [--webp-quality QUALITY] [--webp-lossless]
110
+ [--webp-method METHOD] [-od OUTPUT_DIR]
110
111
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
111
112
 
112
113
  positional arguments:
@@ -116,10 +117,16 @@ options:
116
117
  -h, --help show this help message and exit
117
118
  -f, --format FORMAT set the image format (default: 'gif')
118
119
  -d, --duration DURATION
119
- set the duration in milliseconds (default: '2500')
120
+ set the duration in milliseconds (must be a positive
121
+ integer, default: '2500')
120
122
  -l, --loop LOOP set the loop cycle (default: '0')
121
123
  -op, --open open the image using default program (default:
122
124
  'False')
125
+ --webp-quality QUALITY
126
+ set WEBP quality (0-100, default: '80')
127
+ --webp-lossless enable WEBP lossless compression (default: 'False')
128
+ --webp-method METHOD set WEBP encoding method (0=fast, 6=slow/best,
129
+ default: '4')
123
130
  -od, --output-dir OUTPUT_DIR
124
131
  set default output folder (default: 'output')
125
132
  ```
@@ -16,10 +16,6 @@
16
16
  ]
17
17
  },
18
18
  "default": {
19
- "fotolab": {
20
- "file": ".",
21
- "markers": "python_version >= '3.9'"
22
- },
23
19
  "pillow": {
24
20
  "hashes": [
25
21
  "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928",
@@ -83,7 +83,8 @@ fotolab animate -h
83
83
 
84
84
  ```console
85
85
  usage: fotolab animate [-h] [-f FORMAT] [-d DURATION] [-l LOOP] [-op]
86
- [-od OUTPUT_DIR]
86
+ [--webp-quality QUALITY] [--webp-lossless]
87
+ [--webp-method METHOD] [-od OUTPUT_DIR]
87
88
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
88
89
 
89
90
  positional arguments:
@@ -93,10 +94,16 @@ options:
93
94
  -h, --help show this help message and exit
94
95
  -f, --format FORMAT set the image format (default: 'gif')
95
96
  -d, --duration DURATION
96
- set the duration in milliseconds (default: '2500')
97
+ set the duration in milliseconds (must be a positive
98
+ integer, default: '2500')
97
99
  -l, --loop LOOP set the loop cycle (default: '0')
98
100
  -op, --open open the image using default program (default:
99
101
  'False')
102
+ --webp-quality QUALITY
103
+ set WEBP quality (0-100, default: '80')
104
+ --webp-lossless enable WEBP lossless compression (default: 'False')
105
+ --webp-method METHOD set WEBP encoding method (0=fast, 6=slow/best,
106
+ default: '4')
100
107
  -od, --output-dir OUTPUT_DIR
101
108
  set default output folder (default: 'output')
102
109
  ```
@@ -24,7 +24,7 @@ from pathlib import Path
24
24
 
25
25
  from PIL import Image
26
26
 
27
- __version__ = "0.29.1"
27
+ __version__ = "0.30.0"
28
28
 
29
29
  log = logging.getLogger(__name__)
30
30
 
@@ -15,9 +15,9 @@
15
15
 
16
16
  """A console program to manipulate photos.
17
17
 
18
- website: https://github.com/kianmeng/fotolab
19
- changelog: https://github.com/kianmeng/fotolab/blob/master/CHANGELOG.md
20
- issues: https://github.com/kianmeng/fotolab/issues
18
+ website: https://github.com/kianmeng/fotolab
19
+ changelog: https://github.com/kianmeng/fotolab/blob/master/CHANGELOG.md
20
+ issues: https://github.com/kianmeng/fotolab/issues
21
21
  """
22
22
 
23
23
  import argparse
@@ -143,7 +143,7 @@ def main(args: Optional[Sequence[str]] = None) -> None:
143
143
  # correctly
144
144
  log.error(
145
145
  "subcommand '%s' is missing its execution function.",
146
- parsed_args.command
146
+ parsed_args.command,
147
147
  )
148
148
  parser.print_help(sys.stderr)
149
149
  raise SystemExit(1)
@@ -0,0 +1,193 @@
1
+ # Copyright (C) 2024,2025 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
+ """Animate subcommand."""
17
+
18
+ import argparse
19
+ import logging
20
+ from pathlib import Path
21
+
22
+ from PIL import Image
23
+
24
+ from fotolab import _open_image
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+
29
+ def _validate_duration(value: str) -> int:
30
+ """Validate that the duration is a positive integer."""
31
+ try:
32
+ ivalue = int(value)
33
+ if ivalue <= 0:
34
+ raise argparse.ArgumentTypeError(
35
+ f"duration must be a positive integer, but got {value}"
36
+ )
37
+ return ivalue
38
+ except ValueError as e:
39
+ raise argparse.ArgumentTypeError(
40
+ f"duration must be an integer, but got '{value}'"
41
+ ) from e
42
+
43
+
44
+ def build_subparser(subparsers) -> None:
45
+ """Build the subparser."""
46
+ animate_parser = subparsers.add_parser("animate", help="animate an image")
47
+
48
+ animate_parser.set_defaults(func=run)
49
+
50
+ animate_parser.add_argument(
51
+ dest="image_filenames",
52
+ help="set the image filenames",
53
+ nargs="+",
54
+ type=str,
55
+ default=None,
56
+ metavar="IMAGE_FILENAMES",
57
+ )
58
+
59
+ animate_parser.add_argument(
60
+ "-f",
61
+ "--format",
62
+ dest="format",
63
+ type=str,
64
+ choices=["gif", "webp"],
65
+ default="gif",
66
+ help="set the image format (default: '%(default)s')",
67
+ metavar="FORMAT",
68
+ )
69
+
70
+ animate_parser.add_argument(
71
+ "-d",
72
+ "--duration",
73
+ dest="duration",
74
+ type=_validate_duration,
75
+ default=2500,
76
+ help="set the duration in milliseconds (must be a positive integer, default: '%(default)s')",
77
+ metavar="DURATION",
78
+ )
79
+
80
+ animate_parser.add_argument(
81
+ "-l",
82
+ "--loop",
83
+ dest="loop",
84
+ type=int,
85
+ default=0,
86
+ help="set the loop cycle (default: '%(default)s')",
87
+ metavar="LOOP",
88
+ )
89
+
90
+ animate_parser.add_argument(
91
+ "-op",
92
+ "--open",
93
+ default=False,
94
+ action="store_true",
95
+ dest="open",
96
+ help="open the image using default program (default: '%(default)s')",
97
+ )
98
+
99
+ animate_parser.add_argument(
100
+ "--webp-quality",
101
+ dest="webp_quality",
102
+ type=int,
103
+ default=80,
104
+ choices=range(0, 101),
105
+ help="set WEBP quality (0-100, default: '%(default)s')",
106
+ metavar="QUALITY",
107
+ )
108
+
109
+ animate_parser.add_argument(
110
+ "--webp-lossless",
111
+ dest="webp_lossless",
112
+ default=False,
113
+ action="store_true",
114
+ help="enable WEBP lossless compression (default: '%(default)s')",
115
+ )
116
+
117
+ animate_parser.add_argument(
118
+ "--webp-method",
119
+ dest="webp_method",
120
+ type=int,
121
+ default=4,
122
+ choices=range(0, 7),
123
+ help="set WEBP encoding method (0=fast, 6=slow/best, default: '%(default)s')",
124
+ metavar="METHOD",
125
+ )
126
+
127
+ animate_parser.add_argument(
128
+ "-od",
129
+ "--output-dir",
130
+ dest="output_dir",
131
+ default="output",
132
+ help="set default output folder (default: '%(default)s')",
133
+ )
134
+
135
+
136
+ def run(args: argparse.Namespace) -> None:
137
+ """Run animate subcommand.
138
+
139
+ Args:
140
+ args (argparse.Namespace): Config from command line arguments
141
+
142
+ Returns:
143
+ None
144
+ """
145
+ log.debug(args)
146
+
147
+ first_image_filepath = args.image_filenames[0]
148
+ main_frame = None
149
+ other_frames = []
150
+
151
+ try:
152
+ main_frame = Image.open(first_image_filepath)
153
+
154
+ for image_filename in args.image_filenames[1:]:
155
+ img = Image.open(image_filename)
156
+ other_frames.append(img)
157
+
158
+ image_file = Path(first_image_filepath)
159
+ new_filename = Path(
160
+ args.output_dir,
161
+ image_file.with_name(f"animate_{image_file.stem}.{args.format}"),
162
+ )
163
+ new_filename.parent.mkdir(parents=True, exist_ok=True)
164
+
165
+ log.info("animate image: %s", new_filename)
166
+
167
+ save_kwargs = {
168
+ "format": args.format,
169
+ "append_images": other_frames,
170
+ "save_all": True,
171
+ "duration": args.duration,
172
+ "loop": args.loop,
173
+ "optimize": True, # General optimization, good for GIF
174
+ }
175
+
176
+ if args.format == "webp":
177
+ save_kwargs["quality"] = args.webp_quality
178
+ save_kwargs["lossless"] = args.webp_lossless
179
+ save_kwargs["method"] = args.webp_method
180
+ # Pillow's WEBP save doesn't use a general 'optimize' like GIF.
181
+ # Specific WEBP params like 'method' and 'quality' control this.
182
+ # We can remove 'optimize' if it causes issues or is ignored for WEBP.
183
+ # For now, let's keep it, Pillow might handle it or ignore it.
184
+
185
+ main_frame.save(new_filename, **save_kwargs)
186
+ finally:
187
+ if main_frame:
188
+ main_frame.close()
189
+ for frame in other_frames:
190
+ frame.close()
191
+
192
+ if args.open:
193
+ _open_image(new_filename)
@@ -31,7 +31,9 @@ def _validate_cutoff(value: str) -> float:
31
31
  try:
32
32
  f_value = float(value)
33
33
  except ValueError as e:
34
- raise argparse.ArgumentTypeError(f"invalid float value: '{value}'") from e
34
+ raise argparse.ArgumentTypeError(
35
+ f"invalid float value: '{value}'"
36
+ ) from e
35
37
  if not 0 <= f_value <= 50:
36
38
  raise argparse.ArgumentTypeError(
37
39
  f"cutoff value {f_value} must be between 0 and 50"
@@ -141,11 +141,13 @@ def _draw_halftone_dot(
141
141
  x = min(int(cell.col * cell.cellsize + cell.cellsize / 2), img_width - 1)
142
142
  y = min(int(cell.row * cell.cellsize + cell.cellsize / 2), img_height - 1)
143
143
 
144
- # Ensure coordinates are non-negative (shouldn't happen with current logic, but safe)
144
+ # Ensure coordinates are non-negative (shouldn't happen with current logic,
145
+ # but safe)
145
146
  x = max(0, x)
146
147
  y = max(0, y)
147
148
 
148
- # Get pixel value (brightness or color) from the source image using clamped coordinates
149
+ # Get pixel value (brightness or color) from the source image using clamped
150
+ # coordinates
149
151
  pixel_value = source_image.getpixel((x, y))
150
152
 
151
153
  if grayscale:
@@ -160,8 +162,8 @@ def _draw_halftone_dot(
160
162
  )
161
163
  dot_fill = pixel_value # Use original color for color dots
162
164
 
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
+ # Calculate dot radius relative to cell size based on brightness Max radius
166
+ # is half the cell size. Scale by brightness (0-255).
165
167
  dot_radius = (brightness / 255.0) * (cell.cellsize / 2)
166
168
 
167
169
  # Draw the dot
@@ -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,29 +73,22 @@ def run(args: argparse.Namespace) -> None:
74
73
  """
75
74
  log.debug(args)
76
75
 
77
- # TODO: Add error handling for file open
78
- # TODO: Use context manager `with Image.open(...)`
79
- image = Image.open(args.image_filename)
76
+ with Image.open(args.image_filename) as image:
77
+ exif_tags = extract_exif_tags(image, args.sort)
80
78
 
81
- exif_tags = extract_exif_tags(image, args.sort)
79
+ if not exif_tags:
80
+ print("No metadata found!")
81
+ return
82
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 = []
83
+ output_info = []
90
84
  specific_info_requested = False
91
85
 
92
86
  if args.camera:
93
87
  specific_info_requested = True
94
- # TODO: Add error handling for missing keys
95
88
  output_info.append(camera_metadata(exif_tags))
96
89
 
97
90
  if args.datetime:
98
91
  specific_info_requested = True
99
- # TODO: Add error handling for missing keys
100
92
  output_info.append(datetime(exif_tags))
101
93
 
102
94
  if specific_info_requested:
@@ -107,9 +99,6 @@ def run(args: argparse.Namespace) -> None:
107
99
  for tag_name, tag_value in exif_tags.items():
108
100
  print(f"{tag_name:<{tag_name_width}}: {tag_value}")
109
101
 
110
- # Close the image if opened outside a 'with' block
111
- image.close()
112
-
113
102
 
114
103
  def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
115
104
  """Extract Exif metadata from image."""
@@ -135,13 +124,11 @@ def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
135
124
 
136
125
  def datetime(exif_tags: dict):
137
126
  """Extract datetime metadata."""
138
- # TODO: Add error handling for missing key
139
- return exif_tags["DateTime"]
127
+ return exif_tags.get("DateTime", "Not available")
140
128
 
141
129
 
142
130
  def camera_metadata(exif_tags: dict):
143
131
  """Extract camera and model metadata."""
144
- # TODO: Add error handling for missing keys
145
132
  make = exif_tags.get("Make", "")
146
133
  model = exif_tags.get("Model", "")
147
134
  metadata = f"{make} {model}"
@@ -179,6 +179,7 @@ def release(session: nox.Session) -> None:
179
179
  session.run(
180
180
  "git",
181
181
  "commit",
182
+ "--no-verify",
182
183
  "-am",
183
184
  f"Bump {after_version} release",
184
185
  external=True,
@@ -1,131 +0,0 @@
1
- # Copyright (C) 2024,2025 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
- """Animate subcommand."""
17
-
18
- import argparse
19
- import logging
20
- from pathlib import Path
21
-
22
- from PIL import Image
23
-
24
- from fotolab import _open_image
25
-
26
- log = logging.getLogger(__name__)
27
-
28
-
29
- def build_subparser(subparsers) -> None:
30
- """Build the subparser."""
31
- animate_parser = subparsers.add_parser("animate", help="animate an image")
32
-
33
- animate_parser.set_defaults(func=run)
34
-
35
- animate_parser.add_argument(
36
- dest="image_filenames",
37
- help="set the image filenames",
38
- nargs="+",
39
- type=str,
40
- default=None,
41
- metavar="IMAGE_FILENAMES",
42
- )
43
-
44
- animate_parser.add_argument(
45
- "-f",
46
- "--format",
47
- dest="format",
48
- type=str,
49
- choices=["gif", "webp"],
50
- default="gif",
51
- help="set the image format (default: '%(default)s')",
52
- metavar="FORMAT",
53
- )
54
-
55
- animate_parser.add_argument(
56
- "-d",
57
- "--duration",
58
- dest="duration",
59
- type=int,
60
- default=2500,
61
- help="set the duration in milliseconds (default: '%(default)s')",
62
- metavar="DURATION",
63
- )
64
-
65
- animate_parser.add_argument(
66
- "-l",
67
- "--loop",
68
- dest="loop",
69
- type=int,
70
- default=0,
71
- help="set the loop cycle (default: '%(default)s')",
72
- metavar="LOOP",
73
- )
74
-
75
- animate_parser.add_argument(
76
- "-op",
77
- "--open",
78
- default=False,
79
- action="store_true",
80
- dest="open",
81
- help="open the image using default program (default: '%(default)s')",
82
- )
83
-
84
- animate_parser.add_argument(
85
- "-od",
86
- "--output-dir",
87
- dest="output_dir",
88
- default="output",
89
- help="set default output folder (default: '%(default)s')",
90
- )
91
-
92
-
93
- def run(args: argparse.Namespace) -> None:
94
- """Run animate subcommand.
95
-
96
- Args:
97
- args (argparse.Namespace): Config from command line arguments
98
-
99
- Returns:
100
- None
101
- """
102
- log.debug(args)
103
-
104
- first_image = args.image_filenames[0]
105
- animated_image = Image.open(first_image)
106
-
107
- append_images = []
108
- for image_filename in args.image_filenames[1:]:
109
- append_images.append(Image.open(image_filename))
110
-
111
- image_file = Path(first_image)
112
- new_filename = Path(
113
- args.output_dir,
114
- image_file.with_name(f"animate_{image_file.stem}.{args.format}"),
115
- )
116
- new_filename.parent.mkdir(parents=True, exist_ok=True)
117
-
118
- log.info("animate image: %s", new_filename)
119
-
120
- animated_image.save(
121
- new_filename,
122
- format=args.format,
123
- append_images=append_images,
124
- save_all=True,
125
- duration=args.duration,
126
- loop=args.loop,
127
- optimize=True,
128
- )
129
-
130
- if args.open:
131
- _open_image(new_filename)
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