Browse Source

feat(watermark):watermark smart resizing for different photos

master
Mohsen Taba 2 weeks ago
parent
commit
3385fb7da8
  1. 106
      utils/watermark.py

106
utils/watermark.py

@ -5,6 +5,7 @@ Default position is bottom-left.
""" """
import io import io
import os import os
import math
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, Union, Tuple from typing import Optional, Union, Tuple
@ -14,17 +15,18 @@ from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
logger = logging.getLogger(__name__) 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): if p and os.path.exists(p):
return p return p
return None return None
@ -52,8 +54,8 @@ def apply_watermark_to_image(
watermark_source: Optional[Union[str, Path, Image.Image]] = None, watermark_source: Optional[Union[str, Path, Image.Image]] = None,
position: str = "bottom_left", position: str = "bottom_left",
opacity: float = 0.85, opacity: float = 0.85,
scale_ratio: float = 0.32,
padding: int = 24,
scale_ratio: float = 0.25,
padding: Optional[int] = None,
) -> Image.Image: ) -> Image.Image:
""" """
Apply a watermark image onto a base PIL 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. watermark_source: Path to watermark image or PIL Image object.
position: 'bottom_left' (default) | 'bottom_right' | 'top_right' | 'top_left' | 'center'. position: 'bottom_left' (default) | 'bottom_right' | 'top_right' | 'top_left' | 'center'.
opacity: Float 0.0 to 1.0. 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: Returns:
New PIL Image with watermark blended. New PIL Image with watermark blended.
@ -108,16 +110,38 @@ def apply_watermark_to_image(
base_rgba = base_img.convert("RGBA") base_rgba = base_img.convert("RGBA")
base_w, base_h = base_rgba.size 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 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) 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) wm_resized = wm_img.resize((target_wm_w, target_wm_h), Image.Resampling.LANCZOS)
# Adjust watermark opacity # Adjust watermark opacity
@ -126,26 +150,32 @@ def apply_watermark_to_image(
alpha = ImageEnhance.Brightness(alpha).enhance(max(0.0, min(1.0, opacity))) alpha = ImageEnhance.Brightness(alpha).enhance(max(0.0, min(1.0, opacity)))
wm_resized.putalpha(alpha) 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 # Determine coordinates
pos = position.lower() pos = position.lower()
if pos == "bottom_left": 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": 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": 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": elif pos == "top_left":
x = padding
y = padding
x = actual_padding
y = actual_padding
elif pos == "center": elif pos == "center":
x = (base_w - target_wm_w) // 2 x = (base_w - target_wm_w) // 2
y = (base_h - target_wm_h) // 2 y = (base_h - target_wm_h) // 2
else: 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 # Keep inside image boundaries
x = max(0, min(x, base_w - target_wm_w)) 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, watermark_source: Optional[Union[str, Path, Image.Image]] = None,
position: str = "bottom_left", position: str = "bottom_left",
opacity: float = 0.85, opacity: float = 0.85,
scale_ratio: float = 0.32,
padding: int = 24,
scale_ratio: float = 0.25,
padding: Optional[int] = None,
quality: int = 90, quality: int = 90,
) -> bytes: ) -> bytes:
""" """
@ -220,8 +250,8 @@ def apply_watermark_to_upload(
watermark_source: Optional[Union[str, Path, Image.Image]] = None, watermark_source: Optional[Union[str, Path, Image.Image]] = None,
position: str = "bottom_left", position: str = "bottom_left",
opacity: float = 0.85, opacity: float = 0.85,
scale_ratio: float = 0.32,
padding: int = 24,
scale_ratio: float = 0.25,
padding: Optional[int] = None,
quality: int = 90, quality: int = 90,
) -> Optional[SimpleUploadedFile]: ) -> Optional[SimpleUploadedFile]:
""" """
@ -273,8 +303,8 @@ def apply_watermark_to_field_file(
field_file, field_file,
position: str = "bottom_left", position: str = "bottom_left",
opacity: float = 0.85, opacity: float = 0.85,
scale_ratio: float = 0.32,
padding: int = 24,
scale_ratio: float = 0.25,
padding: Optional[int] = None,
quality: int = 90, quality: int = 90,
): ):
""" """
@ -316,8 +346,8 @@ try:
watermark_source=None, watermark_source=None,
position="bottom_left", position="bottom_left",
opacity=0.85, opacity=0.85,
scale_ratio=0.32,
padding=24,
scale_ratio=0.25,
padding=None,
quality=90, quality=90,
**kwargs, **kwargs,
): ):

Loading…
Cancel
Save