Files
yaoyaoai/app/core/branding_images.py
fengchuanhn@gmail.com 5ee6a57dd1 11
2026-05-21 01:47:21 +08:00

120 lines
3.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OEM 品牌图:落盘至项目根目录 images/,库内仅存相对路径。"""
from __future__ import annotations
import base64
import re
import uuid
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
IMAGES_DIR = PROJECT_ROOT / "images"
DATA_URL_PATTERN = re.compile(r"^data:image/([\w+.-]+);base64,(.+)$", re.DOTALL)
MAX_IMAGE_BYTES = 5 * 1024 * 1024
EXT_ALIASES = {"jpeg": "jpg", "svg+xml": "svg"}
class BrandingImageError(Exception):
def __init__(self, message: str) -> None:
self.message = message
super().__init__(message)
def ensure_images_dir() -> None:
IMAGES_DIR.mkdir(parents=True, exist_ok=True)
def is_data_url(value: str) -> bool:
return value.strip().startswith("data:image/")
def normalize_relative_path(value: str | None) -> str | None:
"""从完整 URL 或相对路径得到 images/xxx 形式。"""
if not value:
return None
text = value.strip()
if not text or is_data_url(text):
return None
marker = "/images/"
if marker in text:
suffix = text.split(marker, 1)[1].split("?")[0]
return f"images/{suffix}"
normalized = text.replace("\\", "/").lstrip("/")
if normalized.startswith("images/"):
return normalized
return None
def absolute_path(relative_path: str | None) -> Path | None:
rel = normalize_relative_path(relative_path)
if not rel:
return None
name = rel.removeprefix("images/").lstrip("/")
if not name or ".." in Path(name).parts:
return None
return IMAGES_DIR / name
def delete_local_image(relative_path: str | None) -> None:
path = absolute_path(relative_path)
if path is not None and path.is_file():
path.unlink()
def save_data_url(data_url: str, *, file_prefix: str) -> str:
match = DATA_URL_PATTERN.match(data_url.strip())
if not match:
raise BrandingImageError("无效的图片数据")
ext = match.group(1).lower()
ext = EXT_ALIASES.get(ext, ext)
if ext not in {"png", "jpg", "gif", "webp", "svg"}:
raise BrandingImageError(f"不支持的图片格式: {ext}")
try:
raw = base64.b64decode(match.group(2), validate=True)
except Exception as exc:
raise BrandingImageError("图片 Base64 解码失败") from exc
if len(raw) > MAX_IMAGE_BYTES:
raise BrandingImageError("单张图片不能超过 5MB")
ensure_images_dir()
filename = f"oem_{file_prefix}_{uuid.uuid4().hex[:12]}.{ext}"
target = IMAGES_DIR / filename
target.write_bytes(raw)
return f"images/{filename}"
def resolve_image_field(
new_value: str | None,
old_path: str | None,
*,
file_prefix: str,
) -> str | None:
"""
- 空:删除旧文件,返回 None
- data URL写入新文件删除旧文件
- 已是 images/ 路径(或与旧路径相同):原样保留
"""
if new_value is None or not str(new_value).strip():
delete_local_image(old_path)
return None
text = str(new_value).strip()
if is_data_url(text):
delete_local_image(old_path)
return save_data_url(text, file_prefix=file_prefix)
rel = normalize_relative_path(text)
if rel is None:
raise BrandingImageError("图片须为上传数据或 images/ 下的路径")
old_rel = normalize_relative_path(old_path)
if rel == old_rel:
return rel
return rel