fotolab 0.22.0__py3-none-any.whl → 0.25.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 +2 -2
 - fotolab/cli.py +27 -43
 - fotolab/subcommands/__init__.py +32 -0
 - fotolab/{animate.py → subcommands/animate.py} +1 -1
 - fotolab/{auto.py → subcommands/auto.py} +10 -9
 - fotolab/{border.py → subcommands/border.py} +20 -17
 - fotolab/{contrast.py → subcommands/contrast.py} +1 -1
 - fotolab/{env.py → subcommands/env.py} +4 -4
 - fotolab/{info.py → subcommands/info.py} +17 -10
 - fotolab/{montage.py → subcommands/montage.py} +4 -4
 - fotolab/{resize.py → subcommands/resize.py} +1 -1
 - fotolab/{rotate.py → subcommands/rotate.py} +21 -2
 - fotolab/{sharpen.py → subcommands/sharpen.py} +40 -3
 - fotolab/{watermark.py → subcommands/watermark.py} +79 -25
 - {fotolab-0.22.0.dist-info → fotolab-0.25.1.dist-info}/METADATA +17 -11
 - fotolab-0.25.1.dist-info/RECORD +20 -0
 - fotolab-0.22.0.dist-info/RECORD +0 -19
 - {fotolab-0.22.0.dist-info → fotolab-0.25.1.dist-info}/LICENSE.md +0 -0
 - {fotolab-0.22.0.dist-info → fotolab-0.25.1.dist-info}/WHEEL +0 -0
 - {fotolab-0.22.0.dist-info → fotolab-0.25.1.dist-info}/entry_points.txt +0 -0
 
    
        fotolab/__init__.py
    CHANGED
    
    | 
         @@ -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.1"
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
            log = logging.getLogger(__name__)
         
     | 
| 
       27 
27 
     | 
    
         | 
    
        fotolab/cli.py
    CHANGED
    
    | 
         @@ -1,4 +1,4 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            # Copyright ( 
     | 
| 
      
 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
         
     | 
| 
         @@ -23,46 +23,40 @@ 
     | 
|
| 
       23 
23 
     | 
    
         
             
            import argparse
         
     | 
| 
       24 
24 
     | 
    
         
             
            import logging
         
     | 
| 
       25 
25 
     | 
    
         
             
            import sys
         
     | 
| 
       26 
     | 
    
         
            -
            from typing import  
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
            import fotolab. 
     | 
| 
       29 
     | 
    
         
            -
            import fotolab.auto
         
     | 
| 
       30 
     | 
    
         
            -
            import fotolab.border
         
     | 
| 
       31 
     | 
    
         
            -
            import fotolab.contrast
         
     | 
| 
       32 
     | 
    
         
            -
            import fotolab.env
         
     | 
| 
       33 
     | 
    
         
            -
            import fotolab.info
         
     | 
| 
       34 
     | 
    
         
            -
            import fotolab.montage
         
     | 
| 
       35 
     | 
    
         
            -
            import fotolab.resize
         
     | 
| 
       36 
     | 
    
         
            -
            import fotolab.rotate
         
     | 
| 
       37 
     | 
    
         
            -
            import fotolab.sharpen
         
     | 
| 
       38 
     | 
    
         
            -
            import fotolab.watermark
         
     | 
| 
      
 26 
     | 
    
         
            +
            from typing import Optional, Sequence
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
            import fotolab.subcommands
         
     | 
| 
       39 
29 
     | 
    
         
             
            from fotolab import __version__
         
     | 
| 
       40 
30 
     | 
    
         | 
| 
       41 
31 
     | 
    
         
             
            log = logging.getLogger(__name__)
         
     | 
| 
       42 
32 
     | 
    
         | 
| 
       43 
33 
     | 
    
         | 
| 
       44 
34 
     | 
    
         
             
            def setup_logging(args: argparse.Namespace) -> None:
         
     | 
| 
       45 
     | 
    
         
            -
                """ 
     | 
| 
       46 
     | 
    
         
            -
                if args.verbose == 0:
         
     | 
| 
       47 
     | 
    
         
            -
                    logging.getLogger("PIL").setLevel(logging.ERROR)
         
     | 
| 
      
 35 
     | 
    
         
            +
                """Sets up logging configuration based on command-line arguments.
         
     | 
| 
       48 
36 
     | 
    
         | 
| 
      
 37 
     | 
    
         
            +
                Args:
         
     | 
| 
      
 38 
     | 
    
         
            +
                    args (argparse.Namespace): Namespace containing parsed arguments.
         
     | 
| 
      
 39 
     | 
    
         
            +
                """
         
     | 
| 
       49 
40 
     | 
    
         
             
                if args.quiet:
         
     | 
| 
       50 
41 
     | 
    
         
             
                    logging.disable(logging.NOTSET)
         
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
       56 
     | 
    
         
            -
             
     | 
| 
       57 
     | 
    
         
            -
             
     | 
| 
       58 
     | 
    
         
            -
                     
     | 
| 
       59 
     | 
    
         
            -
             
     | 
| 
       60 
     | 
    
         
            -
                     
     | 
| 
       61 
     | 
    
         
            -
             
     | 
| 
       62 
     | 
    
         
            -
             
     | 
| 
       63 
     | 
    
         
            -
             
     | 
| 
       64 
     | 
    
         
            -
             
     | 
| 
       65 
     | 
    
         
            -
                     
     | 
| 
      
 42 
     | 
    
         
            +
                    return
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                if args.verbose == 0:
         
     | 
| 
      
 45 
     | 
    
         
            +
                    logging.getLogger("PIL").setLevel(logging.ERROR)
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                level = logging.DEBUG if args.debug else logging.INFO
         
     | 
| 
      
 48 
     | 
    
         
            +
                format_string = (
         
     | 
| 
      
 49 
     | 
    
         
            +
                    "[%(asctime)s] %(levelname)s: %(name)s: %(message)s"
         
     | 
| 
      
 50 
     | 
    
         
            +
                    if args.debug
         
     | 
| 
      
 51 
     | 
    
         
            +
                    else "%(message)s"
         
     | 
| 
      
 52 
     | 
    
         
            +
                )
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                logging.basicConfig(
         
     | 
| 
      
 55 
     | 
    
         
            +
                    level=level,
         
     | 
| 
      
 56 
     | 
    
         
            +
                    format=format_string,
         
     | 
| 
      
 57 
     | 
    
         
            +
                    stream=sys.stdout,
         
     | 
| 
      
 58 
     | 
    
         
            +
                    datefmt="%Y-%m-%d %H:%M:%S",
         
     | 
| 
      
 59 
     | 
    
         
            +
                )
         
     | 
| 
       66 
60 
     | 
    
         | 
| 
       67 
61 
     | 
    
         | 
| 
       68 
62 
     | 
    
         
             
            def build_parser() -> argparse.ArgumentParser:
         
     | 
| 
         @@ -136,17 +130,7 @@ def build_parser() -> argparse.ArgumentParser: 
     | 
|
| 
       136 
130 
     | 
    
         
             
                )
         
     | 
| 
       137 
131 
     | 
    
         | 
| 
       138 
132 
     | 
    
         
             
                subparsers = parser.add_subparsers(help="sub-command help")
         
     | 
| 
       139 
     | 
    
         
            -
                fotolab. 
     | 
| 
       140 
     | 
    
         
            -
                fotolab.auto.build_subparser(subparsers)
         
     | 
| 
       141 
     | 
    
         
            -
                fotolab.border.build_subparser(subparsers)
         
     | 
| 
       142 
     | 
    
         
            -
                fotolab.contrast.build_subparser(subparsers)
         
     | 
| 
       143 
     | 
    
         
            -
                fotolab.info.build_subparser(subparsers)
         
     | 
| 
       144 
     | 
    
         
            -
                fotolab.resize.build_subparser(subparsers)
         
     | 
| 
       145 
     | 
    
         
            -
                fotolab.rotate.build_subparser(subparsers)
         
     | 
| 
       146 
     | 
    
         
            -
                fotolab.montage.build_subparser(subparsers)
         
     | 
| 
       147 
     | 
    
         
            -
                fotolab.sharpen.build_subparser(subparsers)
         
     | 
| 
       148 
     | 
    
         
            -
                fotolab.watermark.build_subparser(subparsers)
         
     | 
| 
       149 
     | 
    
         
            -
                fotolab.env.build_subparser(subparsers)
         
     | 
| 
      
 133 
     | 
    
         
            +
                fotolab.subcommands.build_subparser(subparsers)
         
     | 
| 
       150 
134 
     | 
    
         | 
| 
       151 
135 
     | 
    
         
             
                return parser
         
     | 
| 
       152 
136 
     | 
    
         | 
| 
         @@ -0,0 +1,32 @@ 
     | 
|
| 
      
 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 
     | 
    
         
            +
            """Common utils for subcommand."""
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            import importlib
         
     | 
| 
      
 19 
     | 
    
         
            +
            import pkgutil
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            def build_subparser(subparsers):
         
     | 
| 
      
 23 
     | 
    
         
            +
                """Build subparser for each subcommands."""
         
     | 
| 
      
 24 
     | 
    
         
            +
                iter_namespace = pkgutil.iter_modules(__path__, __name__ + ".")
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                subcommands = {
         
     | 
| 
      
 27 
     | 
    
         
            +
                    name: importlib.import_module(name)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    for finder, name, ispkg in iter_namespace
         
     | 
| 
      
 29 
     | 
    
         
            +
                }
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                for subcommand in subcommands.values():
         
     | 
| 
      
 32 
     | 
    
         
            +
                    subcommand.build_subparser(subparsers)
         
     | 
| 
         @@ -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
         
     | 
| 
         @@ -18,10 +18,10 @@ 
     | 
|
| 
       18 
18 
     | 
    
         
             
            import argparse
         
     | 
| 
       19 
19 
     | 
    
         
             
            import logging
         
     | 
| 
       20 
20 
     | 
    
         | 
| 
       21 
     | 
    
         
            -
            import fotolab.contrast
         
     | 
| 
       22 
     | 
    
         
            -
            import fotolab.resize
         
     | 
| 
       23 
     | 
    
         
            -
            import fotolab.sharpen
         
     | 
| 
       24 
     | 
    
         
            -
            import fotolab.watermark
         
     | 
| 
      
 21 
     | 
    
         
            +
            import fotolab.subcommands.contrast
         
     | 
| 
      
 22 
     | 
    
         
            +
            import fotolab.subcommands.resize
         
     | 
| 
      
 23 
     | 
    
         
            +
            import fotolab.subcommands.sharpen
         
     | 
| 
      
 24 
     | 
    
         
            +
            import fotolab.subcommands.watermark
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
            log = logging.getLogger(__name__)
         
     | 
| 
       27 
27 
     | 
    
         | 
| 
         @@ -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
         
     | 
| 
         @@ -77,7 +78,7 @@ def run(args: argparse.Namespace) -> None: 
     | 
|
| 
       77 
78 
     | 
    
         
             
                log.debug(args)
         
     | 
| 
       78 
79 
     | 
    
         
             
                log.debug(combined_args)
         
     | 
| 
       79 
80 
     | 
    
         | 
| 
       80 
     | 
    
         
            -
                fotolab.resize.run(combined_args)
         
     | 
| 
       81 
     | 
    
         
            -
                fotolab.contrast.run(combined_args)
         
     | 
| 
       82 
     | 
    
         
            -
                fotolab.sharpen.run(combined_args)
         
     | 
| 
       83 
     | 
    
         
            -
                fotolab.watermark.run(combined_args)
         
     | 
| 
      
 81 
     | 
    
         
            +
                fotolab.subcommands.resize.run(combined_args)
         
     | 
| 
      
 82 
     | 
    
         
            +
                fotolab.subcommands.contrast.run(combined_args)
         
     | 
| 
      
 83 
     | 
    
         
            +
                fotolab.subcommands.sharpen.run(combined_args)
         
     | 
| 
      
 84 
     | 
    
         
            +
                fotolab.subcommands.watermark.run(combined_args)
         
     | 
| 
         @@ -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
         
     | 
| 
         @@ -115,25 +115,28 @@ def run(args: argparse.Namespace) -> None: 
     | 
|
| 
       115 
115 
     | 
    
         
             
                for image_filename in args.image_filenames:
         
     | 
| 
       116 
116 
     | 
    
         
             
                    original_image = Image.open(image_filename)
         
     | 
| 
       117 
117 
     | 
    
         | 
| 
       118 
     | 
    
         
            -
                     
     | 
| 
       119 
     | 
    
         
            -
                        args.width_left
         
     | 
| 
       120 
     | 
    
         
            -
                        or args.width_top
         
     | 
| 
       121 
     | 
    
         
            -
                        or args.width_right
         
     | 
| 
       122 
     | 
    
         
            -
                        or args.width_bottom
         
     | 
| 
       123 
     | 
    
         
            -
                    ):
         
     | 
| 
       124 
     | 
    
         
            -
                        border = (
         
     | 
| 
       125 
     | 
    
         
            -
                            int(args.width_left),
         
     | 
| 
       126 
     | 
    
         
            -
                            int(args.width_top),
         
     | 
| 
       127 
     | 
    
         
            -
                            int(args.width_right),
         
     | 
| 
       128 
     | 
    
         
            -
                            int(args.width_bottom),
         
     | 
| 
       129 
     | 
    
         
            -
                        )
         
     | 
| 
       130 
     | 
    
         
            -
                    else:
         
     | 
| 
       131 
     | 
    
         
            -
                        border = args.width
         
     | 
| 
       132 
     | 
    
         
            -
             
     | 
| 
      
 118 
     | 
    
         
            +
                    border = get_border(args)
         
     | 
| 
       133 
119 
     | 
    
         
             
                    bordered_image = ImageOps.expand(
         
     | 
| 
       134 
120 
     | 
    
         
             
                        original_image,
         
     | 
| 
       135 
121 
     | 
    
         
             
                        border=border,
         
     | 
| 
       136 
122 
     | 
    
         
             
                        fill=ImageColor.getrgb(args.color),
         
     | 
| 
       137 
123 
     | 
    
         
             
                    )
         
     | 
| 
       138 
124 
     | 
    
         | 
| 
       139 
     | 
    
         
            -
                    save_image(args, bordered_image, image_filename, " 
     | 
| 
      
 125 
     | 
    
         
            +
                    save_image(args, bordered_image, image_filename, "border")
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
            def get_border(args: argparse.Namespace) -> tuple:
         
     | 
| 
      
 129 
     | 
    
         
            +
                """Calculate the border."""
         
     | 
| 
      
 130 
     | 
    
         
            +
                if (
         
     | 
| 
      
 131 
     | 
    
         
            +
                    args.width_left
         
     | 
| 
      
 132 
     | 
    
         
            +
                    or args.width_top
         
     | 
| 
      
 133 
     | 
    
         
            +
                    or args.width_right
         
     | 
| 
      
 134 
     | 
    
         
            +
                    or args.width_bottom
         
     | 
| 
      
 135 
     | 
    
         
            +
                ):
         
     | 
| 
      
 136 
     | 
    
         
            +
                    return (
         
     | 
| 
      
 137 
     | 
    
         
            +
                        int(args.width_left),
         
     | 
| 
      
 138 
     | 
    
         
            +
                        int(args.width_top),
         
     | 
| 
      
 139 
     | 
    
         
            +
                        int(args.width_right),
         
     | 
| 
      
 140 
     | 
    
         
            +
                        int(args.width_bottom),
         
     | 
| 
      
 141 
     | 
    
         
            +
                    )
         
     | 
| 
      
 142 
     | 
    
         
            +
                return args.width
         
     | 
| 
         @@ -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,12 +73,20 @@ def run(args: argparse.Namespace) -> None: 
     | 
|
| 
       73 
73 
     | 
    
         
             
                    None
         
     | 
| 
       74 
74 
     | 
    
         
             
                """
         
     | 
| 
       75 
75 
     | 
    
         
             
                log.debug(args)
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                info = []
         
     | 
| 
      
 78 
     | 
    
         
            +
                image = Image.open(args.image_filename)
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
       76 
80 
     | 
    
         
             
                if args.camera:
         
     | 
| 
       77 
     | 
    
         
            -
                     
     | 
| 
      
 81 
     | 
    
         
            +
                    info.append(camera_metadata(image))
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
       78 
83 
     | 
    
         
             
                if args.datetime:
         
     | 
| 
       79 
     | 
    
         
            -
                     
     | 
| 
      
 84 
     | 
    
         
            +
                    info.append(datetime(image))
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                if info:
         
     | 
| 
      
 87 
     | 
    
         
            +
                    print("\n".join(info))
         
     | 
| 
       80 
88 
     | 
    
         
             
                else:
         
     | 
| 
       81 
     | 
    
         
            -
                    exif_tags = extract_exif_tags( 
     | 
| 
      
 89 
     | 
    
         
            +
                    exif_tags = extract_exif_tags(image)
         
     | 
| 
       82 
90 
     | 
    
         
             
                    if exif_tags:
         
     | 
| 
       83 
91 
     | 
    
         
             
                        tag_name_width = max(map(len, exif_tags))
         
     | 
| 
       84 
92 
     | 
    
         
             
                        for tag_name, tag_value in exif_tags.items():
         
     | 
| 
         @@ -87,9 +95,8 @@ def run(args: argparse.Namespace) -> None: 
     | 
|
| 
       87 
95 
     | 
    
         
             
                        print("No metadata found!")
         
     | 
| 
       88 
96 
     | 
    
         | 
| 
       89 
97 
     | 
    
         | 
| 
       90 
     | 
    
         
            -
            def extract_exif_tags( 
     | 
| 
      
 98 
     | 
    
         
            +
            def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
         
     | 
| 
       91 
99 
     | 
    
         
             
                """Extract Exif metadata from image."""
         
     | 
| 
       92 
     | 
    
         
            -
                image = Image.open(image_filename)
         
     | 
| 
       93 
100 
     | 
    
         
             
                exif = image._getexif()
         
     | 
| 
       94 
101 
     | 
    
         
             
                log.debug(exif)
         
     | 
| 
       95 
102 
     | 
    
         | 
| 
         @@ -106,14 +113,14 @@ def extract_exif_tags(image_filename: str, sort: bool = False) -> dict: 
     | 
|
| 
       106 
113 
     | 
    
         
             
                return filtered_info
         
     | 
| 
       107 
114 
     | 
    
         | 
| 
       108 
115 
     | 
    
         | 
| 
       109 
     | 
    
         
            -
            def datetime( 
     | 
| 
      
 116 
     | 
    
         
            +
            def datetime(image: Image.Image):
         
     | 
| 
       110 
117 
     | 
    
         
             
                """Extract datetime metadata."""
         
     | 
| 
       111 
     | 
    
         
            -
                exif_tags = extract_exif_tags( 
     | 
| 
      
 118 
     | 
    
         
            +
                exif_tags = extract_exif_tags(image)
         
     | 
| 
       112 
119 
     | 
    
         
             
                return exif_tags["DateTime"]
         
     | 
| 
       113 
120 
     | 
    
         | 
| 
       114 
121 
     | 
    
         | 
| 
       115 
     | 
    
         
            -
            def camera_metadata( 
     | 
| 
      
 122 
     | 
    
         
            +
            def camera_metadata(image: Image.Image):
         
     | 
| 
       116 
123 
     | 
    
         
             
                """Extract camera and model metadata."""
         
     | 
| 
       117 
     | 
    
         
            -
                exif_tags = extract_exif_tags( 
     | 
| 
      
 124 
     | 
    
         
            +
                exif_tags = extract_exif_tags(image)
         
     | 
| 
       118 
125 
     | 
    
         
             
                metadata = f'{exif_tags["Make"]} {exif_tags["Model"]}'
         
     | 
| 
       119 
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
         
     | 
| 
         @@ -63,9 +63,9 @@ def run(args: argparse.Namespace) -> None: 
     | 
|
| 
       63 
63 
     | 
    
         
             
                montaged_image = Image.new("RGB", (total_width, total_height))
         
     | 
| 
       64 
64 
     | 
    
         | 
| 
       65 
65 
     | 
    
         
             
                x_offset = 0
         
     | 
| 
       66 
     | 
    
         
            -
                for  
     | 
| 
       67 
     | 
    
         
            -
                    montaged_image.paste( 
     | 
| 
       68 
     | 
    
         
            -
                    x_offset +=  
     | 
| 
      
 66 
     | 
    
         
            +
                for image in images:
         
     | 
| 
      
 67 
     | 
    
         
            +
                    montaged_image.paste(image, (x_offset, 0))
         
     | 
| 
      
 68 
     | 
    
         
            +
                    x_offset += image.width
         
     | 
| 
       69 
69 
     | 
    
         | 
| 
       70 
70 
     | 
    
         
             
                output_image_filename = args.image_filenames[0].name
         
     | 
| 
       71 
71 
     | 
    
         
             
                save_image(args, montaged_image, output_image_filename, "montage")
         
     | 
| 
         @@ -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
         
     | 
| 
         @@ -40,6 +40,21 @@ def build_subparser(subparsers) -> None: 
     | 
|
| 
       40 
40 
     | 
    
         
             
                    metavar="IMAGE_FILENAMES",
         
     | 
| 
       41 
41 
     | 
    
         
             
                )
         
     | 
| 
       42 
42 
     | 
    
         | 
| 
      
 43 
     | 
    
         
            +
                rotate_parser.add_argument(
         
     | 
| 
      
 44 
     | 
    
         
            +
                    "-r",
         
     | 
| 
      
 45 
     | 
    
         
            +
                    "--rotation",
         
     | 
| 
      
 46 
     | 
    
         
            +
                    type=int,
         
     | 
| 
      
 47 
     | 
    
         
            +
                    default=0,
         
     | 
| 
      
 48 
     | 
    
         
            +
                    help="Rotation angle in degrees (default: '%(default)s')",
         
     | 
| 
      
 49 
     | 
    
         
            +
                )
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                rotate_parser.add_argument(
         
     | 
| 
      
 52 
     | 
    
         
            +
                    "-cw",
         
     | 
| 
      
 53 
     | 
    
         
            +
                    "--clockwise",
         
     | 
| 
      
 54 
     | 
    
         
            +
                    action="store_true",
         
     | 
| 
      
 55 
     | 
    
         
            +
                    help="Rotate clockwise (default: '%(default)s)",
         
     | 
| 
      
 56 
     | 
    
         
            +
                )
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
       43 
58 
     | 
    
         | 
| 
       44 
59 
     | 
    
         
             
            def run(args: argparse.Namespace) -> None:
         
     | 
| 
       45 
60 
     | 
    
         
             
                """Run rotate subcommand.
         
     | 
| 
         @@ -52,10 +67,14 @@ def run(args: argparse.Namespace) -> None: 
     | 
|
| 
       52 
67 
     | 
    
         
             
                """
         
     | 
| 
       53 
68 
     | 
    
         
             
                log.debug(args)
         
     | 
| 
       54 
69 
     | 
    
         | 
| 
      
 70 
     | 
    
         
            +
                rotation = args.rotation
         
     | 
| 
      
 71 
     | 
    
         
            +
                if args.clockwise:
         
     | 
| 
      
 72 
     | 
    
         
            +
                    rotation = -rotation
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
       55 
74 
     | 
    
         
             
                for image_filename in args.image_filenames:
         
     | 
| 
       56 
75 
     | 
    
         
             
                    original_image = Image.open(image_filename)
         
     | 
| 
       57 
76 
     | 
    
         
             
                    rotated_image = original_image.rotate(
         
     | 
| 
       58 
     | 
    
         
            -
                         
     | 
| 
      
 77 
     | 
    
         
            +
                        rotation,
         
     | 
| 
       59 
78 
     | 
    
         
             
                        expand=True,
         
     | 
| 
       60 
79 
     | 
    
         
             
                    )
         
     | 
| 
       61 
80 
     | 
    
         
             
                    save_image(args, rotated_image, image_filename, "rotate")
         
     | 
| 
         @@ -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,10 +19,10 @@ 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 
     | 
    
         
            -
            from fotolab.info import camera_metadata
         
     | 
| 
      
 25 
     | 
    
         
            +
            from fotolab.subcommands.info import camera_metadata
         
     | 
| 
       26 
26 
     | 
    
         | 
| 
       27 
27 
     | 
    
         
             
            log = logging.getLogger(__name__)
         
     | 
| 
       28 
28 
     | 
    
         | 
| 
         @@ -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()
         
     | 
| 
         @@ -237,19 +296,14 @@ def calc_padding(image, args) -> int: 
     | 
|
| 
       237 
296 
     | 
    
         | 
| 
       238 
297 
     | 
    
         
             
            def calc_position(image, text_width, text_height, position, padding) -> tuple:
         
     | 
| 
       239 
298 
     | 
    
         
             
                """Calculate the boundary coordinates of the watermark text."""
         
     | 
| 
       240 
     | 
    
         
            -
                 
     | 
| 
       241 
     | 
    
         
            -
             
     | 
| 
       242 
     | 
    
         
            -
             
     | 
| 
       243 
     | 
    
         
            -
                     
     | 
| 
       244 
     | 
    
         
            -
                     
     | 
| 
       245 
     | 
    
         
            -
             
     | 
| 
       246 
     | 
    
         
            -
             
     | 
| 
       247 
     | 
    
         
            -
                     
     | 
| 
       248 
     | 
    
         
            -
                 
     | 
| 
       249 
     | 
    
         
            -
             
     | 
| 
       250 
     | 
    
         
            -
             
     | 
| 
       251 
     | 
    
         
            -
                elif position == "bottom-right":
         
     | 
| 
       252 
     | 
    
         
            -
                    position_x = image.width - text_width - padding
         
     | 
| 
       253 
     | 
    
         
            -
                    position_y = image.height - text_height - padding
         
     | 
| 
       254 
     | 
    
         
            -
             
     | 
| 
       255 
     | 
    
         
            -
                return (position_x, position_y)
         
     | 
| 
      
 299 
     | 
    
         
            +
                positions = {
         
     | 
| 
      
 300 
     | 
    
         
            +
                    "top-left": (padding, padding),
         
     | 
| 
      
 301 
     | 
    
         
            +
                    "top-right": (image.width - text_width - padding, padding),
         
     | 
| 
      
 302 
     | 
    
         
            +
                    "bottom-left": (padding, image.height - text_height - padding),
         
     | 
| 
      
 303 
     | 
    
         
            +
                    "bottom-right": (
         
     | 
| 
      
 304 
     | 
    
         
            +
                        image.width - text_width - padding,
         
     | 
| 
      
 305 
     | 
    
         
            +
                        image.height - text_height - padding,
         
     | 
| 
      
 306 
     | 
    
         
            +
                    ),
         
     | 
| 
      
 307 
     | 
    
         
            +
                }
         
     | 
| 
      
 308 
     | 
    
         
            +
             
     | 
| 
      
 309 
     | 
    
         
            +
                return positions.get(position, (0, 0))
         
     | 
| 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Metadata-Version: 2.1
         
     | 
| 
       2 
2 
     | 
    
         
             
            Name: fotolab
         
     | 
| 
       3 
     | 
    
         
            -
            Version: 0. 
     | 
| 
      
 3 
     | 
    
         
            +
            Version: 0.25.1
         
     | 
| 
       4 
4 
     | 
    
         
             
            Summary: A console program that manipulate images.
         
     | 
| 
       5 
5 
     | 
    
         
             
            Keywords: photography,photo
         
     | 
| 
       6 
6 
     | 
    
         
             
            Author-email: Kian-Meng Ang <kianmeng@cpan.org>
         
     | 
| 
         @@ -61,7 +61,7 @@ fotolab -h 
     | 
|
| 
       61 
61 
     | 
    
         | 
| 
       62 
62 
     | 
    
         
             
            ```console
         
     | 
| 
       63 
63 
     | 
    
         
             
            usage: fotolab [-h] [-o] [-op] [-od OUTPUT_DIR] [-q] [-v] [-d] [-V]
         
     | 
| 
       64 
     | 
    
         
            -
                           {animate,auto,border,contrast,info,resize,rotate, 
     | 
| 
      
 64 
     | 
    
         
            +
                           {animate,auto,border,contrast,env,info,montage,resize,rotate,sharpen,watermark} ...
         
     | 
| 
       65 
65 
     | 
    
         | 
| 
       66 
66 
     | 
    
         
             
            A console program to manipulate photos.
         
     | 
| 
       67 
67 
     | 
    
         | 
| 
         @@ -70,19 +70,19 @@ changelog: https://github.com/kianmeng/fotolab/blob/master/CHANGELOG.md 
     | 
|
| 
       70 
70 
     | 
    
         
             
            issues: https://github.com/kianmeng/fotolab/issues
         
     | 
| 
       71 
71 
     | 
    
         | 
| 
       72 
72 
     | 
    
         
             
            positional arguments:
         
     | 
| 
       73 
     | 
    
         
            -
              {animate,auto,border,contrast,info,resize,rotate, 
     | 
| 
      
 73 
     | 
    
         
            +
              {animate,auto,border,contrast,env,info,montage,resize,rotate,sharpen,watermark}
         
     | 
| 
       74 
74 
     | 
    
         
             
                                sub-command help
         
     | 
| 
       75 
75 
     | 
    
         
             
                animate         animate an image
         
     | 
| 
       76 
76 
     | 
    
         
             
                auto            auto adjust (resize, contrast, and watermark) a photo
         
     | 
| 
       77 
77 
     | 
    
         
             
                border          add border to image
         
     | 
| 
       78 
78 
     | 
    
         
             
                contrast        contrast an image
         
     | 
| 
      
 79 
     | 
    
         
            +
                env             print environment information for bug reporting
         
     | 
| 
       79 
80 
     | 
    
         
             
                info            info an image
         
     | 
| 
      
 81 
     | 
    
         
            +
                montage         montage a list of image
         
     | 
| 
       80 
82 
     | 
    
         
             
                resize          resize an image
         
     | 
| 
       81 
83 
     | 
    
         
             
                rotate          rotate an image
         
     | 
| 
       82 
     | 
    
         
            -
                montage         montage a list of image
         
     | 
| 
       83 
84 
     | 
    
         
             
                sharpen         sharpen an image
         
     | 
| 
       84 
85 
     | 
    
         
             
                watermark       watermark an image
         
     | 
| 
       85 
     | 
    
         
            -
                env             print environment information for bug reporting
         
     | 
| 
       86 
86 
     | 
    
         | 
| 
       87 
87 
     | 
    
         
             
            options:
         
     | 
| 
       88 
88 
     | 
    
         
             
              -h, --help        show this help message and exit
         
     | 
| 
         @@ -206,7 +206,7 @@ fotolab info -h 
     | 
|
| 
       206 
206 
     | 
    
         
             
            <!--help-info !-->
         
     | 
| 
       207 
207 
     | 
    
         | 
| 
       208 
208 
     | 
    
         
             
            ```console
         
     | 
| 
       209 
     | 
    
         
            -
            usage: fotolab info [-h] [-s] [--camera] IMAGE_FILENAME
         
     | 
| 
      
 209 
     | 
    
         
            +
            usage: fotolab info [-h] [-s] [--camera] [--datetime] IMAGE_FILENAME
         
     | 
| 
       210 
210 
     | 
    
         | 
| 
       211 
211 
     | 
    
         
             
            positional arguments:
         
     | 
| 
       212 
212 
     | 
    
         
             
              IMAGE_FILENAME  set the image filename
         
     | 
| 
         @@ -215,6 +215,7 @@ options: 
     | 
|
| 
       215 
215 
     | 
    
         
             
              -h, --help      show this help message and exit
         
     | 
| 
       216 
216 
     | 
    
         
             
              -s, --sort      show image info by sorted field name
         
     | 
| 
       217 
217 
     | 
    
         
             
              --camera        show the camera maker details
         
     | 
| 
      
 218 
     | 
    
         
            +
              --datetime      show the datetime
         
     | 
| 
       218 
219 
     | 
    
         
             
            ```
         
     | 
| 
       219 
220 
     | 
    
         | 
| 
       220 
221 
     | 
    
         
             
            <!--help-info !-->
         
     | 
| 
         @@ -228,13 +229,17 @@ fotolab rotate -h 
     | 
|
| 
       228 
229 
     | 
    
         
             
            <!--help-rotate !-->
         
     | 
| 
       229 
230 
     | 
    
         | 
| 
       230 
231 
     | 
    
         
             
            ```console
         
     | 
| 
       231 
     | 
    
         
            -
            usage: fotolab rotate [-h]  
     | 
| 
      
 232 
     | 
    
         
            +
            usage: fotolab rotate [-h] [-r ROTATION] [-cw]
         
     | 
| 
      
 233 
     | 
    
         
            +
                                  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
         
     | 
| 
       232 
234 
     | 
    
         | 
| 
       233 
235 
     | 
    
         
             
            positional arguments:
         
     | 
| 
       234 
     | 
    
         
            -
              IMAGE_FILENAMES 
     | 
| 
      
 236 
     | 
    
         
            +
              IMAGE_FILENAMES       set the image filenames
         
     | 
| 
       235 
237 
     | 
    
         | 
| 
       236 
238 
     | 
    
         
             
            options:
         
     | 
| 
       237 
     | 
    
         
            -
              -h, --help 
     | 
| 
      
 239 
     | 
    
         
            +
              -h, --help            show this help message and exit
         
     | 
| 
      
 240 
     | 
    
         
            +
              -r, --rotation ROTATION
         
     | 
| 
      
 241 
     | 
    
         
            +
                                    Rotation angle in degrees (default: '0')
         
     | 
| 
      
 242 
     | 
    
         
            +
              -cw, --clockwise      Rotate clockwise (default: 'False)
         
     | 
| 
       238 
243 
     | 
    
         
             
            ```
         
     | 
| 
       239 
244 
     | 
    
         | 
| 
       240 
245 
     | 
    
         
             
            <!--help-rotate !-->
         
     | 
| 
         @@ -295,7 +300,7 @@ fotolab sharpen -h 
     | 
|
| 
       295 
300 
     | 
    
         
             
            <!--help-sharpen !-->
         
     | 
| 
       296 
301 
     | 
    
         | 
| 
       297 
302 
     | 
    
         
             
            ```console
         
     | 
| 
       298 
     | 
    
         
            -
            usage: fotolab sharpen [-h] [-r RADIUS] [-p PERCENT] [-t THRESHOLD]
         
     | 
| 
      
 303 
     | 
    
         
            +
            usage: fotolab sharpen [-h] [-r RADIUS] [-p PERCENT] [-t THRESHOLD] [-ba]
         
     | 
| 
       299 
304 
     | 
    
         
             
                                   IMAGE_FILENAMES [IMAGE_FILENAMES ...]
         
     | 
| 
       300 
305 
     | 
    
         | 
| 
       301 
306 
     | 
    
         
             
            positional arguments:
         
     | 
| 
         @@ -310,6 +315,7 @@ options: 
     | 
|
| 
       310 
315 
     | 
    
         
             
              -t, --threshold THRESHOLD
         
     | 
| 
       311 
316 
     | 
    
         
             
                                    set the minimum brightness changed to be sharpened
         
     | 
| 
       312 
317 
     | 
    
         
             
                                    (default: '3')
         
     | 
| 
      
 318 
     | 
    
         
            +
              -ba, --before-after   generate a GIF showing before and after changes
         
     | 
| 
       313 
319 
     | 
    
         
             
            ```
         
     | 
| 
       314 
320 
     | 
    
         | 
| 
       315 
321 
     | 
    
         
             
            <!--help-sharpen !-->
         
     | 
| 
         @@ -380,7 +386,7 @@ options: 
     | 
|
| 
       380 
386 
     | 
    
         | 
| 
       381 
387 
     | 
    
         
             
            ## Copyright and License
         
     | 
| 
       382 
388 
     | 
    
         | 
| 
       383 
     | 
    
         
            -
            Copyright (C) 2024 Kian-Meng Ang
         
     | 
| 
      
 389 
     | 
    
         
            +
            Copyright (C) 2024,2025 Kian-Meng Ang
         
     | 
| 
       384 
390 
     | 
    
         | 
| 
       385 
391 
     | 
    
         
             
            This program is free software: you can redistribute it and/or modify it under
         
     | 
| 
       386 
392 
     | 
    
         
             
            the terms of the GNU Affero General Public License as published by the Free
         
     | 
| 
         @@ -0,0 +1,20 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            fotolab/__init__.py,sha256=VQ7SOkbn3y2Pp5mPmk4P5E7_pFuSvOSW_dASQpdxOCQ,2066
         
     | 
| 
      
 2 
     | 
    
         
            +
            fotolab/__main__.py,sha256=aboOURPs_snOXTEWYR0q8oq1UTY9e-NxCd1j33V0wHI,833
         
     | 
| 
      
 3 
     | 
    
         
            +
            fotolab/cli.py,sha256=9rhS-yhX1nE_zKBfR05QyEt-S7Y43mIP0yUQA6bsgV8,4326
         
     | 
| 
      
 4 
     | 
    
         
            +
            fotolab/subcommands/__init__.py,sha256=l3DlIaJ3u3jGjnC1H1yV8LZ_nPqOLJ6gikD4BCaMAQ0,1129
         
     | 
| 
      
 5 
     | 
    
         
            +
            fotolab/subcommands/animate.py,sha256=Zp0LPM7ktg6V2rIAP8pof2mmPAph_0O3TEySvg55-h8,2972
         
     | 
| 
      
 6 
     | 
    
         
            +
            fotolab/subcommands/auto.py,sha256=p_e1f4mcrIFLqBXMNKvPQRDkNrAlK7FR6sdR5NAR0t8,2427
         
     | 
| 
      
 7 
     | 
    
         
            +
            fotolab/subcommands/border.py,sha256=v5rdLb7Yq1kQI0MTSfVb0iroBroTV8oxOk-jMLB2who,3637
         
     | 
| 
      
 8 
     | 
    
         
            +
            fotolab/subcommands/contrast.py,sha256=8uPCd5xI-aUsL7rjdEPmfSskdzMwM-1tv0eRRONkW2M,2100
         
     | 
| 
      
 9 
     | 
    
         
            +
            fotolab/subcommands/env.py,sha256=JamU3a2xWPbwlAj5iThHs58KYkLmjpUphZfTQODBp_4,1471
         
     | 
| 
      
 10 
     | 
    
         
            +
            fotolab/subcommands/info.py,sha256=DANbfBNy2SzFfeE4KqOViAZkaME6xujfZvJTHIaZyCY,3312
         
     | 
| 
      
 11 
     | 
    
         
            +
            fotolab/subcommands/montage.py,sha256=hhRH9LsWxXa86_qtVDKGvk--Eap2nP17OTy81kq3Xjk,2048
         
     | 
| 
      
 12 
     | 
    
         
            +
            fotolab/subcommands/resize.py,sha256=d2Nlslzlvri9L2rqmE-HbmnLozylSk3U1Hi3DF1q3Mc,5023
         
     | 
| 
      
 13 
     | 
    
         
            +
            fotolab/subcommands/rotate.py,sha256=vhtiAD5r0i5humpjyAbkoxh2nQsSuBYUD-TgcECwUwE,2149
         
     | 
| 
      
 14 
     | 
    
         
            +
            fotolab/subcommands/sharpen.py,sha256=xz8RW8cRPM4eUvJTO1Stsur3G67DBftVGza8kF5j2Pc,3700
         
     | 
| 
      
 15 
     | 
    
         
            +
            fotolab/subcommands/watermark.py,sha256=zKIFMreqSRaTD89JPjtLZljfTw-5ZkbZNBAyLeFveGw,8905
         
     | 
| 
      
 16 
     | 
    
         
            +
            fotolab-0.25.1.dist-info/entry_points.txt,sha256=mvw7AY_yZkIyjAxPtHNed9X99NZeLnMxEeAfEJUbrCM,44
         
     | 
| 
      
 17 
     | 
    
         
            +
            fotolab-0.25.1.dist-info/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
         
     | 
| 
      
 18 
     | 
    
         
            +
            fotolab-0.25.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
         
     | 
| 
      
 19 
     | 
    
         
            +
            fotolab-0.25.1.dist-info/METADATA,sha256=SOBJC9BClYTSg61YN6SmVbIOSn6NB4qpegx7cNBj7pY,11058
         
     | 
| 
      
 20 
     | 
    
         
            +
            fotolab-0.25.1.dist-info/RECORD,,
         
     | 
    
        fotolab-0.22.0.dist-info/RECORD
    DELETED
    
    | 
         @@ -1,19 +0,0 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            fotolab/__init__.py,sha256=pw2AneRyAlWbI9FH4Ikuepdam_hEWBbqqmTUXny6WP4,2061
         
     | 
| 
       2 
     | 
    
         
            -
            fotolab/__main__.py,sha256=aboOURPs_snOXTEWYR0q8oq1UTY9e-NxCd1j33V0wHI,833
         
     | 
| 
       3 
     | 
    
         
            -
            fotolab/animate.py,sha256=ejimhTozo9DN7BbqqcV4x8zLnanZRKq1pxBBFeOdr6Q,2967
         
     | 
| 
       4 
     | 
    
         
            -
            fotolab/auto.py,sha256=l_-Kf5V5Anvwz1QV1ET-42YsDWEeHf_okHkXWOycWAI,2295
         
     | 
| 
       5 
     | 
    
         
            -
            fotolab/border.py,sha256=5ch2d7LVPhB2OFuuXSW5ci6Cn967CPDQu0qSfaO7uMg,3591
         
     | 
| 
       6 
     | 
    
         
            -
            fotolab/cli.py,sha256=FBFSeMNqcOiJ6MuAcy0qUvc9cscdFUG946HlWZXBPtY,4984
         
     | 
| 
       7 
     | 
    
         
            -
            fotolab/contrast.py,sha256=l7Bs5p8W8ypN9Cg3fFHnU-A20UwMKtjTiPk6D0PRwpM,2095
         
     | 
| 
       8 
     | 
    
         
            -
            fotolab/env.py,sha256=fzUoRWgYEiYJIWYEiiSLEb7dH_xVUOnhMpQgc1yjrTY,1457
         
     | 
| 
       9 
     | 
    
         
            -
            fotolab/info.py,sha256=jIWmsDAk8ikKb3uZ568DHu3m21OW2-1Erg9PRYa4giw,3281
         
     | 
| 
       10 
     | 
    
         
            -
            fotolab/montage.py,sha256=lUVY-zDSH7mwH-s34_XefdNp7CoDJHkwpbTUGiyJGgs,2037
         
     | 
| 
       11 
     | 
    
         
            -
            fotolab/resize.py,sha256=2bH1Kgoe_DqU8ozJ1E_oA6a9JPtuwIlo5a4sq_4Yles,5018
         
     | 
| 
       12 
     | 
    
         
            -
            fotolab/rotate.py,sha256=l_vQgf0IcI8AR1TSVsk4PrMZtJ3j_wpU77rKiGJ-KTA,1715
         
     | 
| 
       13 
     | 
    
         
            -
            fotolab/sharpen.py,sha256=wUPtJdtB6mCRmcHrA0CoEVO0O0ROBJWhejTvUeL67QU,2655
         
     | 
| 
       14 
     | 
    
         
            -
            fotolab/watermark.py,sha256=XuVj-Cg1duNtW1Z9S8Y0AN4Q268lTM3dne5HnzLWgj0,7252
         
     | 
| 
       15 
     | 
    
         
            -
            fotolab-0.22.0.dist-info/entry_points.txt,sha256=mvw7AY_yZkIyjAxPtHNed9X99NZeLnMxEeAfEJUbrCM,44
         
     | 
| 
       16 
     | 
    
         
            -
            fotolab-0.22.0.dist-info/LICENSE.md,sha256=tGtFDwxWTjuR9syrJoSv1Hiffd2u8Tu8cYClfrXS_YU,31956
         
     | 
| 
       17 
     | 
    
         
            -
            fotolab-0.22.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
         
     | 
| 
       18 
     | 
    
         
            -
            fotolab-0.22.0.dist-info/METADATA,sha256=4pqzWJZoXD0c7sSRQM5RrSlC6HuEFFf9o2K-Yx6C_m0,10724
         
     | 
| 
       19 
     | 
    
         
            -
            fotolab-0.22.0.dist-info/RECORD,,
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |