fix(sync): compress oversized tweet images before Bluesky upload
This commit is contained in:
@@ -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)}")
|
||||
|
||||
Reference in New Issue
Block a user