diff --git a/.env.dev.example b/.env.dev.example index f42aaaaec..d4476fd24 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -43,6 +43,9 @@ EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_USE_TLS=true EMAIL_USE_SSL=false +# Thumbnails Generation +ENABLE_THUMBNAIL_GENERATION=false + # S3 configuration USE_S3=false AWS_ACCESS_KEY_ID= @@ -58,6 +61,7 @@ AWS_SECRET_ACCESS_KEY= # AWS_S3_REGION_NAME=None # "fr-par" # AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud" + # Preview image generation can be computing and storage intensive # ENABLE_PREVIEW_IMAGES=True diff --git a/.env.prod.example b/.env.prod.example index 5115469ca..99520916a 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -43,6 +43,9 @@ EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_USE_TLS=true EMAIL_USE_SSL=false +# Thumbnails Generation +ENABLE_THUMBNAIL_GENERATION=false + # S3 configuration USE_S3=false AWS_ACCESS_KEY_ID= @@ -58,6 +61,7 @@ AWS_SECRET_ACCESS_KEY= # AWS_S3_REGION_NAME=None # "fr-par" # AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud" + # Preview image generation can be computing and storage intensive # ENABLE_PREVIEW_IMAGES=True diff --git a/bookwyrm/context_processors.py b/bookwyrm/context_processors.py index 1f0387fe7..0610a8b9a 100644 --- a/bookwyrm/context_processors.py +++ b/bookwyrm/context_processors.py @@ -11,6 +11,7 @@ def site_settings(request): # pylint: disable=unused-argument return { "site": models.SiteSettings.objects.get(), "active_announcements": models.Announcement.active_announcements(), + "thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION, "media_full_url": settings.MEDIA_FULL_URL, "preview_images_enabled": settings.ENABLE_PREVIEW_IMAGES, "request_protocol": request_protocol, diff --git a/bookwyrm/imagegenerators.py b/bookwyrm/imagegenerators.py new file mode 100644 index 000000000..1d065192e --- /dev/null +++ b/bookwyrm/imagegenerators.py @@ -0,0 +1,113 @@ +"""Generators for all the different thumbnail sizes""" +from imagekit import ImageSpec, register +from imagekit.processors import ResizeToFit + + +class BookXSmallWebp(ImageSpec): + """Handles XSmall size in Webp format""" + + processors = [ResizeToFit(80, 80)] + format = "WEBP" + options = {"quality": 95} + + +class BookXSmallJpg(ImageSpec): + """Handles XSmall size in Jpeg format""" + + processors = [ResizeToFit(80, 80)] + format = "JPEG" + options = {"quality": 95} + + +class BookSmallWebp(ImageSpec): + """Handles Small size in Webp format""" + + processors = [ResizeToFit(100, 100)] + format = "WEBP" + options = {"quality": 95} + + +class BookSmallJpg(ImageSpec): + """Handles Small size in Jpeg format""" + + processors = [ResizeToFit(100, 100)] + format = "JPEG" + options = {"quality": 95} + + +class BookMediumWebp(ImageSpec): + """Handles Medium size in Webp format""" + + processors = [ResizeToFit(150, 150)] + format = "WEBP" + options = {"quality": 95} + + +class BookMediumJpg(ImageSpec): + """Handles Medium size in Jpeg format""" + + processors = [ResizeToFit(150, 150)] + format = "JPEG" + options = {"quality": 95} + + +class BookLargeWebp(ImageSpec): + """Handles Large size in Webp format""" + + processors = [ResizeToFit(200, 200)] + format = "WEBP" + options = {"quality": 95} + + +class BookLargeJpg(ImageSpec): + """Handles Large size in Jpeg format""" + + processors = [ResizeToFit(200, 200)] + format = "JPEG" + options = {"quality": 95} + + +class BookXLargeWebp(ImageSpec): + """Handles XLarge size in Webp format""" + + processors = [ResizeToFit(250, 250)] + format = "WEBP" + options = {"quality": 95} + + +class BookXLargeJpg(ImageSpec): + """Handles XLarge size in Jpeg format""" + + processors = [ResizeToFit(250, 250)] + format = "JPEG" + options = {"quality": 95} + + +class BookXxLargeWebp(ImageSpec): + """Handles XxLarge size in Webp format""" + + processors = [ResizeToFit(500, 500)] + format = "WEBP" + options = {"quality": 95} + + +class BookXxLargeJpg(ImageSpec): + """Handles XxLarge size in Jpeg format""" + + processors = [ResizeToFit(500, 500)] + format = "JPEG" + options = {"quality": 95} + + +register.generator("bw:book:xsmall:webp", BookXSmallWebp) +register.generator("bw:book:xsmall:jpg", BookXSmallJpg) +register.generator("bw:book:small:webp", BookSmallWebp) +register.generator("bw:book:small:jpg", BookSmallJpg) +register.generator("bw:book:medium:webp", BookMediumWebp) +register.generator("bw:book:medium:jpg", BookMediumJpg) +register.generator("bw:book:large:webp", BookLargeWebp) +register.generator("bw:book:large:jpg", BookLargeJpg) +register.generator("bw:book:xlarge:webp", BookXLargeWebp) +register.generator("bw:book:xlarge:jpg", BookXLargeJpg) +register.generator("bw:book:xxlarge:webp", BookXxLargeWebp) +register.generator("bw:book:xxlarge:jpg", BookXxLargeJpg) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index a6aa5de2d..8bed69249 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -7,10 +7,16 @@ from django.db import models from django.dispatch import receiver from model_utils import FieldTracker from model_utils.managers import InheritanceManager +from imagekit.models import ImageSpecField from bookwyrm import activitypub from bookwyrm.preview_images import generate_edition_preview_image_task -from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE, ENABLE_PREVIEW_IMAGES +from bookwyrm.settings import ( + DOMAIN, + DEFAULT_LANGUAGE, + ENABLE_PREVIEW_IMAGES, + ENABLE_THUMBNAIL_GENERATION, +) from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin from .base_model import BookWyrmModel @@ -97,6 +103,40 @@ class Book(BookDataModel): objects = InheritanceManager() field_tracker = FieldTracker(fields=["authors", "title", "subtitle", "cover"]) + if ENABLE_THUMBNAIL_GENERATION: + cover_bw_book_xsmall_webp = ImageSpecField( + source="cover", id="bw:book:xsmall:webp" + ) + cover_bw_book_xsmall_jpg = ImageSpecField( + source="cover", id="bw:book:xsmall:jpg" + ) + cover_bw_book_small_webp = ImageSpecField( + source="cover", id="bw:book:small:webp" + ) + cover_bw_book_small_jpg = ImageSpecField(source="cover", id="bw:book:small:jpg") + cover_bw_book_medium_webp = ImageSpecField( + source="cover", id="bw:book:medium:webp" + ) + cover_bw_book_medium_jpg = ImageSpecField( + source="cover", id="bw:book:medium:jpg" + ) + cover_bw_book_large_webp = ImageSpecField( + source="cover", id="bw:book:large:webp" + ) + cover_bw_book_large_jpg = ImageSpecField(source="cover", id="bw:book:large:jpg") + cover_bw_book_xlarge_webp = ImageSpecField( + source="cover", id="bw:book:xlarge:webp" + ) + cover_bw_book_xlarge_jpg = ImageSpecField( + source="cover", id="bw:book:xlarge:jpg" + ) + cover_bw_book_xxlarge_webp = ImageSpecField( + source="cover", id="bw:book:xxlarge:webp" + ) + cover_bw_book_xxlarge_jpg = ImageSpecField( + source="cover", id="bw:book:xxlarge:jpg" + ) + @property def author_text(self): """format a list of authors""" diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 180191d98..c1f900794 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -75,6 +75,7 @@ INSTALLED_APPS = [ "django_rename_app", "bookwyrm", "celery", + "imagekit", "storages", ] @@ -191,6 +192,9 @@ USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % ( DOMAIN, ) +# Imagekit generated thumbnails +ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False) +IMAGEKIT_CACHEFILE_DIR = "thumbnails" # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index 8fbdcfc6c..6eb068abc 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -232,16 +232,21 @@ body { /* Cover caption * -------------------------------------------------------------------------- */ -.no-cover .cover_caption { +.no-cover .cover-caption { position: absolute; top: 0; right: 0; bottom: 0; left: 0; - padding: 0.25em; + padding: 0.5em; font-size: 0.75em; color: white; background-color: #002549; + display: flex; + align-items: center; + justify-content: center; + white-space: initial; + text-align: center; } /** Avatars diff --git a/bookwyrm/storage_backends.py b/bookwyrm/storage_backends.py index e10dfb841..4fb0feff0 100644 --- a/bookwyrm/storage_backends.py +++ b/bookwyrm/storage_backends.py @@ -1,4 +1,6 @@ """Handles backends for storages""" +import os +from tempfile import SpooledTemporaryFile from storages.backends.s3boto3 import S3Boto3Storage @@ -15,3 +17,33 @@ class ImagesStorage(S3Boto3Storage): # pylint: disable=abstract-method location = "images" default_acl = "public-read" file_overwrite = False + + """ + This is our custom version of S3Boto3Storage that fixes a bug in + boto3 where the passed in file is closed upon upload. + From: + https://github.com/matthewwithanm/django-imagekit/issues/391#issuecomment-275367006 + https://github.com/boto/boto3/issues/929 + https://github.com/matthewwithanm/django-imagekit/issues/391 + """ + + def _save(self, name, content): + """ + We create a clone of the content file as when this is passed to + boto3 it wrongly closes the file upon upload where as the storage + backend expects it to still be open + """ + # Seek our content back to the start + content.seek(0, os.SEEK_SET) + + # Create a temporary file that will write to disk after a specified + # size. This file will be automatically deleted when closed by + # boto3 or after exiting the `with` statement if the boto3 is fixed + with SpooledTemporaryFile() as content_autoclose: + + # Write our original content into our copy that will be closed by boto3 + content_autoclose.write(content.read()) + + # Upload the object which will auto close the + # content_autoclose instance + return super()._save(name, content_autoclose) diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 2e8ff0d04..28655fa6b 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -62,7 +62,7 @@
- {% include 'snippets/book_cover.html' with book=book cover_class='is-h-m-mobile' %} + {% include 'snippets/book_cover.html' with size='xxlarge' size_mobile='medium' book=book cover_class='is-h-m-mobile' %} {% include 'snippets/rate_action.html' with user=request.user book=book %}
diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html index 32018a251..ee6cdcade 100644 --- a/bookwyrm/templates/book/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -220,7 +220,7 @@

{% trans "Cover" %}

- {% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' %} + {% include 'snippets/book_cover.html' with book=book cover_class='is-h-xl-mobile is-w-auto-tablet' size_mobile='xlarge' size='large' %}
diff --git a/bookwyrm/templates/book/editions.html b/bookwyrm/templates/book/editions.html index e2a0bdda5..7a4338f12 100644 --- a/bookwyrm/templates/book/editions.html +++ b/bookwyrm/templates/book/editions.html @@ -15,7 +15,7 @@
diff --git a/bookwyrm/templates/discover/large-book.html b/bookwyrm/templates/discover/large-book.html index 81654d576..c18342cea 100644 --- a/bookwyrm/templates/discover/large-book.html +++ b/bookwyrm/templates/discover/large-book.html @@ -10,7 +10,7 @@ {% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto-tablet' %} + >{% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto-tablet' size='xlarge' %} {% include 'snippets/stars.html' with rating=book|rating:request.user %}

diff --git a/bookwyrm/templates/discover/small-book.html b/bookwyrm/templates/discover/small-book.html index 79fbd77c8..5f93bbff3 100644 --- a/bookwyrm/templates/discover/small-book.html +++ b/bookwyrm/templates/discover/small-book.html @@ -6,7 +6,7 @@ {% if status.book or status.mention_books.exists %} {% load_book status as book %} - {% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto align to-b to-l' %} + {% include 'snippets/book_cover.html' with cover_class='is-w-l-mobile is-w-auto align to-b to-l' size='large' %}
diff --git a/bookwyrm/templates/get_started/book_preview.html b/bookwyrm/templates/get_started/book_preview.html index d8941ad50..893e7593a 100644 --- a/bookwyrm/templates/get_started/book_preview.html +++ b/bookwyrm/templates/get_started/book_preview.html @@ -1,6 +1,6 @@ {% load i18n %}
- {% include 'snippets/book_cover.html' with book=book cover_class='is-h-l is-h-m-mobile' %} + {% include 'snippets/book_cover.html' with book=book cover_class='is-h-l is-h-m-mobile' size_mobile='medium' size='large' %}