fix: preserve transparent email logos

This commit is contained in:
3252a8
2026-06-07 23:21:34 +03:00
parent 4b8f939a25
commit 00e1f51abe
2 changed files with 72 additions and 1 deletions
+36 -1
View File
@@ -10,12 +10,15 @@ copy goes through the shared `JsonI18n` instance so translations live in
from __future__ import annotations
import html
import io
import re
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Sequence, Tuple
from urllib.parse import urlsplit
from PIL import Image, ImageOps, UnidentifiedImageError
if TYPE_CHECKING:
from bot.middlewares.i18n import JsonI18n
from config.settings import Settings
@@ -42,6 +45,7 @@ _LOGO_CONTENT_TYPES = {
".svg": "image/svg+xml",
".webp": "image/webp",
}
_EMAIL_LOGO_PNG_FALLBACK_EXTENSIONS = {".ico", ".webp"}
@dataclass(frozen=True)
@@ -129,6 +133,8 @@ def _inline_uploaded_logo(settings: Settings) -> Optional[EmailInlineImage]:
if not body or len(body) > _WEBAPP_LOGO_MAX_BYTES:
return None
content_type, body = _email_logo_payload(filename, content_type, body)
return EmailInlineImage(
content_id=_EMAIL_LOGO_CONTENT_ID,
content_type=content_type,
@@ -136,6 +142,35 @@ def _inline_uploaded_logo(settings: Settings) -> Optional[EmailInlineImage]:
)
def _email_logo_payload(filename: str, content_type: str, body: bytes) -> Tuple[str, bytes]:
suffix = Path(filename).suffix.lower()
if suffix not in _EMAIL_LOGO_PNG_FALLBACK_EXTENSIONS:
return content_type, body
png_body = _static_raster_logo_to_png(body)
if png_body and len(png_body) <= _WEBAPP_LOGO_MAX_BYTES:
return "image/png", png_body
return content_type, body
def _static_raster_logo_to_png(body: bytes) -> Optional[bytes]:
try:
with Image.open(io.BytesIO(body)) as image:
image.seek(0)
if getattr(image, "is_animated", False):
return None
source = ImageOps.exif_transpose(image).convert("RGBA")
except (OSError, UnidentifiedImageError, ValueError, EOFError):
return None
if source.width < 1 or source.height < 1 or source.width > 8192 or source.height > 8192:
return None
output = io.BytesIO()
source.save(output, format="PNG", optimize=True)
return output.getvalue()
def _email_logo(settings: Settings) -> Tuple[Optional[str], Tuple[EmailInlineImage, ...]]:
inline_logo = _inline_uploaded_logo(settings)
if inline_logo:
@@ -198,7 +233,7 @@ def _layout(
logo_block = (
f'<img src="{html.escape(logo_url, quote=True)}" width="64" height="64" '
f'alt="" style="display:block;border:0;outline:none;text-decoration:none;'
f'border-radius:16px;">'
f'border-radius:16px;background:transparent;background-color:transparent;">'
)
layout_html = f"""<!DOCTYPE html>
+36
View File
@@ -2,9 +2,12 @@ import json
import re
import subprocess
import sys
from io import BytesIO
from pathlib import Path
from types import SimpleNamespace
from PIL import Image
from bot.middlewares.i18n import JsonI18n
from bot.services import email_templates as email_templates_module
from bot.services.email_templates import (
@@ -318,6 +321,39 @@ def test_uploaded_webapp_logo_is_embedded_inline(tmp_path, monkeypatch):
assert inline_logo.data == logo_body
def test_uploaded_webp_logo_is_embedded_as_transparent_png(tmp_path, monkeypatch):
uploads_dir = tmp_path / "uploads"
uploads_dir.mkdir()
filename = "logo-2222222222222222.webp"
source = Image.new("RGBA", (3, 3), (0, 0, 0, 0))
source.putpixel((1, 1), (255, 0, 0, 255))
raw = BytesIO()
source.save(raw, format="WEBP", lossless=True)
(uploads_dir / filename).write_bytes(raw.getvalue())
monkeypatch.setattr(email_templates_module, "_WEBAPP_UPLOADED_LOGO_DIR", uploads_dir)
settings = _settings()
settings.WEBAPP_LOGO_URL = f"/webapp-uploaded-logo/{filename}"
content = render_login_code(
settings,
code="123456",
language_code="en",
purpose="login",
i18n=_i18n("en"),
)
assert 'src="cid:webapp-logo"' in content.html
assert len(content.inline_images) == 1
inline_logo = content.inline_images[0]
assert inline_logo.content_type == "image/png"
with Image.open(BytesIO(inline_logo.data)) as converted:
assert converted.mode == "RGBA"
assert converted.getpixel((0, 0))[3] == 0
assert converted.getpixel((1, 1))[3] == 255
def test_public_https_webapp_logo_remains_external():
settings = _settings()
settings.WEBAPP_LOGO_URL = "https://cdn.example.com/logo.png"