Full source for the-third-rev: Discord bot (discord.py), FastAPI web UI (React/TS/Vite/Tailwind), ComfyUI integration, generation history DB, preset manager, workflow inspector, and all supporting modules. Excluded from tracking: .env, invite_tokens.json, *.db (SQLite), current-workflow-changes.json, user_settings/, presets/, logs/, web-static/ (build output), frontend/node_modules/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
"""
|
|
image_utils.py
|
|
==============
|
|
|
|
Shared image processing utilities.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from PIL import Image as _PILImage
|
|
_HAS_PIL = True
|
|
except ImportError:
|
|
_HAS_PIL = False
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DISCORD_MAX_BYTES = 8 * 1024 * 1024 # 8 MiB Discord free-tier upload limit
|
|
|
|
|
|
def compress_to_discord_limit(data: bytes, filename: str) -> tuple[bytes, str]:
|
|
"""
|
|
Compress image bytes to fit within Discord's 8 MiB upload limit.
|
|
|
|
Tries quality reduction first, then progressive downsizing.
|
|
Converts to JPEG if the source format (e.g. PNG) cannot be quality-compressed.
|
|
Returns (data, filename) — filename may change if format conversion is needed.
|
|
No-op if data is already within the limit.
|
|
"""
|
|
if len(data) <= DISCORD_MAX_BYTES:
|
|
logger.info(f"Image size is less than {DISCORD_MAX_BYTES}")
|
|
return data, filename
|
|
|
|
if not _HAS_PIL:
|
|
logger.warning("Pillow not installed — cannot compress %s (%d bytes), uploading as-is", filename, len(data))
|
|
return data, filename
|
|
|
|
suffix = Path(filename).suffix.lower()
|
|
stem = Path(filename).stem
|
|
|
|
img = _PILImage.open(io.BytesIO(data))
|
|
|
|
# PNG/GIF/BMP don't support lossy quality — convert to JPEG
|
|
logger.info("Checking file extension for convert to jpeg")
|
|
if suffix in (".jpg", ".jpeg"):
|
|
fmt, out_name = "JPEG", filename
|
|
elif suffix == ".webp":
|
|
fmt, out_name = "WEBP", filename
|
|
else:
|
|
fmt, out_name = "JPEG", stem + ".jpg"
|
|
if img.mode in ("RGBA", "P", "LA"):
|
|
img = img.convert("RGB")
|
|
logger.info("File extension checked")
|
|
|
|
# Round 1: quality reduction only
|
|
logger.info("# Round 1: quality reduction only")
|
|
for quality in (85, 70, 55, 40, 25, 15):
|
|
logger.info(f"# Round 1: Trying quality: {quality}")
|
|
buf = io.BytesIO()
|
|
img.save(buf, format=fmt, quality=quality)
|
|
if buf.tell() <= DISCORD_MAX_BYTES:
|
|
logger.info("Compressed %s at quality=%d: %d → %d bytes", filename, quality, len(data), buf.tell())
|
|
return buf.getvalue(), out_name
|
|
|
|
# Round 2: resize + low quality
|
|
logger.info("# Round 2: resize + low quality")
|
|
for scale in (0.75, 0.5, 0.35, 0.25):
|
|
w, h = img.size
|
|
resized = img.resize((int(w * scale), int(h * scale)), _PILImage.LANCZOS)
|
|
logger.info(f"# Round 2: Trying to resize: {resized.size}")
|
|
buf = io.BytesIO()
|
|
resized.save(buf, format=fmt, quality=15)
|
|
if buf.tell() <= DISCORD_MAX_BYTES:
|
|
logger.info("Compressed %s at scale=%.2f: %d → %d bytes", filename, scale, len(data), buf.tell())
|
|
return buf.getvalue(), out_name
|
|
|
|
logger.warning("Could not compress %s under %d bytes — uploading best effort", filename, DISCORD_MAX_BYTES)
|
|
buf = io.BytesIO()
|
|
img.save(buf, format=fmt, quality=10)
|
|
return buf.getvalue(), out_name
|