From 3385fb7da8377998bc869fae2233ed0b34771bb5 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Thu, 3 Sep 2026 11:38:16 +0330 Subject: [PATCH] feat(watermark):watermark smart resizing for different photos --- utils/watermark.py | 106 +++++++++++++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 38 deletions(-) diff --git a/utils/watermark.py b/utils/watermark.py index 7fabd33..04edb72 100644 --- a/utils/watermark.py +++ b/utils/watermark.py @@ -5,6 +5,7 @@ Default position is bottom-left. """ import io import os +import math import logging from pathlib import Path from typing import Optional, Union, Tuple @@ -14,17 +15,18 @@ from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile logger = logging.getLogger(__name__) -# Default watermark search paths -DEFAULT_WATERMARK_PATHS = [ - getattr(settings, "WATERMARK_IMAGE_PATH", None), - os.path.join(settings.BASE_DIR, "static", "images", "watermark.png"), - os.path.join(settings.BASE_DIR, "static", "images", "watermark.svg"), - os.path.join(settings.BASE_DIR, "static", "images", "Frame.svg"), -] +def _get_default_watermark_path() -> Optional[str]: + base_dir = getattr(settings, "BASE_DIR", None) or Path(__file__).resolve().parent.parent + base_dir_str = str(base_dir) + search_paths = [ + getattr(settings, "WATERMARK_IMAGE_PATH", None), + os.path.join(base_dir_str, "static", "images", "watermark.png"), + os.path.join(base_dir_str, "static", "images", "watermark.svg"), + os.path.join(base_dir_str, "static", "images", "Frame.svg"), + ] -def _get_default_watermark_path() -> Optional[str]: - for p in DEFAULT_WATERMARK_PATHS: + for p in search_paths: if p and os.path.exists(p): return p return None @@ -52,8 +54,8 @@ def apply_watermark_to_image( watermark_source: Optional[Union[str, Path, Image.Image]] = None, position: str = "bottom_left", opacity: float = 0.85, - scale_ratio: float = 0.32, - padding: int = 24, + scale_ratio: float = 0.25, + padding: Optional[int] = None, ) -> Image.Image: """ Apply a watermark image onto a base PIL Image. @@ -63,8 +65,8 @@ def apply_watermark_to_image( watermark_source: Path to watermark image or PIL Image object. position: 'bottom_left' (default) | 'bottom_right' | 'top_right' | 'top_left' | 'center'. opacity: Float 0.0 to 1.0. - scale_ratio: Watermark width relative to base image width (default 0.32). - padding: Pixel padding from borders. + scale_ratio: Watermark scale relative to image geometric mean area (default 0.25). + padding: Pixel padding from borders. If None, dynamically calculated based on image size. Returns: New PIL Image with watermark blended. @@ -108,16 +110,38 @@ def apply_watermark_to_image( base_rgba = base_img.convert("RGBA") base_w, base_h = base_rgba.size - # Compute responsive dimensions for watermark (clear and prominent) - target_wm_w = max(int(base_w * scale_ratio), 120) + # 1. Compute responsive dimensions using Geometric Mean Area + # This keeps watermark visual area consistent across wide, square, and portrait images + ref_dim = math.sqrt(base_w * base_h) + target_wm_w = int(ref_dim * scale_ratio) wm_aspect = wm_img.height / wm_img.width - target_wm_h = max(int(target_wm_w * wm_aspect), 35) - - # Scale down if height exceeds 45% of base image height - if target_wm_h > int(base_h * 0.45): - target_wm_h = int(base_h * 0.45) + target_wm_h = int(target_wm_w * wm_aspect) + + # 2. Dynamic aspect-ratio-aware clamping: + # - Width constraint: between 14% and 38% of base width + # - Height constraint: between 4% and 16% of base height + max_w = max(int(base_w * 0.38), 60) + min_w = max(int(base_w * 0.14), 40) + max_h = max(int(base_h * 0.16), 20) + min_h = max(int(base_h * 0.04), 12) + + if target_wm_w > max_w: + target_wm_w = max_w + target_wm_h = int(target_wm_w * wm_aspect) + if target_wm_h > max_h: + target_wm_h = max_h + target_wm_w = int(target_wm_h / wm_aspect) + if target_wm_w < min_w: + target_wm_w = min_w + target_wm_h = int(target_wm_w * wm_aspect) + if target_wm_h < min_h: + target_wm_h = min_h target_wm_w = int(target_wm_h / wm_aspect) + # Safe clamp to stay within physical bounds + target_wm_w = max(1, min(target_wm_w, base_w - 4)) + target_wm_h = max(1, min(target_wm_h, base_h - 4)) + wm_resized = wm_img.resize((target_wm_w, target_wm_h), Image.Resampling.LANCZOS) # Adjust watermark opacity @@ -126,26 +150,32 @@ def apply_watermark_to_image( alpha = ImageEnhance.Brightness(alpha).enhance(max(0.0, min(1.0, opacity))) wm_resized.putalpha(alpha) + # Dynamic responsive padding if not explicitly overridden + if padding is None: + actual_padding = max(8, min(int(min(base_w, base_h) * 0.025), 48)) + else: + actual_padding = padding + # Determine coordinates pos = position.lower() if pos == "bottom_left": - x = padding - y = base_h - target_wm_h - padding + x = actual_padding + y = base_h - target_wm_h - actual_padding elif pos == "bottom_right": - x = base_w - target_wm_w - padding - y = base_h - target_wm_h - padding + x = base_w - target_wm_w - actual_padding + y = base_h - target_wm_h - actual_padding elif pos == "top_right": - x = base_w - target_wm_w - padding - y = padding + x = base_w - target_wm_w - actual_padding + y = actual_padding elif pos == "top_left": - x = padding - y = padding + x = actual_padding + y = actual_padding elif pos == "center": x = (base_w - target_wm_w) // 2 y = (base_h - target_wm_h) // 2 else: - x = padding - y = base_h - target_wm_h - padding + x = actual_padding + y = base_h - target_wm_h - actual_padding # Keep inside image boundaries x = max(0, min(x, base_w - target_wm_w)) @@ -176,8 +206,8 @@ def apply_watermark_to_bytes( watermark_source: Optional[Union[str, Path, Image.Image]] = None, position: str = "bottom_left", opacity: float = 0.85, - scale_ratio: float = 0.32, - padding: int = 24, + scale_ratio: float = 0.25, + padding: Optional[int] = None, quality: int = 90, ) -> bytes: """ @@ -220,8 +250,8 @@ def apply_watermark_to_upload( watermark_source: Optional[Union[str, Path, Image.Image]] = None, position: str = "bottom_left", opacity: float = 0.85, - scale_ratio: float = 0.32, - padding: int = 24, + scale_ratio: float = 0.25, + padding: Optional[int] = None, quality: int = 90, ) -> Optional[SimpleUploadedFile]: """ @@ -273,8 +303,8 @@ def apply_watermark_to_field_file( field_file, position: str = "bottom_left", opacity: float = 0.85, - scale_ratio: float = 0.32, - padding: int = 24, + scale_ratio: float = 0.25, + padding: Optional[int] = None, quality: int = 90, ): """ @@ -316,8 +346,8 @@ try: watermark_source=None, position="bottom_left", opacity=0.85, - scale_ratio=0.32, - padding=24, + scale_ratio=0.25, + padding=None, quality=90, **kwargs, ):