fix(sync): compress oversized tweet images before Bluesky upload

This commit is contained in:
Guillem Hernandez Sola
2026-04-09 16:53:00 +02:00
parent 3d1e202d62
commit 24abd8d32f

View File

@@ -28,6 +28,12 @@ BSKY_TEXT_MAX_LENGTH = 275
VIDEO_MAX_DURATION_SECONDS = 179
MAX_VIDEO_UPLOAD_SIZE_MB = 45
# Tweet image upload safety limits
BSKY_IMAGE_MAX_BYTES = 950 * 1024
BSKY_IMAGE_MAX_DIMENSION = 2000
BSKY_IMAGE_MIN_JPEG_QUALITY = 45
# External card thumbnail limits
EXTERNAL_THUMB_MAX_BYTES = 950 * 1024
EXTERNAL_THUMB_MAX_DIMENSION = 1200
EXTERNAL_THUMB_MIN_JPEG_QUALITY = 40
@@ -773,6 +779,66 @@ def upload_blob_with_retry(client, binary_data, media_label="media"):
return None
def compress_post_image_to_limit(image_bytes, max_bytes=BSKY_IMAGE_MAX_BYTES):
"""
Compress/resize normal tweet images so they fit within Bluesky image blob limits.
Returns JPEG bytes or None.
"""
try:
with Image.open(io.BytesIO(image_bytes)) as img:
img = img.convert("RGB")
width, height = img.size
max_dim = max(width, height)
if max_dim > BSKY_IMAGE_MAX_DIMENSION:
scale = BSKY_IMAGE_MAX_DIMENSION / max_dim
new_size = (max(1, int(width * scale)), max(1, int(height * scale)))
img = img.resize(new_size, Image.LANCZOS)
logging.info(f"🖼️ Resized post image to {new_size[0]}x{new_size[1]}")
for quality in [90, 82, 75, 68, 60, 52, BSKY_IMAGE_MIN_JPEG_QUALITY]:
out = io.BytesIO()
img.save(out, format="JPEG", quality=quality, optimize=True, progressive=True)
data = out.getvalue()
logging.info(
f"🖼️ Post image candidate size at JPEG quality {quality}: "
f"{len(data)} bytes ({len(data) / 1024:.2f} KB)"
)
if len(data) <= max_bytes:
return data
for target_dim in [1800, 1600, 1400, 1200, 1000]:
resized = img.copy()
width, height = resized.size
max_dim = max(width, height)
if max_dim > target_dim:
scale = target_dim / max_dim
new_size = (max(1, int(width * scale)), max(1, int(height * scale)))
resized = resized.resize(new_size, Image.LANCZOS)
for quality in [68, 60, 52, BSKY_IMAGE_MIN_JPEG_QUALITY]:
out = io.BytesIO()
resized.save(out, format="JPEG", quality=quality, optimize=True, progressive=True)
data = out.getvalue()
logging.info(
f"🖼️ Post image resized to <= {target_dim}px at quality {quality}: "
f"{len(data)} bytes ({len(data) / 1024:.2f} KB)"
)
if len(data) <= max_bytes:
return data
except Exception as e:
logging.warning(f"Could not compress post image: {repr(e)}")
return None
def get_blob_from_url(media_url, client, http_client):
try:
r = http_client.get(media_url, timeout=MEDIA_DOWNLOAD_TIMEOUT, follow_redirects=True)
@@ -785,7 +851,30 @@ def get_blob_from_url(media_url, client, http_client):
logging.warning(f"Could not fetch media {media_url}: empty response body")
return None
return upload_blob_with_retry(client, content, media_label=media_url)
content_type = (r.headers.get("content-type") or "").lower()
upload_bytes = content
if content_type.startswith("image/"):
original_size = len(content)
logging.info(f"🖼️ Downloaded post image {media_url} ({original_size} bytes / {original_size / 1024:.2f} KB)")
if original_size > BSKY_IMAGE_MAX_BYTES:
logging.info(
f"🖼️ Post image exceeds safe Bluesky limit "
f"({original_size} bytes > {BSKY_IMAGE_MAX_BYTES} bytes). Compressing..."
)
compressed = compress_post_image_to_limit(content, BSKY_IMAGE_MAX_BYTES)
if compressed:
upload_bytes = compressed
logging.info(
f"✅ Post image compressed to {len(upload_bytes)} bytes "
f"({len(upload_bytes) / 1024:.2f} KB)"
)
else:
logging.warning(f"⚠️ Could not compress post image to safe limit: {media_url}")
return None
return upload_blob_with_retry(client, upload_bytes, media_label=media_url)
except Exception as e:
logging.warning(f"Could not fetch media {media_url}: {repr(e)}")