changeset 57:76dce7813340

Resize images if they're too large to be uploaded to Bluesky. This adds a new dependency to the Pillow library.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 17 Oct 2023 15:34:26 -0700
parents d5ba2179d876
children d65f6dced79f
files requirements.txt setup.py silorider/silos/bluesky.py
diffstat 3 files changed, 35 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/requirements.txt	Tue Oct 17 15:32:23 2023 -0700
+++ b/requirements.txt	Tue Oct 17 15:34:26 2023 -0700
@@ -15,6 +15,7 @@
 mf2py==1.1.2
 mf2util==0.5.2
 oauthlib==3.2.2
+Pillow==10.0.1
 python-dateutil==2.8.2
 python-magic==0.4.27
 pytz==2023.3
--- a/setup.py	Tue Oct 17 15:32:23 2023 -0700
+++ b/setup.py	Tue Oct 17 15:34:26 2023 -0700
@@ -16,6 +16,7 @@
     'Mastodon.py>=1.3.0',
     'mf2py>=1.1.0',
     'mf2util>=0.5.0',
+    'Pillow>=10.0.1',
     'python-dateutil>=2.7.0',
     'python-facebook-api>=0.17.1',
     'python-twitter>=3.4.0',
--- a/silorider/silos/bluesky.py	Tue Oct 17 15:32:23 2023 -0700
+++ b/silorider/silos/bluesky.py	Tue Oct 17 15:34:26 2023 -0700
@@ -1,4 +1,5 @@
 import re
+import os.path
 import json
 import time
 import urllib.parse
@@ -11,6 +12,8 @@
 import atproto
 import atproto.xrpc_client.models as atprotomodels
 
+from PIL import Image
+
 
 logger = logging.getLogger(__name__)
 
@@ -51,6 +54,7 @@
     SILO_TYPE = 'bluesky'
     _DEFAULT_SERVER = 'bsky.app'
     _CLIENT_CLASS = _BlueskyClient
+    _MAX_IMAGE_SIZE = 976560
 
     def __init__(self, ctx):
         super().__init__(ctx)
@@ -99,6 +103,7 @@
         return card
 
     def mediaCallback(self, tmpfile, mt, url, desc):
+        tmpfile = self._ensureFileNotTooLarge(tmpfile)
         with open(tmpfile, 'rb') as tmpfp:
             data = tmpfp.read()
 
@@ -110,6 +115,34 @@
             desc = ""
         return atprotomodels.AppBskyEmbedImages.Image(alt=desc, image=upload.blob)
 
+    def _ensureFileNotTooLarge(self, path):
+        file_size = os.path.getsize(path)
+        if file_size <= self._MAX_IMAGE_SIZE:
+            return path
+
+        loops = 0
+        scale = 0.75
+        path_no_ext, ext = os.path.splitext(path)
+        smaller_path = '%s_bsky%s' % (path_no_ext, ext)
+        with Image.open(path) as orig_im:
+            # Resize down 75% until we get below the size limit.
+            img_width, img_height = orig_im.size
+            while loops < 10:
+                logger.debug("Resizing '%s' by a factor of %f" % (path, scale))
+                img_width = int(img_width * scale)
+                img_height = int(img_height * scale)
+                with orig_im.resize((img_width, img_height)) as smaller_im:
+                    smaller_im.save(smaller_path)
+
+                file_size = os.path.getsize(smaller_path)
+                if file_size <= self._MAX_IMAGE_SIZE:
+                    return smaller_path
+
+                scale = scale * scale
+                loops += 1
+
+        raise Exception("Can't reach a small enough image to upload!")
+
     def postEntry(self, entry_card, media_ids, ctx):
         # Add images as an embed on the atproto record.
         embed = None