fotolab 0.31.0__py3-none-any.whl → 0.31.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 CHANGED
@@ -24,7 +24,7 @@ from pathlib import Path
24
24
 
25
25
  from PIL import Image
26
26
 
27
- __version__ = "0.31.0"
27
+ __version__ = "0.31.2"
28
28
 
29
29
  log = logging.getLogger(__name__)
30
30
 
@@ -24,8 +24,7 @@ def build_subparser(subparsers):
24
24
  iter_namespace = pkgutil.iter_modules(__path__, __name__ + ".")
25
25
 
26
26
  subcommands = {
27
- name: importlib.import_module(name)
28
- for finder, name, ispkg in iter_namespace
27
+ name: importlib.import_module(name) for finder, name, ispkg in iter_namespace
29
28
  }
30
29
 
31
30
  for subcommand in subcommands.values():
@@ -73,7 +73,10 @@ def build_subparser(subparsers) -> None:
73
73
  dest="duration",
74
74
  type=_validate_duration,
75
75
  default=2500,
76
- help="set the duration in milliseconds (must be a positive integer, default: '%(default)s')",
76
+ help=(
77
+ "set the duration in milliseconds "
78
+ "(must be a positive integer, default: '%(default)s')"
79
+ ),
77
80
  metavar="DURATION",
78
81
  )
79
82
 
@@ -120,7 +123,7 @@ def build_subparser(subparsers) -> None:
120
123
  type=int,
121
124
  default=4,
122
125
  choices=range(0, 7),
123
- help="set WEBP encoding method (0=fast, 6=slow/best, default: '%(default)s')",
126
+ help=("set WEBP encoding method (0=fast, 6=slow/best, default: '%(default)s')"),
124
127
  metavar="METHOD",
125
128
  )
126
129
 
@@ -170,17 +173,17 @@ def run(args: argparse.Namespace) -> None:
170
173
  "save_all": True,
171
174
  "duration": args.duration,
172
175
  "loop": args.loop,
173
- "optimize": True, # General optimization, good for GIF
176
+ "optimize": True,
174
177
  }
175
178
 
179
+ # Pillow's WEBP save doesn't use a general 'optimize' like GIF.
180
+ # Specific WEBP params like 'method' and 'quality' control this. We
181
+ # can remove 'optimize' if it causes issues or is ignored for WEBP.
182
+ # For now, let's keep it, Pillow might handle it or ignore it.
176
183
  if args.format == "webp":
177
184
  save_kwargs["quality"] = args.webp_quality
178
185
  save_kwargs["lossless"] = args.webp_lossless
179
186
  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
187
 
185
188
  main_frame.save(new_filename, **save_kwargs)
186
189
  finally:
@@ -71,6 +71,7 @@ def run(args: argparse.Namespace) -> None:
71
71
  "canvas": False,
72
72
  "lowercase": False,
73
73
  "before_after": False,
74
+ "alpha": 128,
74
75
  }
75
76
  combined_args = argparse.Namespace(**vars(args), **extra_args)
76
77
  combined_args.overwrite = True
@@ -17,7 +17,7 @@
17
17
 
18
18
  import argparse
19
19
  import logging
20
- from typing import Tuple, Union
20
+ from typing import Tuple
21
21
 
22
22
  from PIL import Image, ImageColor, ImageOps
23
23
 
@@ -77,10 +77,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
77
77
  dest="width_right",
78
78
  type=int,
79
79
  default=0,
80
- help=(
81
- "set the width of right border in pixels"
82
- " (default: '%(default)s')"
83
- ),
80
+ help=("set the width of right border in pixels (default: '%(default)s')"),
84
81
  metavar="WIDTH",
85
82
  )
86
83
 
@@ -90,10 +87,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
90
87
  dest="width_bottom",
91
88
  type=int,
92
89
  default=0,
93
- help=(
94
- "set the width of bottom border in pixels"
95
- " (default: '%(default)s')"
96
- ),
90
+ help=("set the width of bottom border in pixels (default: '%(default)s')"),
97
91
  metavar="WIDTH",
98
92
  )
99
93
 
@@ -31,9 +31,7 @@ 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(
35
- f"invalid float value: '{value}'"
36
- ) from e
34
+ raise argparse.ArgumentTypeError(f"invalid float value: '{value}'") from e
37
35
  if not 0 <= f_value <= 50:
38
36
  raise argparse.ArgumentTypeError(
39
37
  f"cutoff value {f_value} must be between 0 and 50"
@@ -43,9 +41,7 @@ def _validate_cutoff(value: str) -> float:
43
41
 
44
42
  def build_subparser(subparsers: argparse._SubParsersAction) -> None:
45
43
  """Build the subparser."""
46
- contrast_parser = subparsers.add_parser(
47
- "contrast", help="contrast an image."
48
- )
44
+ contrast_parser = subparsers.add_parser("contrast", help="contrast an image.")
49
45
 
50
46
  contrast_parser.set_defaults(func=run)
51
47
 
@@ -103,8 +99,6 @@ def run(args: argparse.Namespace) -> None:
103
99
 
104
100
  for image_filename in args.image_filenames:
105
101
  original_image = Image.open(image_filename)
106
- contrast_image = ImageOps.autocontrast(
107
- original_image, cutoff=args.cutoff
108
- )
102
+ contrast_image = ImageOps.autocontrast(original_image, cutoff=args.cutoff)
109
103
 
110
104
  save_image(args, contrast_image, image_filename, "contrast")
@@ -37,9 +37,7 @@ class HalftoneCell(NamedTuple):
37
37
 
38
38
  def build_subparser(subparsers) -> None:
39
39
  """Build the subparser."""
40
- halftone_parser = subparsers.add_parser(
41
- "halftone", help="halftone an image"
42
- )
40
+ halftone_parser = subparsers.add_parser("halftone", help="halftone an image")
43
41
 
44
42
  halftone_parser.set_defaults(func=run)
45
43
 
@@ -156,9 +154,7 @@ def _draw_halftone_dot(
156
154
  else:
157
155
  # Calculate brightness (luminance) from the RGB color
158
156
  brightness = int(
159
- 0.299 * pixel_value[0]
160
- + 0.587 * pixel_value[1]
161
- + 0.114 * pixel_value[2]
157
+ 0.299 * pixel_value[0] + 0.587 * pixel_value[1] + 0.114 * pixel_value[2]
162
158
  )
163
159
  dot_fill = pixel_value # Use original color for color dots
164
160
 
@@ -81,31 +81,25 @@ def run(args: argparse.Namespace) -> None:
81
81
  return
82
82
 
83
83
  output_info = []
84
- specific_info_requested = False
85
84
 
86
- if args.camera:
87
- specific_info_requested = True
88
- output_info.append(camera_metadata(exif_tags))
85
+ if args.camera:
86
+ output_info.append(get_formatted_camera_info(exif_tags))
89
87
 
90
- if args.datetime:
91
- specific_info_requested = True
92
- output_info.append(datetime(exif_tags))
88
+ if args.datetime:
89
+ output_info.append(get_formatted_datetime_info(exif_tags))
93
90
 
94
- if specific_info_requested:
95
- print("\n".join(output_info))
96
- else:
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}")
91
+ if output_info: # Check if any specific info was added
92
+ print("\n".join(output_info))
93
+ else:
94
+ # Print all tags if no specific info was requested
95
+ tag_name_width = max(map(len, exif_tags))
96
+ for tag_name, tag_value in exif_tags.items():
97
+ print(f"{tag_name:<{tag_name_width}}: {tag_value}")
101
98
 
102
99
 
103
100
  def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
104
101
  """Extract Exif metadata from image."""
105
- try:
106
- exif = image._getexif()
107
- except AttributeError:
108
- exif = None
102
+ exif = image.getexif()
109
103
 
110
104
  log.debug(exif)
111
105
 
@@ -113,22 +107,20 @@ def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
113
107
  if exif:
114
108
  info = {ExifTags.TAGS.get(tag_id): exif.get(tag_id) for tag_id in exif}
115
109
 
116
- filtered_info = {
117
- key: value for key, value in info.items() if key is not None
118
- }
110
+ filtered_info = {key: value for key, value in info.items() if key is not None}
119
111
  if sort:
120
112
  filtered_info = dict(sorted(filtered_info.items()))
121
113
 
122
114
  return filtered_info
123
115
 
124
116
 
125
- def datetime(exif_tags: dict):
126
- """Extract datetime metadata."""
117
+ def get_formatted_datetime_info(exif_tags: dict):
118
+ """Extract and format datetime metadata."""
127
119
  return exif_tags.get("DateTime", "Not available")
128
120
 
129
121
 
130
- def camera_metadata(exif_tags: dict):
131
- """Extract camera and model metadata."""
122
+ def get_formatted_camera_info(exif_tags: dict):
123
+ """Extract and format camera make and model metadata."""
132
124
  make = exif_tags.get("Make", "")
133
125
  model = exif_tags.get("Model", "")
134
126
  metadata = f"{make} {model}"
@@ -27,9 +27,7 @@ log = logging.getLogger(__name__)
27
27
 
28
28
  def build_subparser(subparsers) -> None:
29
29
  """Build the subparser."""
30
- montage_parser = subparsers.add_parser(
31
- "montage", help="montage a list of image"
32
- )
30
+ montage_parser = subparsers.add_parser("montage", help="montage a list of image")
33
31
 
34
32
  montage_parser.set_defaults(func=run)
35
33
 
@@ -59,10 +59,7 @@ def build_subparser(subparsers) -> None:
59
59
  "--canvas-color",
60
60
  default="black",
61
61
  dest="canvas_color",
62
- help=(
63
- "the color of the extended larger canvas"
64
- "(default: '%(default)s')"
65
- ),
62
+ help=("the color of the extended larger canvas(default: '%(default)s')"),
66
63
  )
67
64
 
68
65
  if "-c" in sys.argv or "--canvas" in sys.argv:
@@ -24,7 +24,7 @@ from typing import Tuple
24
24
  from PIL import Image, ImageColor, ImageDraw, ImageFont, ImageSequence
25
25
 
26
26
  from fotolab import save_image
27
- from fotolab.subcommands.info import camera_metadata
27
+ from fotolab.subcommands.info import get_formatted_camera_info
28
28
 
29
29
  log: logging.Logger = logging.getLogger(__name__)
30
30
 
@@ -109,10 +109,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
109
109
  dest="outline_width",
110
110
  type=int,
111
111
  default=2,
112
- help=(
113
- "set the outline width of the watermark text "
114
- "(default: '%(default)s')"
115
- ),
112
+ help=("set the outline width of the watermark text (default: '%(default)s')"),
116
113
  metavar="OUTLINE_WIDTH",
117
114
  )
118
115
 
@@ -122,10 +119,7 @@ def build_subparser(subparsers: argparse._SubParsersAction) -> None:
122
119
  dest="outline_color",
123
120
  type=str,
124
121
  default="black",
125
- help=(
126
- "set the outline color of the watermark text "
127
- "(default: '%(default)s')"
128
- ),
122
+ help=("set the outline color of the watermark text (default: '%(default)s')"),
129
123
  metavar="OUTLINE_COLOR",
130
124
  )
131
125
 
@@ -203,9 +197,7 @@ def run(args: argparse.Namespace) -> None:
203
197
  if image.format == "GIF":
204
198
  watermark_gif_image(image, image_filename, args)
205
199
  else:
206
- watermarked_image: Image.Image = watermark_non_gif_image(
207
- image, args
208
- )
200
+ watermarked_image: Image.Image = watermark_non_gif_image(image, args)
209
201
  save_image(args, watermarked_image, image_filename, "watermark")
210
202
 
211
203
 
@@ -312,10 +304,10 @@ def watermark_image(
312
304
 
313
305
  def prepare_text(args: argparse.Namespace, image: Image.Image) -> str:
314
306
  """Prepare the watermark text."""
315
- text: str = args.text # Default text
307
+ text = args.text
316
308
  if args.camera:
317
- metadata_text: str | None = camera_metadata(image)
318
- if metadata_text: # Use metadata only if it's not None or empty
309
+ metadata_text = get_formatted_camera_info(image)
310
+ if metadata_text:
319
311
  text = metadata_text
320
312
  else:
321
313
  log.warning(
@@ -341,16 +333,12 @@ def calc_font_size(image: Image.Image, args: argparse.Namespace) -> int:
341
333
  return new_font_size
342
334
 
343
335
 
344
- def calc_font_outline_width(
345
- image: Image.Image, args: argparse.Namespace
346
- ) -> int:
336
+ def calc_font_outline_width(image: Image.Image, args: argparse.Namespace) -> int:
347
337
  """Calculate the font padding based on the width of the image."""
348
338
  width, _height = image.size
349
339
  new_font_outline_width: int = args.outline_width
350
340
  if width > 600:
351
- new_font_outline_width = math.floor(
352
- FONT_OUTLINE_WIDTH_ASPECT_RATIO * width
353
- )
341
+ new_font_outline_width = math.floor(FONT_OUTLINE_WIDTH_ASPECT_RATIO * width)
354
342
 
355
343
  log.debug("new font outline width: %d", new_font_outline_width)
356
344
  return new_font_outline_width
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fotolab
3
- Version: 0.31.0
3
+ Version: 0.31.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>
@@ -371,7 +371,8 @@ fotolab watermark -h
371
371
  usage: fotolab watermark [-h] [-t WATERMARK_TEXT]
372
372
  [-p {top-left,top-right,bottom-left,bottom-right}]
373
373
  [-pd PADDING] [-fs FONT_SIZE] [-fc FONT_COLOR]
374
- [-ow OUTLINE_WIDTH] [-oc OUTLINE_COLOR] [--camera]
374
+ [-ow OUTLINE_WIDTH] [-oc OUTLINE_COLOR]
375
+ [-a ALPHA_VALUE] [--camera]
375
376
  [-l | --lowercase | --no-lowercase] [-op]
376
377
  [-od OUTPUT_DIR]
377
378
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
@@ -400,6 +401,10 @@ options:
400
401
  -oc, --outline-color OUTLINE_COLOR
401
402
  set the outline color of the watermark text (default:
402
403
  'black')
404
+ -a, --alpha ALPHA_VALUE
405
+ set the transparency of the watermark text (0-255,
406
+ where 0 is fully transparent and 255 is fully opaque;
407
+ default: '128')
403
408
  --camera use camera metadata as watermark
404
409
  -l, --lowercase, --no-lowercase
405
410
  lowercase the watermark text
@@ -0,0 +1,21 @@
1
+ fotolab/__init__.py,sha256=YnbcUmuDk3jXO4_lS-2boxLjzR1NHqL68fluo1CRt94,3087
2
+ fotolab/__main__.py,sha256=aboOURPs_snOXTEWYR0q8oq1UTY9e-NxCd1j33V0wHI,833
3
+ fotolab/cli.py,sha256=oFiQXmsu3wIsM_DpZnL4B94sAoB62L16Am-cjxGmosY,4406
4
+ fotolab/subcommands/__init__.py,sha256=zPNE3lTK3yNjBq2zP1GiL13R4ywO_zh5MRFp5aOFp_g,1121
5
+ fotolab/subcommands/animate.py,sha256=Bzp1Y708UZogcLFwZSoJBfLAxLNZ5c0PESp1xl03sQg,5512
6
+ fotolab/subcommands/auto.py,sha256=3Hl1cCdu65GcfwvwiMRAcex6c7zA-KqIY6AFnB6nI3w,2447
7
+ fotolab/subcommands/border.py,sha256=3mD6ebdPjGk39ldUq9766jEXCbszPlbyV48k13dmrSA,4621
8
+ fotolab/subcommands/contrast.py,sha256=k-NGncYgm7x821CP-NhDbyZGzW1FPxaDgJi00xd97kw,2969
9
+ fotolab/subcommands/env.py,sha256=QoxRvzZKgmoHTUxDV4QYhdChCpMWs5TbXFY_qIpIQpE,1469
10
+ fotolab/subcommands/halftone.py,sha256=4YpHkp3zSYp36qJVPinWBOAUhTJjc-aGxiaNd1IkhXA,6655
11
+ fotolab/subcommands/info.py,sha256=pT2NBZ-KBjBCJJRpC9HC04iZj3s6RhbLWTRG6vjsarg,3549
12
+ fotolab/subcommands/montage.py,sha256=6-3VFMOQm3KOhcGaDfzStTKvBB5GHrotFkHU9kxZe5E,2552
13
+ fotolab/subcommands/resize.py,sha256=h4t425Xf95lkh6Trp-qw_cu0bBgMgBSBvJvUIR4dqiE,5407
14
+ fotolab/subcommands/rotate.py,sha256=uBFjHyjiBSQLtrtH1p9myODIHUDr1gkL4PpU-6Y1Ofo,2575
15
+ fotolab/subcommands/sharpen.py,sha256=YNho2IPbc-lPvSy3Bsjehc2JOEy27LPqFSGRULs9MyY,3492
16
+ fotolab/subcommands/watermark.py,sha256=uVcFynGt2hkTNF5RnfdHhBxSoLmlA7o8-_I65pe_di0,11166
17
+ fotolab-0.31.2.dist-info/entry_points.txt,sha256=mvw7AY_yZkIyjAxPtHNed9X99NZeLnMxEeAfEJUbrCM,44
18
+ fotolab-0.31.2.dist-info/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
19
+ fotolab-0.31.2.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
20
+ fotolab-0.31.2.dist-info/METADATA,sha256=D6VMZWUIsEYEuuPU7YcAJGUs3eOl3LhAmgKctMYSqd8,13656
21
+ fotolab-0.31.2.dist-info/RECORD,,
@@ -1,21 +0,0 @@
1
- fotolab/__init__.py,sha256=TKTFXeMPGcnb4yYlrnCjAsghD_bbcPvtFrlRNRWVX6U,3087
2
- fotolab/__main__.py,sha256=aboOURPs_snOXTEWYR0q8oq1UTY9e-NxCd1j33V0wHI,833
3
- fotolab/cli.py,sha256=oFiQXmsu3wIsM_DpZnL4B94sAoB62L16Am-cjxGmosY,4406
4
- fotolab/subcommands/__init__.py,sha256=l3DlIaJ3u3jGjnC1H1yV8LZ_nPqOLJ6gikD4BCaMAQ0,1129
5
- fotolab/subcommands/animate.py,sha256=vmviz3cLnHfVENxFKiTimhx8nmbGbzumOP6dUd_UiUI,5524
6
- fotolab/subcommands/auto.py,sha256=ia-xegV1Z4HvYsbKgmTzf1NfNFdTDPWfZe7vQ1_90Ik,2425
7
- fotolab/subcommands/border.py,sha256=3RbTAMyKZRRTWKXHHsM584_Z6qzl5_leQn-U9DD4dCk,4702
8
- fotolab/subcommands/contrast.py,sha256=fcXmHnxDw74j5ZUDQ5cwWh0N4tpyqqvEjymnpITgrEk,3027
9
- fotolab/subcommands/env.py,sha256=QoxRvzZKgmoHTUxDV4QYhdChCpMWs5TbXFY_qIpIQpE,1469
10
- fotolab/subcommands/halftone.py,sha256=lt6RV0OuZkGs1LigTC1EcCCY42CocPFHWaJTDjJ5LFM,6693
11
- fotolab/subcommands/info.py,sha256=vie578cEOesAyHohm0xCWwZBqXpblYJtdHGSyBgxsoU,3581
12
- fotolab/subcommands/montage.py,sha256=d_3EcyRSFS8fKkczlHO8IRP-mrKhQUtkQndjfd0MKsc,2566
13
- fotolab/subcommands/resize.py,sha256=UOb2rg_5ArRj0gxPTDOww_ix3tqJRC0W5b_S-Lt1G4w,5444
14
- fotolab/subcommands/rotate.py,sha256=uBFjHyjiBSQLtrtH1p9myODIHUDr1gkL4PpU-6Y1Ofo,2575
15
- fotolab/subcommands/sharpen.py,sha256=YNho2IPbc-lPvSy3Bsjehc2JOEy27LPqFSGRULs9MyY,3492
16
- fotolab/subcommands/watermark.py,sha256=_l7u-l1D6LWd5G8B1l1XpUVw2HCyacjloWX2hOSRb7c,11358
17
- fotolab-0.31.0.dist-info/entry_points.txt,sha256=mvw7AY_yZkIyjAxPtHNed9X99NZeLnMxEeAfEJUbrCM,44
18
- fotolab-0.31.0.dist-info/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
19
- fotolab-0.31.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
20
- fotolab-0.31.0.dist-info/METADATA,sha256=T9NbfLt9R31dDV8BaBeHVM9aBWH-xNyfo0CsF7llIAI,13395
21
- fotolab-0.31.0.dist-info/RECORD,,