diff --git a/app/blueprints/packages/packages.py b/app/blueprints/packages/packages.py index 1328a3c0..67558950 100644 --- a/app/blueprints/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -426,7 +426,7 @@ def move_to_state(package): if not package.approved_at: post_discord_webhook.delay(package.author.display_name, "New package {}".format(package.get_url("packages.view", absolute=True)), False, - package.title, package.short_desc, package.get_thumb_url(2, True)) + package.title, package.short_desc, package.get_thumb_url(2, True, "png")) package.approved_at = datetime.datetime.now() screenshots = PackageScreenshot.query.filter_by(package=package, approved=False).all() @@ -437,7 +437,7 @@ def move_to_state(package): elif state == PackageState.READY_FOR_REVIEW: post_discord_webhook.delay(package.author.display_name, "Ready for Review: {}".format(package.get_url("packages.view", absolute=True)), True, - package.title, package.short_desc, package.get_thumb_url(2, True)) + package.title, package.short_desc, package.get_thumb_url(2, True, "png")) add_notification(package.maintainers, current_user, NotificationType.PACKAGE_APPROVAL, msg, package.get_url("packages.view"), package) severity = AuditSeverity.NORMAL if current_user in package.maintainers else AuditSeverity.EDITOR @@ -480,7 +480,7 @@ def remove(package): post_discord_webhook.delay(current_user.username, f"Deleted package {package.author.username}/{package.name} with reason '{reason}'", - True, package.title, package.short_desc, package.get_thumb_url(2, True)) + True, package.title, package.short_desc, package.get_thumb_url(2, True, "png")) flash(gettext("Deleted package"), "success") @@ -500,7 +500,7 @@ def remove(package): post_discord_webhook.delay(current_user.username, "Unapproved package with reason {}\n\n{}".format(reason, package.get_url("packages.view", absolute=True)), True, - package.title, package.short_desc, package.get_thumb_url(2, True)) + package.title, package.short_desc, package.get_thumb_url(2, True, "png")) flash(gettext("Unapproved package"), "success") diff --git a/app/blueprints/thumbnails/__init__.py b/app/blueprints/thumbnails/__init__.py index 309004cc..2722e35d 100644 --- a/app/blueprints/thumbnails/__init__.py +++ b/app/blueprints/thumbnails/__init__.py @@ -14,15 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - from flask import abort, send_file, Blueprint, current_app +import os +from PIL import Image + bp = Blueprint("thumbnails", __name__) -import os -from PIL import Image -ALLOWED_RESOLUTIONS=[(100,67), (270,180), (350,233), (1100,520)] +ALLOWED_RESOLUTIONS = [(100, 67), (270, 180), (350, 233), (1100, 520)] +ALLOWED_EXTENSIONS = {"png", "webp"} + def mkdir(path): assert path != "" and path is not None @@ -34,10 +36,7 @@ def mkdir(path): def resize_and_crop(img_path, modified_path, size): - try: - img = Image.open(img_path) - except FileNotFoundError: - abort(404) + img = Image.open(img_path) # Get current and desired ratio for the images img_ratio = img.size[0] / float(img.size[1]) @@ -64,21 +63,40 @@ def resize_and_crop(img_path, modified_path, size): img.save(modified_path) +def find_source_file(img): + upload_dir = current_app.config["UPLOAD_DIR"] + source_filepath = os.path.join(upload_dir, img) + if os.path.isfile(source_filepath): + return source_filepath + + period = source_filepath.rfind(".") + start = source_filepath[:period] + ext = source_filepath[period + 1:] + if ext not in ALLOWED_EXTENSIONS: + abort(404) + + for other_ext in ALLOWED_EXTENSIONS: + other_path = f"{start}.{other_ext}" + if ext != other_ext and os.path.isfile(other_path): + return other_path + + abort(404) + + @bp.route("/thumbnails//") def make_thumbnail(img, level): if level > len(ALLOWED_RESOLUTIONS) or level <= 0: abort(403) w, h = ALLOWED_RESOLUTIONS[level - 1] - upload_dir = current_app.config["UPLOAD_DIR"] thumbnail_dir = current_app.config["THUMBNAIL_DIR"] mkdir(thumbnail_dir) output_dir = os.path.join(thumbnail_dir, str(level)) mkdir(output_dir) - cache_filepath = os.path.join(output_dir, img) - source_filepath = os.path.join(upload_dir, img) + cache_filepath = os.path.join(output_dir, img) + source_filepath = find_source_file(img) resize_and_crop(source_filepath, cache_filepath, (w, h)) return send_file(cache_filepath) diff --git a/app/models/packages.py b/app/models/packages.py index fb35aa34..c5327e91 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -528,7 +528,7 @@ def as_key_dict(self): } def as_short_dict(self, base_url, version=None, release_id=None, no_load=False): - tnurl = self.get_thumb_url(1) + tnurl = self.get_thumb_url(1, format="png") if release_id is None and no_load == False: release = self.get_download_release(version=version) @@ -555,7 +555,7 @@ def as_short_dict(self, base_url, version=None, release_id=None, no_load=False): return ret def as_dict(self, base_url, version=None): - tnurl = self.get_thumb_url(1) + tnurl = self.get_thumb_url(1, format="png") release = self.get_download_release(version=version) return { "author": self.author.username, @@ -603,21 +603,21 @@ def as_dict(self, base_url, version=None): ] } - def get_thumb_or_placeholder(self, level=2): - return self.get_thumb_url(level) or "/static/placeholder.png" + def get_thumb_or_placeholder(self, level=2, format="webp"): + return self.get_thumb_url(level, False, format) or "/static/placeholder.png" - def get_thumb_url(self, level=2, abs=False): + def get_thumb_url(self, level=2, abs=False, format="webp"): screenshot = self.main_screenshot - url = screenshot.get_thumb_url(level) if screenshot is not None else None + url = screenshot.get_thumb_url(level, format) if screenshot is not None else None if abs: from app.utils import abs_url return abs_url(url) else: return url - def get_cover_image_url(self): + def get_cover_image_url(self, format="webp"): screenshot = self.cover_image or self.main_screenshot - return screenshot and screenshot.get_thumb_url(4) + return screenshot and screenshot.get_thumb_url(4, format) def get_url(self, endpoint, absolute=False, **kwargs): if absolute: @@ -1101,8 +1101,12 @@ def get_delete_url(self): name=self.package.name, id=self.id) - def get_thumb_url(self, level=2): - return self.url.replace("/uploads/", "/thumbnails/{:d}/".format(level)) + def get_thumb_url(self, level=2, format="webp"): + url = self.url.replace("/uploads/", "/thumbnails/{:d}/".format(level)) + if format is not None: + start = url[:url.rfind(".")] + url = f"{start}.{format}" + return url def as_dict(self, base_url=""): return { diff --git a/app/templates/collections/view.html b/app/templates/collections/view.html index 6d9c389a..09fa6466 100644 --- a/app/templates/collections/view.html +++ b/app/templates/collections/view.html @@ -9,7 +9,7 @@ {%- endblock %} {% block headextra %} - {% set thumb_url = collection.packages and collection.packages[0].get_thumb_url(3, True) %} + {% set thumb_url = collection.packages and collection.packages[0].get_thumb_url(3, True, "png") %} {% if thumb_url -%} {%- endif %} diff --git a/app/templates/macros/packagegridtile.html b/app/templates/macros/packagegridtile.html index 9884e7c6..c041b100 100644 --- a/app/templates/macros/packagegridtile.html +++ b/app/templates/macros/packagegridtile.html @@ -1,6 +1,6 @@ {% macro render_pkgtile(package, show_author) -%}
  • - +