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 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,
):

Loading…
Cancel
Save