From 836a3c5c693a69f8752b9a0c8f8f6438b7577395 Mon Sep 17 00:00:00 2001 From: GeoJulien Date: Mon, 28 Aug 2023 20:45:23 +0200 Subject: [PATCH 1/2] Add pillow as extra dependency --- requirements/extra.img-local.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 requirements/extra.img-local.txt diff --git a/requirements/extra.img-local.txt b/requirements/extra.img-local.txt new file mode 100644 index 0000000..f9352f7 --- /dev/null +++ b/requirements/extra.img-local.txt @@ -0,0 +1 @@ +pillow>=9.5,<10 diff --git a/setup.py b/setup.py index 453111d..f73933b 100644 --- a/setup.py +++ b/setup.py @@ -108,6 +108,7 @@ def load_requirements(requirements_files: Union[Path, list[Path]]) -> list: "all": load_requirements( list(HERE.joinpath("requirements").glob("extra.*.txt")) ), + "img-local": load_requirements(HERE / "requirements/extra.img-local.txt"), "img-remote": load_requirements(HERE / "requirements/extra.img-remote.txt"), }, # cli From 5ebf45d4ab7f39b51029bd74595ccc4a3230b896 Mon Sep 17 00:00:00 2001 From: GeoJulien Date: Mon, 28 Aug 2023 21:12:35 +0200 Subject: [PATCH 2/2] Add image optimization with pillow (local) --- geotribu_cli/images/images_optimizer.py | 90 ++++++++++++++++-------- geotribu_cli/images/optim_pillow.py | 93 +++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 geotribu_cli/images/optim_pillow.py diff --git a/geotribu_cli/images/images_optimizer.py b/geotribu_cli/images/images_optimizer.py index a7854ae..0688b83 100644 --- a/geotribu_cli/images/images_optimizer.py +++ b/geotribu_cli/images/images_optimizer.py @@ -15,6 +15,7 @@ from geotribu_cli.__about__ import __package_name__ from geotribu_cli.console import console from geotribu_cli.constants import GeotribuDefaults +from geotribu_cli.images.optim_pillow import PILLOW_INSTALLED, pil_redimensionner_image from geotribu_cli.images.optim_tinify import TINIFY_INSTALLED, optimize_with_tinify from geotribu_cli.utils.check_path import check_path from geotribu_cli.utils.start_uri import open_uri @@ -82,10 +83,12 @@ def parser_images_optimizer( subparser.add_argument( "-w", "--with", - choices=["tinypng"], - default="tinypng", + choices=["local", "tinypng"], + default=getenv("GEOTRIBU_DEFAULT_IMAGE_OPTIMIZER", "tinypng"), dest="tool_to_use", - help="Outil à utiliser pour réaliser l'optimisation.", + help="Outil à utiliser pour réaliser l'optimisation. Local (pillow), ou tinypng " + "(service distant nécessitant une clé d'API)", + metavar="GEOTRIBU_DEFAULT_IMAGE_OPTIMIZER", ) subparser.set_defaults(func=run) @@ -108,7 +111,29 @@ def run(args: argparse.Namespace): """ logger.debug(f"Running {args.command} with {args}") - # check Tinify API KEY + # liste l'image ou les images à optimiser + if check_path( + input_path=args.image_path, + must_be_a_folder=True, + must_be_a_file=False, + must_be_readable=True, + must_exists=True, + raise_error=False, + ): + logger.info(f"Dossier d'images passé : {args.image_path}") + li_images = [ + image.resolve() + for image in Path(args.image_path).glob("*") + if image.suffix.lower() in defaults_settings.images_body_extensions + ] + if not li_images: + print(":person_shrugging: Aucune image trouvée dans {args.image_path}") + sys.exit(0) + else: + logger.debug(f"Image unique passée : {args.image_path}") + li_images = [args.image_path] + + # Utilise l'outil d'optimisation if args.tool_to_use == "tinypng": if not TINIFY_INSTALLED: logger.critical( @@ -126,26 +151,6 @@ def run(args: argparse.Namespace): ) sys.exit(1) - if check_path( - input_path=args.image_path, - must_be_a_folder=True, - must_be_a_file=False, - must_be_readable=True, - must_exists=True, - raise_error=False, - ): - logger.info("Dossier d'images passé. L") - li_images = [ - image.resolve() - for image in Path(args.image_path).glob("*") - if image.suffix.lower() in defaults_settings.images_body_extensions - ] - if not li_images: - print(":person_shrugging: Aucune image trouvée dans {args.image_path}") - sys.exit(0) - else: - li_images = [args.image_path] - # optimize the image(s) count_optim_success = 0 count_optim_error = 0 @@ -165,14 +170,41 @@ def run(args: argparse.Namespace): f"{args.tool_to_use} a échoué. Trace : {err}" ) count_optim_error += 1 + elif args.tool_to_use == "local": + if not PILLOW_INSTALLED: + logger.critical( + "Pillow n'est pas installé. " + "Pour l'utiliser, installer l'outil avec les dépendances " + f"supplémentaires : pip install {__package_name__}[img-local] ou " + f"pip install {__package_name__}[all]" + ) + sys.exit(1) - # open output folder if success and not disabled - if args.opt_auto_open_disabled and count_optim_success > 0: - open_uri( - in_filepath=defaults_settings.geotribu_working_folder.joinpath( - "images/optim" + # optimize the image(s) + count_optim_success = 0 + count_optim_error = 0 + for img in li_images: + try: + optimized_image = pil_redimensionner_image(image_path_or_url=img) + console.print( + f":clamp: L'image {img} a été redimensionnée et " + f"compressée avec {args.tool_to_use} : {optimized_image}" ) + count_optim_success += 1 + except Exception as err: + logger.error( + f"La compression de l'image {img} avec " + f"{args.tool_to_use} a échoué. Trace : {err}" + ) + count_optim_error += 1 + + # open output folder if success and not disabled + if args.opt_auto_open_disabled and count_optim_success > 0: + open_uri( + in_filepath=defaults_settings.geotribu_working_folder.joinpath( + "images/optim" ) + ) # -- Stand alone execution diff --git a/geotribu_cli/images/optim_pillow.py b/geotribu_cli/images/optim_pillow.py new file mode 100644 index 0000000..b0f976f --- /dev/null +++ b/geotribu_cli/images/optim_pillow.py @@ -0,0 +1,93 @@ +#! python3 # noqa: E265 + +# ############################################################################ +# ########## IMPORTS ############# +# ################################ + +# standard library +import logging +from pathlib import Path + +# 3rd party +try: + from PIL import Image + from PIL.ImageOps import contain + + PILLOW_INSTALLED = True +except ImportError: + PILLOW_INSTALLED = False + +# package +from geotribu_cli.constants import GeotribuDefaults + +# ############################################################################ +# ########## GLOBALS ############# +# ################################ + +logger = logging.getLogger(__name__) +defaults_settings = GeotribuDefaults() + +# ############################################################################ +# ########## FUNCTIONS ########### +# ################################ + + +def pil_redimensionner_image( + image_path_or_url: Path, + largeur_max_paysage: int = 1000, + hauteur_max_portrait: int = 600, +) -> Path: + """Redimensionne l'image dont le chemin est passé en entrée en tenant compte d'une + contrainte de largeur max pour les images orientées paysage (largeur > hauteur) et + une hauteur max pour les images orientées portrait (hauteur > largeur). + + Args: + image_path_or_url: chemin ou URL vers l'image + largeur_max_paysage: largeur maximum pour une image orientée paysage. + Defaults to 1000. + hauteur_max_portrait: hauteur maximum pour une image orientée portrait. + Defaults to 600. + + Returns: + path to the resized image. + """ + # Ouvrir l'image + try: + img = Image.open(image_path_or_url) + except Exception as err: + logger.error(f"Impossible de lire l'image {image_path_or_url}. Trace : {err}") + return None + + # Vérifier le rapport hauteur/largeur de l'image + rapport_hauteur_largeur = img.height / img.width + + if rapport_hauteur_largeur > 1: + # L'image est en mode portrait + logger.info(f"{image_path_or_url} est orientée PORTRAIT") + nouvelle_hauteur = min(hauteur_max_portrait, img.height) + nouvelle_taille = ( + int(nouvelle_hauteur / rapport_hauteur_largeur), + nouvelle_hauteur, + ) + else: + logger.info(f"{image_path_or_url} est orientée PAYSAGE (ou carrée)") + # L'image est en mode paysage ou carré + nouvelle_largeur = min(largeur_max_paysage, img.width) + nouvelle_taille = ( + nouvelle_largeur, + int(nouvelle_largeur * rapport_hauteur_largeur), + ) + + # Redimensionner l'image + new_img = contain(img, nouvelle_taille) + + # sauvegarder l'image + output_filepath = defaults_settings.geotribu_working_folder.joinpath( + f"images/optim/{image_path_or_url.stem}_resized_{new_img.width}x" + f"{new_img.height}{image_path_or_url.suffix}" + ) + output_filepath.parent.mkdir(parents=True, exist_ok=True) + new_img.save(output_filepath) + new_img.close() + + return output_filepath