From d1737b44bdad3ad435721c6363dca18ac3e4f94b Mon Sep 17 00:00:00 2001 From: Joachim Date: Tue, 25 May 2021 00:41:53 +0200 Subject: [PATCH 001/130] First functioning commit TODO - [ ] Delay task (Celery?) - [ ] Store the image in a subfolder unique to the edition, to make cleaning up the image easy - [ ] Clean up the image before replacing it - [ ] Ensure that the image will be cleaned when the edition is deleted ?? - [ ] Use instance custom colors? - [ ] Use book cover color base? --- .gitignore | 3 + .../migrations/0076_book_preview_image.py | 21 +++ bookwyrm/models/book.py | 12 ++ bookwyrm/preview_images.py | 133 ++++++++++++++++++ bookwyrm/settings.py | 5 + bookwyrm/static/fonts/public_sans/OFL.txt | 93 ++++++++++++ .../fonts/public_sans/PublicSans-Bold.ttf | Bin 0 -> 56580 bytes .../fonts/public_sans/PublicSans-Light.ttf | Bin 0 -> 56452 bytes .../fonts/public_sans/PublicSans-Regular.ttf | Bin 0 -> 56424 bytes celerywyrm/celery.py | 1 + 10 files changed, 268 insertions(+) create mode 100644 bookwyrm/migrations/0076_book_preview_image.py create mode 100644 bookwyrm/preview_images.py create mode 100644 bookwyrm/static/fonts/public_sans/OFL.txt create mode 100644 bookwyrm/static/fonts/public_sans/PublicSans-Bold.ttf create mode 100644 bookwyrm/static/fonts/public_sans/PublicSans-Light.ttf create mode 100644 bookwyrm/static/fonts/public_sans/PublicSans-Regular.ttf diff --git a/.gitignore b/.gitignore index cf88e9878..624ce100c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ #nginx nginx/default.conf + +#macOS +**/.DS_Store diff --git a/bookwyrm/migrations/0076_book_preview_image.py b/bookwyrm/migrations/0076_book_preview_image.py new file mode 100644 index 000000000..070be663f --- /dev/null +++ b/bookwyrm/migrations/0076_book_preview_image.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2 on 2021-05-24 18:03 + +import bookwyrm.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0075_announcement"), + ] + + operations = [ + migrations.AddField( + model_name="edition", + name="preview_image", + field=bookwyrm.models.fields.ImageField( + blank=True, null=True, upload_to="previews/" + ), + ), + ] diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 869ff04d2..72f0547bf 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -2,10 +2,13 @@ import re from django.db import models +from django.dispatch import receiver from model_utils.managers import InheritanceManager from bookwyrm import activitypub +from bookwyrm.preview_images import generate_preview_image_task from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE +from bookwyrm.tasks import app from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin from .base_model import BookWyrmModel @@ -204,6 +207,9 @@ class Edition(Book): activitypub_field="work", ) edition_rank = fields.IntegerField(default=0) + preview_image = fields.ImageField( + upload_to="previews/", blank=True, null=True, alt_field="alt_text" + ) activity_serializer = activitypub.Edition name_field = "title" @@ -293,3 +299,9 @@ def isbn_13_to_10(isbn_13): if checkdigit == 10: checkdigit = "X" return converted + str(checkdigit) + + +@receiver(models.signals.post_save, sender=Edition) +# pylint: disable=unused-argument +def preview_image(instance, *args, **kwargs): + generate_preview_image_task(instance, *args, **kwargs) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py new file mode 100644 index 000000000..b659f678b --- /dev/null +++ b/bookwyrm/preview_images.py @@ -0,0 +1,133 @@ +import math +import textwrap + +from io import BytesIO +from PIL import Image, ImageDraw, ImageFont, ImageOps +from pathlib import Path +from uuid import uuid4 + +from django.core.files.base import ContentFile +from django.core.files.uploadedfile import InMemoryUploadedFile + +from bookwyrm import models, settings +from bookwyrm.tasks import app + +# dev +import logging + +IMG_WIDTH = settings.PREVIEW_IMG_WIDTH +IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT +BG_COLOR = (182, 186, 177) +TRANSPARENT_COLOR = (0, 0, 0, 0) +TEXT_COLOR = (16, 16, 16) + +margin = math.ceil(IMG_HEIGHT / 10) +gutter = math.ceil(margin / 2) +cover_img_limits = math.ceil(IMG_HEIGHT * 0.8) +path = Path(__file__).parent.absolute() +font_path = path.joinpath("static/fonts/public_sans") + + +def generate_texts_layer(edition, text_x): + try: + font_title = ImageFont.truetype("%s/PublicSans-Bold.ttf" % font_path, 48) + font_authors = ImageFont.truetype("%s/PublicSans-Regular.ttf" % font_path, 40) + except OSError: + font_title = ImageFont.load_default() + font_authors = ImageFont.load_default() + + text_layer = Image.new("RGBA", (IMG_WIDTH, IMG_HEIGHT), color=TRANSPARENT_COLOR) + text_layer_draw = ImageDraw.Draw(text_layer) + + text_y = 0 + + text_y = text_y + 6 + + # title + title = textwrap.fill(edition.title, width=28) + text_layer_draw.multiline_text((0, text_y), title, font=font_title, fill=TEXT_COLOR) + + text_y = text_y + font_title.getsize_multiline(title)[1] + 16 + + # subtitle + authors_text = ", ".join(a.name for a in edition.authors.all()) + authors = textwrap.fill(authors_text, width=36) + text_layer_draw.multiline_text( + (0, text_y), authors, font=font_authors, fill=TEXT_COLOR + ) + + imageBox = text_layer.getbbox() + return text_layer.crop(imageBox) + + +def generate_site_layer(text_x): + try: + font_instance = ImageFont.truetype("%s/PublicSans-Light.ttf" % font_path, 28) + except OSError: + font_instance = ImageFont.load_default() + + site = models.SiteSettings.objects.get() + + if site.logo_small: + logo_img = Image.open(site.logo_small) + else: + static_path = path.joinpath("static/images/logo-small.png") + logo_img = Image.open(static_path) + + site_layer = Image.new("RGBA", (IMG_WIDTH - text_x - margin, 50), color=BG_COLOR) + + logo_img.thumbnail((50, 50), Image.ANTIALIAS) + + site_layer.paste(logo_img, (0, 0)) + + site_layer_draw = ImageDraw.Draw(site_layer) + site_layer_draw.text((60, 10), site.name, font=font_instance, fill=TEXT_COLOR) + + return site_layer + + +def generate_preview_image(edition): + img = Image.new("RGBA", (IMG_WIDTH, IMG_HEIGHT), color=BG_COLOR) + + cover_img_layer = Image.open(edition.cover) + cover_img_layer.thumbnail((cover_img_limits, cover_img_limits), Image.ANTIALIAS) + + text_x = margin + cover_img_layer.width + gutter + + texts_layer = generate_texts_layer(edition, text_x) + text_y = IMG_HEIGHT - margin - texts_layer.height + + site_layer = generate_site_layer(text_x) + + # Composite all layers + img.paste(cover_img_layer, (margin, margin)) + img.alpha_composite(texts_layer, (text_x, text_y)) + img.alpha_composite(site_layer, (text_x, margin)) + + file_name = "%s.png" % str(uuid4()) + + image_buffer = BytesIO() + try: + img.save(image_buffer, format="png") + edition.preview_image = InMemoryUploadedFile( + ContentFile(image_buffer.getvalue()), + "preview_image", + file_name, + "image/png", + image_buffer.tell(), + None, + ) + + edition.save(update_fields=["preview_image"]) + finally: + image_buffer.close() + + +@app.task +def generate_preview_image_task(instance, *args, **kwargs): + """generate preview_image after save""" + updated_fields = kwargs["update_fields"] + + if not updated_fields or "preview_image" not in updated_fields: + logging.warn("image name to delete", instance.preview_image.name) + generate_preview_image(edition=instance) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index d694e33fd..cee07e913 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -37,6 +37,11 @@ LOCALE_PATHS = [ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +# preview image + +PREVIEW_IMG_WIDTH = 1200 +PREVIEW_IMG_HEIGHT = 630 + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ diff --git a/bookwyrm/static/fonts/public_sans/OFL.txt b/bookwyrm/static/fonts/public_sans/OFL.txt new file mode 100644 index 000000000..ac793eaaa --- /dev/null +++ b/bookwyrm/static/fonts/public_sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2015, Pablo Impallari, Rodrigo Fuenzalida (Modified by Dan O. Williams and USWDS) (https://github.com/uswds/public-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/bookwyrm/static/fonts/public_sans/PublicSans-Bold.ttf b/bookwyrm/static/fonts/public_sans/PublicSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3eb5ac24e2b78297531f6ac6c31255a306af980d GIT binary patch literal 56580 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 G6!ZXGvhhCv literal 0 HcmV?d00001 diff --git a/bookwyrm/static/fonts/public_sans/PublicSans-Light.ttf b/bookwyrm/static/fonts/public_sans/PublicSans-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..13fd7edbceff6e379e28d7ca78b17822b1753d0c GIT binary patch literal 56452 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 ^BFW47ceehP-k4oxRAjdJbS9kxR`MB|P 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 E0J)!T)c^nh literal 0 HcmV?d00001 diff --git a/celerywyrm/celery.py b/celerywyrm/celery.py index 4af8e281d..1dad55888 100644 --- a/celerywyrm/celery.py +++ b/celerywyrm/celery.py @@ -24,5 +24,6 @@ app.autodiscover_tasks(["bookwyrm"], related_name="broadcast") app.autodiscover_tasks(["bookwyrm"], related_name="connectors.abstract_connector") app.autodiscover_tasks(["bookwyrm"], related_name="emailing") app.autodiscover_tasks(["bookwyrm"], related_name="goodreads_import") +app.autodiscover_tasks(["bookwyrm"], related_name="preview_images") app.autodiscover_tasks(["bookwyrm"], related_name="models.user") app.autodiscover_tasks(["bookwyrm"], related_name="views.inbox") From fa7334826c2df4dec35419ee9c596898f3b40998 Mon Sep 17 00:00:00 2001 From: Joachim Date: Tue, 25 May 2021 23:04:28 +0200 Subject: [PATCH 002/130] Update --- bookwyrm/activitypub/book.py | 1 + .../migrations/0076_book_preview_image.py | 8 +- bookwyrm/models/book.py | 15 +- bookwyrm/preview_images.py | 225 ++++++++++++++---- bookwyrm/settings.py | 6 +- bookwyrm/static/images/icons/star-empty.png | Bin 0 -> 1200 bytes bookwyrm/static/images/icons/star-full.png | Bin 0 -> 923 bytes bookwyrm/static/images/icons/star-half.png | Bin 0 -> 1153 bytes requirements.txt | 1 + 9 files changed, 195 insertions(+), 61 deletions(-) create mode 100755 bookwyrm/static/images/icons/star-empty.png create mode 100755 bookwyrm/static/images/icons/star-full.png create mode 100755 bookwyrm/static/images/icons/star-half.png diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 1599b408a..ccb4c0ea3 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -37,6 +37,7 @@ class Book(BookData): publishedDate: str = "" cover: Document = None + preview_image: Document = None type: str = "Book" diff --git a/bookwyrm/migrations/0076_book_preview_image.py b/bookwyrm/migrations/0076_book_preview_image.py index 070be663f..c068e2e27 100644 --- a/bookwyrm/migrations/0076_book_preview_image.py +++ b/bookwyrm/migrations/0076_book_preview_image.py @@ -12,10 +12,8 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( - model_name="edition", - name="preview_image", - field=bookwyrm.models.fields.ImageField( - blank=True, null=True, upload_to="previews/" - ), + model_name='book', + name='preview_image', + field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='cover_previews/'), ), ] diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 72f0547bf..af3005606 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -6,7 +6,7 @@ from django.dispatch import receiver from model_utils.managers import InheritanceManager from bookwyrm import activitypub -from bookwyrm.preview_images import generate_preview_image_task +from bookwyrm.preview_images import generate_preview_image_from_edition_task from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE from bookwyrm.tasks import app @@ -85,6 +85,9 @@ class Book(BookDataModel): cover = fields.ImageField( upload_to="covers/", blank=True, null=True, alt_field="alt_text" ) + preview_image = fields.ImageField( + upload_to="cover_previews/", blank=True, null=True, alt_field="alt_text" + ) first_published_date = fields.DateTimeField(blank=True, null=True) published_date = fields.DateTimeField(blank=True, null=True) @@ -207,9 +210,6 @@ class Edition(Book): activitypub_field="work", ) edition_rank = fields.IntegerField(default=0) - preview_image = fields.ImageField( - upload_to="previews/", blank=True, null=True, alt_field="alt_text" - ) activity_serializer = activitypub.Edition name_field = "title" @@ -302,6 +302,7 @@ def isbn_13_to_10(isbn_13): @receiver(models.signals.post_save, sender=Edition) -# pylint: disable=unused-argument -def preview_image(instance, *args, **kwargs): - generate_preview_image_task(instance, *args, **kwargs) +def preview_image(instance, **kwargs): + updated_fields = kwargs["update_fields"] + + generate_preview_image_from_edition_task.delay(instance.id, updated_fields) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index b659f678b..d0da30839 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -1,13 +1,17 @@ +import colorsys import math +import os import textwrap +from colorthief import ColorThief from io import BytesIO -from PIL import Image, ImageDraw, ImageFont, ImageOps +from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageColor from pathlib import Path from uuid import uuid4 from django.core.files.base import ContentFile from django.core.files.uploadedfile import InMemoryUploadedFile +from django.db.models import Avg from bookwyrm import models, settings from bookwyrm.tasks import app @@ -17,54 +21,64 @@ import logging IMG_WIDTH = settings.PREVIEW_IMG_WIDTH IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT -BG_COLOR = (182, 186, 177) +BG_COLOR = settings.PREVIEW_BG_COLOR +TEXT_COLOR = settings.PREVIEW_TEXT_COLOR +DEFAULT_COVER_COLOR = settings.PREVIEW_DEFAULT_COVER_COLOR TRANSPARENT_COLOR = (0, 0, 0, 0) -TEXT_COLOR = (16, 16, 16) -margin = math.ceil(IMG_HEIGHT / 10) -gutter = math.ceil(margin / 2) -cover_img_limits = math.ceil(IMG_HEIGHT * 0.8) +margin = math.floor(IMG_HEIGHT / 10) +gutter = math.floor(margin / 2) +cover_img_limits = math.floor(IMG_HEIGHT * 0.8) path = Path(__file__).parent.absolute() -font_path = path.joinpath("static/fonts/public_sans") +font_dir = path.joinpath("static/fonts/public_sans") +icon_font_dir = path.joinpath("static/css/fonts") +def get_font(font_name, size=28): + if font_name == "light": + font_path = "%s/PublicSans-Light.ttf" % font_dir + if font_name == "regular": + font_path = "%s/PublicSans-Regular.ttf" % font_dir + elif font_name == "bold": + font_path = "%s/PublicSans-Bold.ttf" % font_dir + elif font_name == "icomoon": + font_path = "%s/icomoon.ttf" % icon_font_dir -def generate_texts_layer(edition, text_x): try: - font_title = ImageFont.truetype("%s/PublicSans-Bold.ttf" % font_path, 48) - font_authors = ImageFont.truetype("%s/PublicSans-Regular.ttf" % font_path, 40) + font = ImageFont.truetype(font_path, size) except OSError: - font_title = ImageFont.load_default() - font_authors = ImageFont.load_default() + font = ImageFont.load_default() - text_layer = Image.new("RGBA", (IMG_WIDTH, IMG_HEIGHT), color=TRANSPARENT_COLOR) + return font + + +def generate_texts_layer(book, content_width): + font_title = get_font("bold", size=48) + font_authors = get_font("regular", size=40) + + text_layer = Image.new("RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR) text_layer_draw = ImageDraw.Draw(text_layer) text_y = 0 - text_y = text_y + 6 - # title - title = textwrap.fill(edition.title, width=28) + title = textwrap.fill(book.title, width=28) text_layer_draw.multiline_text((0, text_y), title, font=font_title, fill=TEXT_COLOR) text_y = text_y + font_title.getsize_multiline(title)[1] + 16 # subtitle - authors_text = ", ".join(a.name for a in edition.authors.all()) + authors_text = book.author_text authors = textwrap.fill(authors_text, width=36) text_layer_draw.multiline_text( (0, text_y), authors, font=font_authors, fill=TEXT_COLOR ) - imageBox = text_layer.getbbox() - return text_layer.crop(imageBox) + text_layer_box = text_layer.getbbox() + return text_layer.crop(text_layer_box) -def generate_site_layer(text_x): - try: - font_instance = ImageFont.truetype("%s/PublicSans-Light.ttf" % font_path, 28) - except OSError: - font_instance = ImageFont.load_default() +def generate_instance_layer(content_width): + font_instance = get_font("light", size=28) site = models.SiteSettings.objects.get() @@ -74,42 +88,157 @@ def generate_site_layer(text_x): static_path = path.joinpath("static/images/logo-small.png") logo_img = Image.open(static_path) - site_layer = Image.new("RGBA", (IMG_WIDTH - text_x - margin, 50), color=BG_COLOR) + instance_layer = Image.new("RGBA", (content_width, 62), color=TRANSPARENT_COLOR) logo_img.thumbnail((50, 50), Image.ANTIALIAS) - site_layer.paste(logo_img, (0, 0)) + instance_layer.paste(logo_img, (0, 0)) - site_layer_draw = ImageDraw.Draw(site_layer) - site_layer_draw.text((60, 10), site.name, font=font_instance, fill=TEXT_COLOR) + instance_layer_draw = ImageDraw.Draw(instance_layer) + instance_layer_draw.text((60, 10), site.name, font=font_instance, fill=TEXT_COLOR) - return site_layer + line_width = 50 + 10 + font_instance.getsize(site.name)[0] + + line_layer = Image.new("RGBA", (line_width, 2), color=(*(ImageColor.getrgb(TEXT_COLOR)), 50)) + instance_layer.alpha_composite(line_layer, (0, 60)) + + return instance_layer -def generate_preview_image(edition): - img = Image.new("RGBA", (IMG_WIDTH, IMG_HEIGHT), color=BG_COLOR) +def generate_rating_layer(rating, content_width): + font_icons = get_font("icomoon", size=60) - cover_img_layer = Image.open(edition.cover) - cover_img_layer.thumbnail((cover_img_limits, cover_img_limits), Image.ANTIALIAS) + icon_star_full = Image.open(path.joinpath("static/images/icons/star-full.png")) + icon_star_empty = Image.open(path.joinpath("static/images/icons/star-empty.png")) + icon_star_half = Image.open(path.joinpath("static/images/icons/star-half.png")) - text_x = margin + cover_img_layer.width + gutter + icon_size = 64 + icon_margin = 10 - texts_layer = generate_texts_layer(edition, text_x) - text_y = IMG_HEIGHT - margin - texts_layer.height + rating_layer_base = Image.new("RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR) + rating_layer_color = Image.new("RGBA", (content_width, icon_size), color=TEXT_COLOR) + rating_layer_mask = Image.new("RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR) - site_layer = generate_site_layer(text_x) + position_x = 0 - # Composite all layers - img.paste(cover_img_layer, (margin, margin)) - img.alpha_composite(texts_layer, (text_x, text_y)) - img.alpha_composite(site_layer, (text_x, margin)) + for r in range(math.floor(rating)): + rating_layer_mask.alpha_composite(icon_star_full, (position_x, 0)) + position_x = position_x + icon_size + icon_margin + + if math.floor(rating) != math.ceil(rating): + rating_layer_mask.alpha_composite(icon_star_half, (position_x, 0)) + position_x = position_x + icon_size + icon_margin + + for r in range(5 - math.ceil(rating)): + rating_layer_mask.alpha_composite(icon_star_empty, (position_x, 0)) + position_x = position_x + icon_size + icon_margin + + rating_layer_mask = rating_layer_mask.getchannel("A") + rating_layer_mask = ImageOps.invert(rating_layer_mask) + + rating_layer_composite = Image.composite(rating_layer_base, rating_layer_color, rating_layer_mask) + + return rating_layer_composite + + +def generate_default_cover(): + font_cover = get_font("light", size=28) + + cover_width = math.floor(cover_img_limits * .7) + default_cover = Image.new("RGB", (cover_width, cover_img_limits), color=DEFAULT_COVER_COLOR) + default_cover_draw = ImageDraw.Draw(default_cover) + + text = "no cover :(" + text_dimensions = font_cover.getsize(text) + text_coords = (math.floor((cover_width - text_dimensions[0]) / 2), + math.floor((cover_img_limits - text_dimensions[1]) / 2)) + default_cover_draw.text(text_coords, text, font=font_cover, fill='white') + + return default_cover + + +def generate_preview_image(book_id, rating=None): + book = models.Book.objects.select_subclasses().get(id=book_id) + + rating = models.Review.objects.filter( + privacy="public", + deleted=False, + book__in=[book_id], + ).aggregate(Avg("rating"))["rating__avg"] + + # Cover + try: + cover_img_layer = Image.open(book.cover) + cover_img_layer.thumbnail((cover_img_limits, cover_img_limits), Image.ANTIALIAS) + color_thief = ColorThief(book.cover) + dominant_color = color_thief.get_color(quality=1) + except: + cover_img_layer = generate_default_cover() + dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR) + + # Color + if BG_COLOR == 'use_dominant_color': + image_bg_color = "rgb(%s, %s, %s)" % dominant_color + # Lighten color + image_bg_color_rgb = [x/255.0 for x in ImageColor.getrgb(image_bg_color)] + image_bg_color_hls = colorsys.rgb_to_hls(*image_bg_color_rgb) + image_bg_color_hls = (image_bg_color_hls[0], 0.9, image_bg_color_hls[1]) + image_bg_color = tuple([math.ceil(x * 255) for x in colorsys.hls_to_rgb(*image_bg_color_hls)]) + else: + image_bg_color = BG_COLOR + + # Background (using the color) + img = Image.new("RGBA", (IMG_WIDTH, IMG_HEIGHT), color=image_bg_color) + + # Contents + content_x = margin + cover_img_layer.width + gutter + content_width = IMG_WIDTH - content_x - margin + + instance_layer = generate_instance_layer(content_width) + texts_layer = generate_texts_layer(book, content_width) + + contents_layer = Image.new("RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR) + contents_composite_y = 0 + contents_layer.alpha_composite(instance_layer, (0, contents_composite_y)) + contents_composite_y = contents_composite_y + instance_layer.height + gutter + contents_layer.alpha_composite(texts_layer, (0, contents_composite_y)) + contents_composite_y = contents_composite_y + texts_layer.height + 30 + + if rating: + # Add some more margin + contents_composite_y = contents_composite_y + 30 + rating_layer = generate_rating_layer(rating, content_width) + contents_layer.alpha_composite(rating_layer, (0, contents_composite_y)) + contents_composite_y = contents_composite_y + rating_layer.height + 30 + + contents_layer_box = contents_layer.getbbox() + contents_layer_height = contents_layer_box[3] - contents_layer_box[1] + + contents_y = math.floor((IMG_HEIGHT - contents_layer_height) / 2) + # Remove Instance Layer from centering calculations + contents_y = contents_y - math.floor((instance_layer.height + gutter) / 2) + + if contents_y < margin: + contents_y = margin + + cover_y = math.floor((IMG_HEIGHT - cover_img_layer.height) / 2) + + # Composite layers + img.paste(cover_img_layer, (margin, cover_y)) + img.alpha_composite(contents_layer, (content_x, contents_y)) file_name = "%s.png" % str(uuid4()) image_buffer = BytesIO() try: + try: + old_path = book.preview_image.path + except ValueError: + old_path = '' + + # Save img.save(image_buffer, format="png") - edition.preview_image = InMemoryUploadedFile( + book.preview_image = InMemoryUploadedFile( ContentFile(image_buffer.getvalue()), "preview_image", file_name, @@ -117,17 +246,17 @@ def generate_preview_image(edition): image_buffer.tell(), None, ) + book.save(update_fields=["preview_image"]) - edition.save(update_fields=["preview_image"]) + # Clean up old file after saving + if os.path.exists(old_path): + os.remove(old_path) finally: image_buffer.close() @app.task -def generate_preview_image_task(instance, *args, **kwargs): +def generate_preview_image_from_edition_task(book_id, updated_fields=None): """generate preview_image after save""" - updated_fields = kwargs["update_fields"] - if not updated_fields or "preview_image" not in updated_fields: - logging.warn("image name to delete", instance.preview_image.name) - generate_preview_image(edition=instance) + generate_preview_image(book_id=book_id) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index cee07e913..cef11630e 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -37,10 +37,14 @@ LOCALE_PATHS = [ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -# preview image +# Preview image +# Specify RGB tuple or RGB hex strings, or 'use_dominant_color' +PREVIEW_BG_COLOR = 'use_dominant_color' PREVIEW_IMG_WIDTH = 1200 PREVIEW_IMG_HEIGHT = 630 +PREVIEW_TEXT_COLOR = '#363636' +PREVIEW_DEFAULT_COVER_COLOR = '#002549' # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ diff --git a/bookwyrm/static/images/icons/star-empty.png b/bookwyrm/static/images/icons/star-empty.png new file mode 100755 index 0000000000000000000000000000000000000000..896417ef69cb053e349720a11b12d7900154011b GIT binary patch literal 1200 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hE1T%)!f9)M#Wl!|>+1fOdA?TL+f8#B3GxAuC8JBEXKa*jt z{>G+AhN4^j)s3oQ2HXyQzoY&K3+iobl4S6@6~4c}x;b-W0$anRZ=p9-!wjSx)_$8F zc`$=v>#g`SR%OO5S@oTwJq!irPfDV0?|8iU)#KuqPt?V^8BWQ>f6o!$%MfycUFlHU z&z^#9M|;2DDta~f6vxMyB|dcxj2HalETS0%o}~%!DGF{=N%6DXdh31{>(;Jt3kOEN z33Y4<)1NUMs8wE)wWaLftk+Rn)_;EJ)Ss=Eze8=?jA9Fh6`6t3*LD=V-0m>X)N5b< z^#ix=v8>+x_^!jleZi%TavU5h>sF{OPuuZ)-_^ez=C3CG`F6Lm*^^mjLbhds`r^z3p8GgLGp!Phk2~OOAZk7I6hPo?VBRrzK5)R?4@Y^~GO@AE{<8Ith!F-Cms)id&m$^t&Q? zFGI(X_?{&uzDGI)ndO(C>^#1A+JbcRtK14tC&x-@26#utggdO@)R zb7oZESy$b6o3HvcYs1RoWh)*Rd$=xcNxv6+{S9lv{69Ts7`U#cHLWxEs$%?Oy6WAh z<6fIRQ<=8QypwKTdAsAzBu0zP-SJahCz~oy<&3(vcY_;$LkQc7nf|QL|5bdDX=n&z zPBK5e`euQj_BHWLmu+&r6J=LCGI$uXOyQ8R{>GHmI~axh*Ou%scI0okl@X)7qcw&p zaCh0orBw_8Ovh*WWHQR%zf_Ug+_2!<0i{QYuld&%@6CP38nEg_#j@jS+hdrfU$kiW ze_Sp1V8W*}-xX^%Mzg+RozQ)RBT(7m`i=0%f?fNsH!#Hcvsp2GWAb>KMySJdpg zoMrp<>kS@jpT2V1%Mess=~wWJd0qa`=@KzKC;r?xUb3?^CUk{(L+_!LzAGPaynlN= z+F{2p0eyzqj5-WPPW=zc>{t`ym-ntOPG#a}P}==(_w2hGfl|TiS3dL7H27+pZCwBE z?8IlAIY5CSp}V7XBJ;&N!W9fhTsi987p$^t|GuL3vD_xUb<5I)&Z=%?Z`k*ry(2Ka ze(jtMUZu9TQ*zHeWo-D_`{`rY!Qgclx0v%c-1CiF`TIX(bh+vKlw%W87#J8BJYD@< J);T3K0RVl4ACdq7 literal 0 HcmV?d00001 diff --git a/bookwyrm/static/images/icons/star-full.png b/bookwyrm/static/images/icons/star-full.png new file mode 100755 index 0000000000000000000000000000000000000000..6d78caf0ce9ad2db1df3df5c9dd9b35efb447606 GIT binary patch literal 923 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hEv zekel-7?wD&7l<-IpzMW%j;sz~Xz>16lF_|CH~c%_Jh9naym@Mj5xRNe<%~hwBY-G^`qMvO*)&VF}(OH&Urwa!FSODQHIa|4A}2* z^w%@2Ww2rXU?p6`_n_9KC5*x1TYhR%Jwt*XN3Je|4daJAr_>6OEer}7R2xk9?5`BbVSJ$1ke!)1d;O0qQ{}F6u>4{_5dKvy`ELk=+9yVaysQtl zN7t!Vcq%u3aFw{@^?S*onamD`=94YDmfY}|=q<3!&fKEK$)e?pF{AZMXNK>_6Q2cM zZJ76)SNWL`ALE*tYpTTT({AtNZCuUqu;_l`3}%aC_RDzonfqV6aoX#XhU&S=v59HL zYW&kSDw}mJU;KairHxtzAsp(DUOrrDRAIug_|bEQ51SV|%n;en@_<7i?dWud8^$fJ zAD1$G<5=iX!`oo%s38{5oxm@!P+RhY@&iizd2W5RZRH)Am^iZGQ;<*NpX3r#U1#6bFBN! z631|Oao$>{gQZ8#aW_1jP~D|_`+3Me73n<;*D5|KOWM77Vio>`^~3e-zg~Y+Ws8oB zl!1I1rmty)Xb)}F7Lxs<=x_8s0KPk3aiEBig)S0(m zdfYiXJe-O3Km_ZJsET#vTrQ=m0Sr9mju!7`tmyjwFr1G&?_}YMoAX@0PKjVV@GDm? z!RXCGL#>w**8UMWlbISGZI^m{w9P*zB6l{^f$WsmdK II;Vst0E!=ulK=n! literal 0 HcmV?d00001 diff --git a/bookwyrm/static/images/icons/star-half.png b/bookwyrm/static/images/icons/star-half.png new file mode 100755 index 0000000000000000000000000000000000000000..75e4eadc6ecb530f833d50cb879761ac5716ab85 GIT binary patch literal 1153 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hE zUCV)W1%v1UX4wMK1xz6gTo)So0vJJp4L|cH&Atcj<;9A88*KA70C=jH{YJKSLmDTG$*Q>zTFa4%wb<64)pKq=-Cs~|(vMElblj&Lh6yor=AKX0V-v1u{mmNoHA58gkF zD==x8_SLkZK;J^hp&!CvI`DSN<;>jDd3}mT4CbpLCicSIpLavby)l>JpyXH7~{&T`dN}4W2)38DhGd47eL2 ze~L9|a2PY}nDU-cfH6^|A^K+=gS)c?qrs9rtOq6v>|tn|x}C{_tBL(U*i)~|&J!=+ zHL6xy{%1+Vr)djzWo$gPVdAE@zwHlnC|fr~YJSUN5|><*X|lalcS++V6u;%B+;iUsS&o+qOUQ zc*LgkD@F(Z?m2$_b5mqyfg77f>c)l(^PZn$QI-o>eQFKch3~=-E8o9%D{S8W;i?vY zxtau{fiiOrV|L?=X`F5h0Zh*?-&;KGKC|Q6M)xVs*X0?C=bC+InsJf&!fs)Ai8cQ( zEj1O0kz0CyDfiYNk6&@_VaS@e<2rYMOzM}56;mGluXgyrH@7in<^k>n)4HP>wuxLn z|Ft3ExO@G-ya-hJHH_cG{6Hk=Q Date: Tue, 25 May 2021 23:05:38 +0200 Subject: [PATCH 003/130] Thank you Black --- .../migrations/0076_book_preview_image.py | 8 ++- bookwyrm/preview_images.py | 59 ++++++++++++------- bookwyrm/settings.py | 6 +- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/bookwyrm/migrations/0076_book_preview_image.py b/bookwyrm/migrations/0076_book_preview_image.py index c068e2e27..bc756f898 100644 --- a/bookwyrm/migrations/0076_book_preview_image.py +++ b/bookwyrm/migrations/0076_book_preview_image.py @@ -12,8 +12,10 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( - model_name='book', - name='preview_image', - field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='cover_previews/'), + model_name="book", + name="preview_image", + field=bookwyrm.models.fields.ImageField( + blank=True, null=True, upload_to="cover_previews/" + ), ), ] diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index d0da30839..960762701 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -33,6 +33,7 @@ path = Path(__file__).parent.absolute() font_dir = path.joinpath("static/fonts/public_sans") icon_font_dir = path.joinpath("static/css/fonts") + def get_font(font_name, size=28): if font_name == "light": font_path = "%s/PublicSans-Light.ttf" % font_dir @@ -99,7 +100,9 @@ def generate_instance_layer(content_width): line_width = 50 + 10 + font_instance.getsize(site.name)[0] - line_layer = Image.new("RGBA", (line_width, 2), color=(*(ImageColor.getrgb(TEXT_COLOR)), 50)) + line_layer = Image.new( + "RGBA", (line_width, 2), color=(*(ImageColor.getrgb(TEXT_COLOR)), 50) + ) instance_layer.alpha_composite(line_layer, (0, 60)) return instance_layer @@ -115,9 +118,13 @@ def generate_rating_layer(rating, content_width): icon_size = 64 icon_margin = 10 - rating_layer_base = Image.new("RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR) + rating_layer_base = Image.new( + "RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR + ) rating_layer_color = Image.new("RGBA", (content_width, icon_size), color=TEXT_COLOR) - rating_layer_mask = Image.new("RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR) + rating_layer_mask = Image.new( + "RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR + ) position_x = 0 @@ -136,7 +143,9 @@ def generate_rating_layer(rating, content_width): rating_layer_mask = rating_layer_mask.getchannel("A") rating_layer_mask = ImageOps.invert(rating_layer_mask) - rating_layer_composite = Image.composite(rating_layer_base, rating_layer_color, rating_layer_mask) + rating_layer_composite = Image.composite( + rating_layer_base, rating_layer_color, rating_layer_mask + ) return rating_layer_composite @@ -144,15 +153,19 @@ def generate_rating_layer(rating, content_width): def generate_default_cover(): font_cover = get_font("light", size=28) - cover_width = math.floor(cover_img_limits * .7) - default_cover = Image.new("RGB", (cover_width, cover_img_limits), color=DEFAULT_COVER_COLOR) + cover_width = math.floor(cover_img_limits * 0.7) + default_cover = Image.new( + "RGB", (cover_width, cover_img_limits), color=DEFAULT_COVER_COLOR + ) default_cover_draw = ImageDraw.Draw(default_cover) text = "no cover :(" text_dimensions = font_cover.getsize(text) - text_coords = (math.floor((cover_width - text_dimensions[0]) / 2), - math.floor((cover_img_limits - text_dimensions[1]) / 2)) - default_cover_draw.text(text_coords, text, font=font_cover, fill='white') + text_coords = ( + math.floor((cover_width - text_dimensions[0]) / 2), + math.floor((cover_img_limits - text_dimensions[1]) / 2), + ) + default_cover_draw.text(text_coords, text, font=font_cover, fill="white") return default_cover @@ -168,22 +181,24 @@ def generate_preview_image(book_id, rating=None): # Cover try: - cover_img_layer = Image.open(book.cover) - cover_img_layer.thumbnail((cover_img_limits, cover_img_limits), Image.ANTIALIAS) - color_thief = ColorThief(book.cover) - dominant_color = color_thief.get_color(quality=1) + cover_img_layer = Image.open(book.cover) + cover_img_layer.thumbnail((cover_img_limits, cover_img_limits), Image.ANTIALIAS) + color_thief = ColorThief(book.cover) + dominant_color = color_thief.get_color(quality=1) except: - cover_img_layer = generate_default_cover() - dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR) + cover_img_layer = generate_default_cover() + dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR) # Color - if BG_COLOR == 'use_dominant_color': + if BG_COLOR == "use_dominant_color": image_bg_color = "rgb(%s, %s, %s)" % dominant_color # Lighten color - image_bg_color_rgb = [x/255.0 for x in ImageColor.getrgb(image_bg_color)] + image_bg_color_rgb = [x / 255.0 for x in ImageColor.getrgb(image_bg_color)] image_bg_color_hls = colorsys.rgb_to_hls(*image_bg_color_rgb) image_bg_color_hls = (image_bg_color_hls[0], 0.9, image_bg_color_hls[1]) - image_bg_color = tuple([math.ceil(x * 255) for x in colorsys.hls_to_rgb(*image_bg_color_hls)]) + image_bg_color = tuple( + [math.ceil(x * 255) for x in colorsys.hls_to_rgb(*image_bg_color_hls)] + ) else: image_bg_color = BG_COLOR @@ -197,7 +212,9 @@ def generate_preview_image(book_id, rating=None): instance_layer = generate_instance_layer(content_width) texts_layer = generate_texts_layer(book, content_width) - contents_layer = Image.new("RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR) + contents_layer = Image.new( + "RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR + ) contents_composite_y = 0 contents_layer.alpha_composite(instance_layer, (0, contents_composite_y)) contents_composite_y = contents_composite_y + instance_layer.height + gutter @@ -217,7 +234,7 @@ def generate_preview_image(book_id, rating=None): contents_y = math.floor((IMG_HEIGHT - contents_layer_height) / 2) # Remove Instance Layer from centering calculations contents_y = contents_y - math.floor((instance_layer.height + gutter) / 2) - + if contents_y < margin: contents_y = margin @@ -234,7 +251,7 @@ def generate_preview_image(book_id, rating=None): try: old_path = book.preview_image.path except ValueError: - old_path = '' + old_path = "" # Save img.save(image_buffer, format="png") diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index cef11630e..db15be468 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -40,11 +40,11 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Preview image # Specify RGB tuple or RGB hex strings, or 'use_dominant_color' -PREVIEW_BG_COLOR = 'use_dominant_color' +PREVIEW_BG_COLOR = "use_dominant_color" PREVIEW_IMG_WIDTH = 1200 PREVIEW_IMG_HEIGHT = 630 -PREVIEW_TEXT_COLOR = '#363636' -PREVIEW_DEFAULT_COVER_COLOR = '#002549' +PREVIEW_TEXT_COLOR = "#363636" +PREVIEW_DEFAULT_COVER_COLOR = "#002549" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ From e305c5d73d42aea0ed51d2053d3766d697fe26c0 Mon Sep 17 00:00:00 2001 From: Joachim Date: Tue, 25 May 2021 23:12:54 +0200 Subject: [PATCH 004/130] Fix color --- bookwyrm/preview_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 960762701..6372aa540 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -195,7 +195,7 @@ def generate_preview_image(book_id, rating=None): # Lighten color image_bg_color_rgb = [x / 255.0 for x in ImageColor.getrgb(image_bg_color)] image_bg_color_hls = colorsys.rgb_to_hls(*image_bg_color_rgb) - image_bg_color_hls = (image_bg_color_hls[0], 0.9, image_bg_color_hls[1]) + image_bg_color_hls = (image_bg_color_hls[0], 0.9, image_bg_color_hls[2]) image_bg_color = tuple( [math.ceil(x * 255) for x in colorsys.hls_to_rgb(*image_bg_color_hls)] ) From 5b03934ec3e243043906dfbd8b1e5b24acf5bebb Mon Sep 17 00:00:00 2001 From: Joachim Date: Tue, 25 May 2021 23:16:33 +0200 Subject: [PATCH 005/130] Update preview_images.py --- bookwyrm/preview_images.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 6372aa540..31b2dc279 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -195,7 +195,11 @@ def generate_preview_image(book_id, rating=None): # Lighten color image_bg_color_rgb = [x / 255.0 for x in ImageColor.getrgb(image_bg_color)] image_bg_color_hls = colorsys.rgb_to_hls(*image_bg_color_rgb) - image_bg_color_hls = (image_bg_color_hls[0], 0.9, image_bg_color_hls[2]) + image_bg_color_hls = ( + image_bg_color_hls[0], + max(0.9, image_bg_color_hls[1]), + image_bg_color_hls[2], + ) image_bg_color = tuple( [math.ceil(x * 255) for x in colorsys.hls_to_rgb(*image_bg_color_hls)] ) From 8c25272462ccaa440d2e99e13bd22e844ce73082 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 09:09:13 +0200 Subject: [PATCH 006/130] Fix last night's bugs --- bookwyrm/activitypub/book.py | 1 - bookwyrm/migrations/0076_book_preview_image.py | 4 +--- bookwyrm/models/book.py | 10 ++++++---- bookwyrm/preview_images.py | 7 +++---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index ccb4c0ea3..1599b408a 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -37,7 +37,6 @@ class Book(BookData): publishedDate: str = "" cover: Document = None - preview_image: Document = None type: str = "Book" diff --git a/bookwyrm/migrations/0076_book_preview_image.py b/bookwyrm/migrations/0076_book_preview_image.py index bc756f898..01ff6576c 100644 --- a/bookwyrm/migrations/0076_book_preview_image.py +++ b/bookwyrm/migrations/0076_book_preview_image.py @@ -14,8 +14,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name="book", name="preview_image", - field=bookwyrm.models.fields.ImageField( - blank=True, null=True, upload_to="cover_previews/" - ), + field=models.ImageField(blank=True, null=True, upload_to="cover_previews/"), ), ] diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index af3005606..5298af929 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -85,8 +85,8 @@ class Book(BookDataModel): cover = fields.ImageField( upload_to="covers/", blank=True, null=True, alt_field="alt_text" ) - preview_image = fields.ImageField( - upload_to="cover_previews/", blank=True, null=True, alt_field="alt_text" + preview_image = models.ImageField( + upload_to="cover_previews/", blank=True, null=True ) first_published_date = fields.DateTimeField(blank=True, null=True) published_date = fields.DateTimeField(blank=True, null=True) @@ -302,7 +302,9 @@ def isbn_13_to_10(isbn_13): @receiver(models.signals.post_save, sender=Edition) -def preview_image(instance, **kwargs): +# pylint: disable=unused-argument +def preview_image(instance, *args, **kwargs): updated_fields = kwargs["update_fields"] - generate_preview_image_from_edition_task.delay(instance.id, updated_fields) + if not updated_fields or "preview_image" not in updated_fields: + generate_preview_image_from_edition_task.delay(instance.id) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 31b2dc279..dd4a62141 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -277,7 +277,6 @@ def generate_preview_image(book_id, rating=None): @app.task -def generate_preview_image_from_edition_task(book_id, updated_fields=None): - """generate preview_image after save""" - if not updated_fields or "preview_image" not in updated_fields: - generate_preview_image(book_id=book_id) +def generate_preview_image_from_edition_task(book_id): + """generate preview_image""" + generate_preview_image(book_id=book_id) From a83aa47c9aded79242bc9fa606faf16c0d3a5410 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 09:10:05 +0200 Subject: [PATCH 007/130] Generate on new rating --- bookwyrm/models/status.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index bd21ec563..987261e46 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -5,11 +5,13 @@ import re from django.apps import apps from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.dispatch import receiver from django.template.loader import get_template from django.utils import timezone from model_utils.managers import InheritanceManager from bookwyrm import activitypub +from bookwyrm.preview_images import generate_preview_image_from_edition_task from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .activitypub_mixin import OrderedCollectionPageMixin from .base_model import BookWyrmModel @@ -398,3 +400,11 @@ class Boost(ActivityMixin, Status): # This constraint can't work as it would cross tables. # class Meta: # unique_together = ('user', 'boosted_status') + + +@receiver(models.signals.post_save) +# pylint: disable=unused-argument +def preview_image(instance, sender, *args, **kwargs): + if sender in (Review, ReviewRating): + edition = instance.book + generate_preview_image_from_edition_task.delay(edition.id) From fd82567cbf28dadf3e784d92de0d3a48c103ba90 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 09:16:15 +0200 Subject: [PATCH 008/130] Update 0076_book_preview_image.py --- bookwyrm/migrations/0076_book_preview_image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bookwyrm/migrations/0076_book_preview_image.py b/bookwyrm/migrations/0076_book_preview_image.py index 01ff6576c..5db550507 100644 --- a/bookwyrm/migrations/0076_book_preview_image.py +++ b/bookwyrm/migrations/0076_book_preview_image.py @@ -1,7 +1,6 @@ # Generated by Django 3.2 on 2021-05-24 18:03 -import bookwyrm.models.fields -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): From 101ca0ff81ee4410e7fd56fb509197d75db162fa Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 09:44:32 +0200 Subject: [PATCH 009/130] Refactor some --- bookwyrm/models/book.py | 4 +- bookwyrm/models/status.py | 4 +- bookwyrm/preview_images.py | 99 +++++++++++++++++++++++--------------- 3 files changed, 65 insertions(+), 42 deletions(-) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 5298af929..01dfbba72 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -6,7 +6,7 @@ from django.dispatch import receiver from model_utils.managers import InheritanceManager from bookwyrm import activitypub -from bookwyrm.preview_images import generate_preview_image_from_edition_task +from bookwyrm.preview_images import generate_edition_preview_image_task from bookwyrm.settings import DOMAIN, DEFAULT_LANGUAGE from bookwyrm.tasks import app @@ -307,4 +307,4 @@ def preview_image(instance, *args, **kwargs): updated_fields = kwargs["update_fields"] if not updated_fields or "preview_image" not in updated_fields: - generate_preview_image_from_edition_task.delay(instance.id) + generate_edition_preview_image_task.delay(instance.id) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 987261e46..c55cd8d69 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -11,7 +11,7 @@ from django.utils import timezone from model_utils.managers import InheritanceManager from bookwyrm import activitypub -from bookwyrm.preview_images import generate_preview_image_from_edition_task +from bookwyrm.preview_images import generate_edition_preview_image_task from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .activitypub_mixin import OrderedCollectionPageMixin from .base_model import BookWyrmModel @@ -407,4 +407,4 @@ class Boost(ActivityMixin, Status): def preview_image(instance, sender, *args, **kwargs): if sender in (Review, ReviewRating): edition = instance.book - generate_preview_image_from_edition_task.delay(edition.id) + generate_edition_preview_image_task.delay(edition.id) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index dd4a62141..82e8dc798 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -31,7 +31,6 @@ gutter = math.floor(margin / 2) cover_img_limits = math.floor(IMG_HEIGHT * 0.8) path = Path(__file__).parent.absolute() font_dir = path.joinpath("static/fonts/public_sans") -icon_font_dir = path.joinpath("static/css/fonts") def get_font(font_name, size=28): @@ -41,8 +40,6 @@ def get_font(font_name, size=28): font_path = "%s/PublicSans-Regular.ttf" % font_dir elif font_name == "bold": font_path = "%s/PublicSans-Bold.ttf" % font_dir - elif font_name == "icomoon": - font_path = "%s/icomoon.ttf" % icon_font_dir try: font = ImageFont.truetype(font_path, size) @@ -52,27 +49,44 @@ def get_font(font_name, size=28): return font -def generate_texts_layer(book, content_width): - font_title = get_font("bold", size=48) - font_authors = get_font("regular", size=40) +def generate_texts_layer(texts, content_width): + font_text_zero = get_font("bold", size=20) + font_text_one = get_font("bold", size=48) + font_text_two = get_font("bold", size=40) + font_text_three = get_font("regular", size=40) text_layer = Image.new("RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR) text_layer_draw = ImageDraw.Draw(text_layer) text_y = 0 - # title - title = textwrap.fill(book.title, width=28) - text_layer_draw.multiline_text((0, text_y), title, font=font_title, fill=TEXT_COLOR) + if 'text_zero' in texts: + # Text one (Book title) + text_zero = textwrap.fill(texts['text_zero'], width=72) + text_layer_draw.multiline_text((0, text_y), text_zero, font=font_text_zero, fill=TEXT_COLOR) - text_y = text_y + font_title.getsize_multiline(title)[1] + 16 + text_y = text_y + font_text_zero.getsize_multiline(text_zero)[1] + 16 - # subtitle - authors_text = book.author_text - authors = textwrap.fill(authors_text, width=36) - text_layer_draw.multiline_text( - (0, text_y), authors, font=font_authors, fill=TEXT_COLOR - ) + if 'text_one' in texts: + # Text one (Book title) + text_one = textwrap.fill(texts['text_one'], width=28) + text_layer_draw.multiline_text((0, text_y), text_one, font=font_text_one, fill=TEXT_COLOR) + + text_y = text_y + font_text_one.getsize_multiline(text_one)[1] + 16 + + if 'text_two' in texts: + # Text one (Book subtitle) + text_two = textwrap.fill(texts['text_two'], width=36) + text_layer_draw.multiline_text((0, text_y), text_two, font=font_text_two, fill=TEXT_COLOR) + + text_y = text_y + font_text_one.getsize_multiline(text_two)[1] + 16 + + if 'text_three' in texts: + # Text three (Book authors) + text_three = textwrap.fill(texts['text_three'], width=36) + text_layer_draw.multiline_text( + (0, text_y), text_three, font=font_text_three, fill=TEXT_COLOR + ) text_layer_box = text_layer.getbbox() return text_layer.crop(text_layer_box) @@ -109,8 +123,6 @@ def generate_instance_layer(content_width): def generate_rating_layer(rating, content_width): - font_icons = get_font("icomoon", size=60) - icon_star_full = Image.open(path.joinpath("static/images/icons/star-full.png")) icon_star_empty = Image.open(path.joinpath("static/images/icons/star-empty.png")) icon_star_half = Image.open(path.joinpath("static/images/icons/star-half.png")) @@ -159,7 +171,7 @@ def generate_default_cover(): ) default_cover_draw = ImageDraw.Draw(default_cover) - text = "no cover :(" + text = "no image :(" text_dimensions = font_cover.getsize(text) text_coords = ( math.floor((cover_width - text_dimensions[0]) / 2), @@ -170,20 +182,12 @@ def generate_default_cover(): return default_cover -def generate_preview_image(book_id, rating=None): - book = models.Book.objects.select_subclasses().get(id=book_id) - - rating = models.Review.objects.filter( - privacy="public", - deleted=False, - book__in=[book_id], - ).aggregate(Avg("rating"))["rating__avg"] - +def generate_preview_image(book, texts={}, picture=None, rating=None): # Cover try: - cover_img_layer = Image.open(book.cover) + cover_img_layer = Image.open(picture) cover_img_layer.thumbnail((cover_img_limits, cover_img_limits), Image.ANTIALIAS) - color_thief = ColorThief(book.cover) + color_thief = ColorThief(picture) dominant_color = color_thief.get_color(quality=1) except: cover_img_layer = generate_default_cover() @@ -214,7 +218,7 @@ def generate_preview_image(book_id, rating=None): content_width = IMG_WIDTH - content_x - margin instance_layer = generate_instance_layer(content_width) - texts_layer = generate_texts_layer(book, content_width) + texts_layer = generate_texts_layer(texts, content_width) contents_layer = Image.new( "RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR @@ -248,8 +252,33 @@ def generate_preview_image(book_id, rating=None): img.paste(cover_img_layer, (margin, cover_y)) img.alpha_composite(contents_layer, (content_x, contents_y)) - file_name = "%s.png" % str(uuid4()) + return img + +@app.task +def generate_edition_preview_image_task(book_id): + """generate preview_image""" + book = models.Book.objects.select_subclasses().get(id=book_id) + + rating = models.Review.objects.filter( + privacy="public", + deleted=False, + book__in=[book_id], + ).aggregate(Avg("rating"))["rating__avg"] + + texts = { + 'text_zero': "ADDED A REVIEW", + 'text_one': book.title, + 'text_two': book.subtitle, + 'text_three': book.author_text + } + + img = generate_preview_image(book=book, + texts=texts, + picture=book.cover, + rating=rating) + + file_name = "%s.png" % str(uuid4()) image_buffer = BytesIO() try: try: @@ -274,9 +303,3 @@ def generate_preview_image(book_id, rating=None): os.remove(old_path) finally: image_buffer.close() - - -@app.task -def generate_preview_image_from_edition_task(book_id): - """generate preview_image""" - generate_preview_image(book_id=book_id) From 34caa36ab70baa51bdeea11cc396289be0e0eebd Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 10:19:39 +0200 Subject: [PATCH 010/130] Add site preview task --- bookwyrm/models/book.py | 2 +- bookwyrm/models/site.py | 13 +++++++ bookwyrm/preview_images.py | 80 +++++++++++++++++++++++++++++++------- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 01dfbba72..aa9a56017 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -86,7 +86,7 @@ class Book(BookDataModel): upload_to="covers/", blank=True, null=True, alt_field="alt_text" ) preview_image = models.ImageField( - upload_to="cover_previews/", blank=True, null=True + upload_to="previews/covers/", blank=True, null=True ) first_published_date = fields.DateTimeField(blank=True, null=True) published_date = fields.DateTimeField(blank=True, null=True) diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 2c5a21642..dc226bce4 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -4,9 +4,12 @@ import datetime from Crypto import Random from django.db import models, IntegrityError +from django.dispatch import receiver from django.utils import timezone +from bookwyrm.preview_images import generate_site_preview_image_task from bookwyrm.settings import DOMAIN +from bookwyrm.tasks import app from .base_model import BookWyrmModel from .user import User @@ -35,6 +38,7 @@ class SiteSettings(models.Model): logo = models.ImageField(upload_to="logos/", null=True, blank=True) logo_small = models.ImageField(upload_to="logos/", null=True, blank=True) favicon = models.ImageField(upload_to="logos/", null=True, blank=True) + preview_image = models.ImageField(upload_to="previews/logos/", null=True, blank=True) # footer support_link = models.CharField(max_length=255, null=True, blank=True) @@ -119,3 +123,12 @@ class PasswordReset(models.Model): def link(self): """formats the invite link""" return "https://{}/password-reset/{}".format(DOMAIN, self.code) + + +@receiver(models.signals.post_save, sender=SiteSettings) +# pylint: disable=unused-argument +def preview_image(instance, *args, **kwargs): + updated_fields = kwargs["update_fields"] + + if not updated_fields or "preview_image" not in updated_fields: + generate_site_preview_image_task.delay() diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 82e8dc798..949ffac12 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -14,6 +14,7 @@ from django.core.files.uploadedfile import InMemoryUploadedFile from django.db.models import Avg from bookwyrm import models, settings +from bookwyrm.settings import DOMAIN from bookwyrm.tasks import app # dev @@ -182,7 +183,7 @@ def generate_default_cover(): return default_cover -def generate_preview_image(book, texts={}, picture=None, rating=None): +def generate_preview_image(texts={}, picture=None, rating=None, show_instance_layer=True): # Cover try: cover_img_layer = Image.open(picture) @@ -217,31 +218,35 @@ def generate_preview_image(book, texts={}, picture=None, rating=None): content_x = margin + cover_img_layer.width + gutter content_width = IMG_WIDTH - content_x - margin - instance_layer = generate_instance_layer(content_width) - texts_layer = generate_texts_layer(texts, content_width) - contents_layer = Image.new( "RGBA", (content_width, IMG_HEIGHT), color=TRANSPARENT_COLOR ) contents_composite_y = 0 - contents_layer.alpha_composite(instance_layer, (0, contents_composite_y)) - contents_composite_y = contents_composite_y + instance_layer.height + gutter + + if show_instance_layer: + instance_layer = generate_instance_layer(content_width) + contents_layer.alpha_composite(instance_layer, (0, contents_composite_y)) + contents_composite_y = contents_composite_y + instance_layer.height + gutter + + texts_layer = generate_texts_layer(texts, content_width) contents_layer.alpha_composite(texts_layer, (0, contents_composite_y)) - contents_composite_y = contents_composite_y + texts_layer.height + 30 + contents_composite_y = contents_composite_y + texts_layer.height + gutter if rating: # Add some more margin - contents_composite_y = contents_composite_y + 30 + contents_composite_y = contents_composite_y + gutter rating_layer = generate_rating_layer(rating, content_width) contents_layer.alpha_composite(rating_layer, (0, contents_composite_y)) - contents_composite_y = contents_composite_y + rating_layer.height + 30 + contents_composite_y = contents_composite_y + rating_layer.height + gutter contents_layer_box = contents_layer.getbbox() contents_layer_height = contents_layer_box[3] - contents_layer_box[1] contents_y = math.floor((IMG_HEIGHT - contents_layer_height) / 2) - # Remove Instance Layer from centering calculations - contents_y = contents_y - math.floor((instance_layer.height + gutter) / 2) + + if show_instance_layer: + # Remove Instance Layer from centering calculations + contents_y = contents_y - math.floor((instance_layer.height + gutter) / 2) if contents_y < margin: contents_y = margin @@ -255,9 +260,56 @@ def generate_preview_image(book, texts={}, picture=None, rating=None): return img +@app.task +def generate_site_preview_image_task(): + """generate preview_image for the website""" + site = models.SiteSettings.objects.get() + + if site.logo: + logo = site.logo + else: + logo = path.joinpath("static/images/logo-small.png") + + texts = { + 'text_zero': DOMAIN, + 'text_one': site.name, + 'text_three': site.instance_tagline, + } + + img = generate_preview_image(texts=texts, + picture=logo, + show_instance_layer=False) + + file_name = "%s.png" % str(uuid4()) + image_buffer = BytesIO() + try: + try: + old_path = site.preview_image.path + except ValueError: + old_path = "" + + # Save + img.save(image_buffer, format="png") + site.preview_image = InMemoryUploadedFile( + ContentFile(image_buffer.getvalue()), + "preview_image", + file_name, + "image/png", + image_buffer.tell(), + None, + ) + site.save(update_fields=["preview_image"]) + + # Clean up old file after saving + if os.path.exists(old_path): + os.remove(old_path) + finally: + image_buffer.close() + + @app.task def generate_edition_preview_image_task(book_id): - """generate preview_image""" + """generate preview_image for a book""" book = models.Book.objects.select_subclasses().get(id=book_id) rating = models.Review.objects.filter( @@ -267,14 +319,12 @@ def generate_edition_preview_image_task(book_id): ).aggregate(Avg("rating"))["rating__avg"] texts = { - 'text_zero': "ADDED A REVIEW", 'text_one': book.title, 'text_two': book.subtitle, 'text_three': book.author_text } - img = generate_preview_image(book=book, - texts=texts, + img = generate_preview_image(texts=texts, picture=book.cover, rating=rating) From bf503d370ce05dca92ffe711c5349aab1e3bfce8 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 12:54:57 +0200 Subject: [PATCH 011/130] Add user preview task --- bookwyrm/models/user.py | 14 ++++ bookwyrm/preview_images.py | 135 +++++++++++++++++++------------------ 2 files changed, 83 insertions(+), 66 deletions(-) diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index d9f3eba99..2c4851520 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -6,6 +6,7 @@ from django.apps import apps from django.contrib.auth.models import AbstractUser, Group from django.contrib.postgres.fields import CICharField from django.core.validators import MinValueValidator +from django.dispatch import receiver from django.db import models from django.utils import timezone import pytz @@ -14,6 +15,7 @@ from bookwyrm import activitypub from bookwyrm.connectors import get_data, ConnectorException from bookwyrm.models.shelf import Shelf from bookwyrm.models.status import Status, Review +from bookwyrm.preview_images import generate_user_preview_image_task from bookwyrm.settings import DOMAIN from bookwyrm.signatures import create_key_pair from bookwyrm.tasks import app @@ -70,6 +72,9 @@ class User(OrderedCollectionPageMixin, AbstractUser): activitypub_field="icon", alt_field="alt_text", ) + preview_image = models.ImageField( + upload_to="previews/avatars/", blank=True, null=True + ) followers = fields.ManyToManyField( "self", link_only=True, @@ -443,3 +448,12 @@ def get_remote_reviews(outbox): if not activity["type"] == "Review": continue activitypub.Review(**activity).to_model() + + +@receiver(models.signals.post_save, sender=User) +# pylint: disable=unused-argument +def preview_image(instance, *args, **kwargs): + updated_fields = kwargs["update_fields"] + + if not updated_fields or "preview_image" not in updated_fields: + generate_user_preview_image_task.delay(instance.id) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 949ffac12..304250a21 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -29,7 +29,7 @@ TRANSPARENT_COLOR = (0, 0, 0, 0) margin = math.floor(IMG_HEIGHT / 10) gutter = math.floor(margin / 2) -cover_img_limits = math.floor(IMG_HEIGHT * 0.8) +inner_img_limits = math.floor(IMG_HEIGHT * 0.8) path = Path(__file__).parent.absolute() font_dir = path.joinpath("static/fonts/public_sans") @@ -163,12 +163,12 @@ def generate_rating_layer(rating, content_width): return rating_layer_composite -def generate_default_cover(): +def generate_default_inner_img(): font_cover = get_font("light", size=28) - cover_width = math.floor(cover_img_limits * 0.7) + cover_width = math.floor(inner_img_limits * 0.7) default_cover = Image.new( - "RGB", (cover_width, cover_img_limits), color=DEFAULT_COVER_COLOR + "RGB", (cover_width, inner_img_limits), color=DEFAULT_COVER_COLOR ) default_cover_draw = ImageDraw.Draw(default_cover) @@ -176,7 +176,7 @@ def generate_default_cover(): text_dimensions = font_cover.getsize(text) text_coords = ( math.floor((cover_width - text_dimensions[0]) / 2), - math.floor((cover_img_limits - text_dimensions[1]) / 2), + math.floor((inner_img_limits - text_dimensions[1]) / 2), ) default_cover_draw.text(text_coords, text, font=font_cover, fill="white") @@ -186,14 +186,15 @@ def generate_default_cover(): def generate_preview_image(texts={}, picture=None, rating=None, show_instance_layer=True): # Cover try: - cover_img_layer = Image.open(picture) - cover_img_layer.thumbnail((cover_img_limits, cover_img_limits), Image.ANTIALIAS) + inner_img_layer = Image.open(picture) + inner_img_layer.thumbnail((inner_img_limits, inner_img_limits), Image.ANTIALIAS) color_thief = ColorThief(picture) dominant_color = color_thief.get_color(quality=1) except: - cover_img_layer = generate_default_cover() + inner_img_layer = generate_default_inner_img() dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR) + # Color if BG_COLOR == "use_dominant_color": image_bg_color = "rgb(%s, %s, %s)" % dominant_color @@ -215,7 +216,7 @@ def generate_preview_image(texts={}, picture=None, rating=None, show_instance_la img = Image.new("RGBA", (IMG_WIDTH, IMG_HEIGHT), color=image_bg_color) # Contents - content_x = margin + cover_img_layer.width + gutter + content_x = margin + inner_img_layer.width + gutter content_width = IMG_WIDTH - content_x - margin contents_layer = Image.new( @@ -251,15 +252,45 @@ def generate_preview_image(texts={}, picture=None, rating=None, show_instance_la if contents_y < margin: contents_y = margin - cover_y = math.floor((IMG_HEIGHT - cover_img_layer.height) / 2) + cover_y = math.floor((IMG_HEIGHT - inner_img_layer.height) / 2) # Composite layers - img.paste(cover_img_layer, (margin, cover_y)) + img.paste(inner_img_layer, (margin, cover_y), inner_img_layer.convert('RGBA')) img.alpha_composite(contents_layer, (content_x, contents_y)) return img +def save_and_cleanup(image, instance=None): + if instance: + file_name = "%s.png" % str(uuid4()) + image_buffer = BytesIO() + + try: + try: + old_path = instance.preview_image.path + except ValueError: + old_path = "" + + # Save + image.save(image_buffer, format="png") + instance.preview_image = InMemoryUploadedFile( + ContentFile(image_buffer.getvalue()), + "preview_image", + file_name, + "image/png", + image_buffer.tell(), + None, + ) + instance.save(update_fields=["preview_image"]) + + # Clean up old file after saving + if os.path.exists(old_path): + os.remove(old_path) + finally: + image_buffer.close() + + @app.task def generate_site_preview_image_task(): """generate preview_image for the website""" @@ -268,7 +299,7 @@ def generate_site_preview_image_task(): if site.logo: logo = site.logo else: - logo = path.joinpath("static/images/logo-small.png") + logo = path.joinpath("static/images/logo.png") texts = { 'text_zero': DOMAIN, @@ -276,35 +307,11 @@ def generate_site_preview_image_task(): 'text_three': site.instance_tagline, } - img = generate_preview_image(texts=texts, - picture=logo, - show_instance_layer=False) + image = generate_preview_image(texts=texts, + picture=logo, + show_instance_layer=False) - file_name = "%s.png" % str(uuid4()) - image_buffer = BytesIO() - try: - try: - old_path = site.preview_image.path - except ValueError: - old_path = "" - - # Save - img.save(image_buffer, format="png") - site.preview_image = InMemoryUploadedFile( - ContentFile(image_buffer.getvalue()), - "preview_image", - file_name, - "image/png", - image_buffer.tell(), - None, - ) - site.save(update_fields=["preview_image"]) - - # Clean up old file after saving - if os.path.exists(old_path): - os.remove(old_path) - finally: - image_buffer.close() + save_and_cleanup(image, instance=site) @app.task @@ -324,32 +331,28 @@ def generate_edition_preview_image_task(book_id): 'text_three': book.author_text } - img = generate_preview_image(texts=texts, - picture=book.cover, - rating=rating) + image = generate_preview_image(texts=texts, + picture=book.cover, + rating=rating) - file_name = "%s.png" % str(uuid4()) - image_buffer = BytesIO() - try: - try: - old_path = book.preview_image.path - except ValueError: - old_path = "" + save_and_cleanup(image, instance=book) - # Save - img.save(image_buffer, format="png") - book.preview_image = InMemoryUploadedFile( - ContentFile(image_buffer.getvalue()), - "preview_image", - file_name, - "image/png", - image_buffer.tell(), - None, - ) - book.save(update_fields=["preview_image"]) +@app.task +def generate_user_preview_image_task(user_id): + """generate preview_image for a book""" + user = models.User.objects.get(id=user_id) - # Clean up old file after saving - if os.path.exists(old_path): - os.remove(old_path) - finally: - image_buffer.close() + texts = { + 'text_one': user.display_name, + 'text_three': "@{}@{}".format(user.localname, DOMAIN) + } + + if user.avatar: + avatar = user.avatar + else: + avatar = path.joinpath("static/images/default_avi.jpg") + + image = generate_preview_image(texts=texts, + picture=avatar) + + save_and_cleanup(image, instance=user) From b47edc5f0d15945044346791982cdd9fb4803845 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 13:07:33 +0200 Subject: [PATCH 012/130] Add dark mode --- bookwyrm/preview_images.py | 13 ++++++++++--- bookwyrm/settings.py | 7 ++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 304250a21..9539edbaf 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -196,14 +196,21 @@ def generate_preview_image(texts={}, picture=None, rating=None, show_instance_la # Color - if BG_COLOR == "use_dominant_color": + if BG_COLOR in ["use_dominant_color_light", "use_dominant_color_dark"]: image_bg_color = "rgb(%s, %s, %s)" % dominant_color - # Lighten color + + # Adjust color image_bg_color_rgb = [x / 255.0 for x in ImageColor.getrgb(image_bg_color)] image_bg_color_hls = colorsys.rgb_to_hls(*image_bg_color_rgb) + + if BG_COLOR == "use_dominant_color_light": + lightness = max(0.9, image_bg_color_hls[1]) + else: + lightness = min(0.15, image_bg_color_hls[1]) + image_bg_color_hls = ( image_bg_color_hls[0], - max(0.9, image_bg_color_hls[1]), + lightness, image_bg_color_hls[2], ) image_bg_color = tuple( diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index db15be468..ce5c4547d 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -39,11 +39,12 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Preview image -# Specify RGB tuple or RGB hex strings, or 'use_dominant_color' -PREVIEW_BG_COLOR = "use_dominant_color" +# Specify RGB tuple or RGB hex strings, +# or "use_dominant_color_light" / "use_dominant_color_dark" +PREVIEW_BG_COLOR = "use_dominant_color_dark" PREVIEW_IMG_WIDTH = 1200 PREVIEW_IMG_HEIGHT = 630 -PREVIEW_TEXT_COLOR = "#363636" +PREVIEW_TEXT_COLOR = "#FFF" PREVIEW_DEFAULT_COVER_COLOR = "#002549" # Quick-start development settings - unsuitable for production From c894f5ef3546e3d6d5bb2a24d52ead1826662a6c Mon Sep 17 00:00:00 2001 From: Fabien Basmaison Date: Wed, 26 May 2021 13:21:06 +0200 Subject: [PATCH 013/130] Update subtitle behaviour: - on Book - Remove problematic punctuation (locale and multiple punctuation if the title ends with `?`, `!` or similar). - Update view. - Use proper semantic to split combined title into `name`, `alternativeHeadline` and series-related microdata. - The author is not a subtitle, just data. - Use parenthesis in the `get_title` filter instead of punctuation. --- bookwyrm/templates/book/book.html | 39 ++++++++++++++++++------------ bookwyrm/templatetags/utilities.py | 2 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index af2230200..94f326e65 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -8,28 +8,35 @@
-

- - {{ book.title }}{% if book.subtitle %}: - {{ book.subtitle }} +

+ {{ book.title }} +

+ + {% if book.subtitle or book.series %} +

+ {% if book.subtitle %} + + + {{ book.subtitle }} {% endif %} - - {% if book.series %} - - + {% if book.series %} + + - ({{ book.series }} {% if book.series_number %} #{{ book.series_number }}{% endif %}) - -
- {% endif %} -

+ {% endif %} +

+ {% endif %} + {% if book.authors %} -

- {% trans "by" %} {% include 'snippets/authors.html' with book=book %} -

+
+ {% trans "by" %} {% include 'snippets/authors.html' with book=book %} +
{% endif %}
diff --git a/bookwyrm/templatetags/utilities.py b/bookwyrm/templatetags/utilities.py index 68befa54a..ae66ca6e1 100644 --- a/bookwyrm/templatetags/utilities.py +++ b/bookwyrm/templatetags/utilities.py @@ -25,7 +25,7 @@ def get_title(book): return "" title = book.title if len(title) < 6 and book.subtitle: - title = "{:s}: {:s}".format(title, book.subtitle) + title = "{:s} ({:s})".format(title, book.subtitle) return title From 65de40a95a8aa64a9bdeb563c25f8e88a1a9fb7a Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 13:52:10 +0200 Subject: [PATCH 014/130] Add `generate_preview_images` command --- .../commands/generate_preview_images.py | 48 +++++++++++++++++++ bookwyrm/preview_images.py | 1 + bookwyrm/settings.py | 4 +- bw-dev | 5 +- 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 bookwyrm/management/commands/generate_preview_images.py diff --git a/bookwyrm/management/commands/generate_preview_images.py b/bookwyrm/management/commands/generate_preview_images.py new file mode 100644 index 000000000..13eaf3a68 --- /dev/null +++ b/bookwyrm/management/commands/generate_preview_images.py @@ -0,0 +1,48 @@ +""" Generate preview images """ +import sys + +from django.core.management.base import BaseCommand + +from bookwyrm import activitystreams, models, settings, preview_images + + +def generate_preview_images(): + """generate preview images""" + print(" | Hello! I will be generating preview images for your instance.") + print("🧑‍🎨 ⎨ This might take quite long if your instance has a lot of books and users.") + print(" | ✧ Thank you for your patience ✧") + + # Site + sys.stdout.write(" → Site preview image: ") + preview_images.generate_site_preview_image_task() + sys.stdout.write(" OK 🖼\n") + + + # Users + users = models.User.objects.filter( + local=True, + is_active=True, + ) + sys.stdout.write(" → User preview images ({}): ".format(len(users))) + for user in users: + preview_images.generate_user_preview_image_task(user.id) + sys.stdout.write(".") + sys.stdout.write(" OK 🖼\n") + + # Books + books = models.Book.objects.select_subclasses().filter() + sys.stdout.write(" → Book preview images ({}): ".format(len(books))) + for book in books: + preview_images.generate_edition_preview_image_task(book.id) + sys.stdout.write(".") + sys.stdout.write(" OK 🖼\n") + + print("🧑‍🎨 ⎨ I’m all done! ✧ Enjoy ✧") + + +class Command(BaseCommand): + help = "Generate preview images" + # pylint: disable=no-self-use,unused-argument + def handle(self, *args, **options): + """run feed builder""" + generate_preview_images() diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 9539edbaf..b0f9792d7 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -344,6 +344,7 @@ def generate_edition_preview_image_task(book_id): save_and_cleanup(image, instance=book) + @app.task def generate_user_preview_image_task(user_id): """generate preview_image for a book""" diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index ce5c4547d..b3ba312f9 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -41,10 +41,10 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Specify RGB tuple or RGB hex strings, # or "use_dominant_color_light" / "use_dominant_color_dark" -PREVIEW_BG_COLOR = "use_dominant_color_dark" +PREVIEW_BG_COLOR = "use_dominant_color_light" PREVIEW_IMG_WIDTH = 1200 PREVIEW_IMG_HEIGHT = 630 -PREVIEW_TEXT_COLOR = "#FFF" +PREVIEW_TEXT_COLOR = "#363636" # Change to "#FFF" if you use "use_dominant_color_dark" PREVIEW_DEFAULT_COVER_COLOR = "#002549" # Quick-start development settings - unsuitable for production diff --git a/bw-dev b/bw-dev index c2b63bc17..95681b115 100755 --- a/bw-dev +++ b/bw-dev @@ -107,7 +107,10 @@ case "$CMD" in populate_streams) runweb python manage.py populate_streams ;; + generate_preview_images) + runweb python manage.py generate_preview_images + ;; *) - echo "Unrecognised command. Try: build, clean, up, initdb, resetdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, test, pytest, test_report, black, populate_feeds" + echo "Unrecognised command. Try: build, clean, up, initdb, resetdb, makemigrations, migrate, bash, shell, dbshell, restart_celery, test, pytest, test_report, black, populate_feeds, generate_preview_images" ;; esac From e5e549d125660544fae55c81d11e9e4dd6459edc Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 14:44:15 +0200 Subject: [PATCH 015/130] Add opengraph image depending on context --- bookwyrm/templates/book/book.html | 7 ++++++- bookwyrm/templates/layout.html | 6 ++++-- bookwyrm/templates/user/layout.html | 10 ++++++---- bookwyrm/templatetags/layout.py | 7 +++++++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index af2230200..09d5634bf 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -1,8 +1,13 @@ {% extends 'layout.html' %} -{% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %} +{% load i18n %}{% load bookwyrm_tags %}{% load humanize %}{% load utilities %}{% load layout %} {% block title %}{{ book|book_title }}{% endblock %} +{% block opengraph_images %} + + +{% endblock %} + {% block content %} {% with user_authenticated=request.user.is_authenticated can_edit_book=perms.bookwyrm.edit_book %}
diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index d899d62cb..bd3dbf7c1 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -16,8 +16,10 @@ - - + {% block opengraph_images %} + + + {% endblock %} diff --git a/bookwyrm/templates/user/layout.html b/bookwyrm/templates/user/layout.html index 0830a4068..c1503ec6d 100644 --- a/bookwyrm/templates/user/layout.html +++ b/bookwyrm/templates/user/layout.html @@ -1,11 +1,13 @@ {% extends 'layout.html' %} -{% load i18n %} -{% load humanize %} -{% load utilities %} -{% load markdown %} +{% load i18n %}{% load humanize %}{% load utilities %}{% load markdown %}{% load layout %} {% block title %}{{ user.display_name }}{% endblock %} +{% block opengraph_images %} + + +{% endblock %} + {% block content %}
{% block header %} diff --git a/bookwyrm/templatetags/layout.py b/bookwyrm/templatetags/layout.py index e0f1d8ba6..f518808c1 100644 --- a/bookwyrm/templatetags/layout.py +++ b/bookwyrm/templatetags/layout.py @@ -1,6 +1,7 @@ """ template filters used for creating the layout""" from django import template, utils +from bookwyrm.settings import DOMAIN register = template.Library() @@ -10,3 +11,9 @@ def get_lang(): """get current language, strip to the first two letters""" language = utils.translation.get_language() return language[0 : language.find("-")] + + +@register.simple_tag(takes_context=False) +def get_path(): + """get protocol and host""" + return "https://%s" % DOMAIN From eb56cced8d19319d622b866f36307cb583fda8ab Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 14:46:34 +0200 Subject: [PATCH 016/130] Lint --- .../commands/generate_preview_images.py | 5 +- bookwyrm/models/site.py | 4 +- bookwyrm/preview_images.py | 62 ++++++++++--------- bookwyrm/settings.py | 2 +- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/bookwyrm/management/commands/generate_preview_images.py b/bookwyrm/management/commands/generate_preview_images.py index 13eaf3a68..754f6e9a9 100644 --- a/bookwyrm/management/commands/generate_preview_images.py +++ b/bookwyrm/management/commands/generate_preview_images.py @@ -9,7 +9,9 @@ from bookwyrm import activitystreams, models, settings, preview_images def generate_preview_images(): """generate preview images""" print(" | Hello! I will be generating preview images for your instance.") - print("🧑‍🎨 ⎨ This might take quite long if your instance has a lot of books and users.") + print( + "🧑‍🎨 ⎨ This might take quite long if your instance has a lot of books and users." + ) print(" | ✧ Thank you for your patience ✧") # Site @@ -17,7 +19,6 @@ def generate_preview_images(): preview_images.generate_site_preview_image_task() sys.stdout.write(" OK 🖼\n") - # Users users = models.User.objects.filter( local=True, diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index dc226bce4..d0076dc63 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -38,7 +38,9 @@ class SiteSettings(models.Model): logo = models.ImageField(upload_to="logos/", null=True, blank=True) logo_small = models.ImageField(upload_to="logos/", null=True, blank=True) favicon = models.ImageField(upload_to="logos/", null=True, blank=True) - preview_image = models.ImageField(upload_to="previews/logos/", null=True, blank=True) + preview_image = models.ImageField( + upload_to="previews/logos/", null=True, blank=True + ) # footer support_link = models.CharField(max_length=255, null=True, blank=True) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index b0f9792d7..8b6f90324 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -61,30 +61,36 @@ def generate_texts_layer(texts, content_width): text_y = 0 - if 'text_zero' in texts: + if "text_zero" in texts: # Text one (Book title) - text_zero = textwrap.fill(texts['text_zero'], width=72) - text_layer_draw.multiline_text((0, text_y), text_zero, font=font_text_zero, fill=TEXT_COLOR) + text_zero = textwrap.fill(texts["text_zero"], width=72) + text_layer_draw.multiline_text( + (0, text_y), text_zero, font=font_text_zero, fill=TEXT_COLOR + ) text_y = text_y + font_text_zero.getsize_multiline(text_zero)[1] + 16 - if 'text_one' in texts: + if "text_one" in texts: # Text one (Book title) - text_one = textwrap.fill(texts['text_one'], width=28) - text_layer_draw.multiline_text((0, text_y), text_one, font=font_text_one, fill=TEXT_COLOR) + text_one = textwrap.fill(texts["text_one"], width=28) + text_layer_draw.multiline_text( + (0, text_y), text_one, font=font_text_one, fill=TEXT_COLOR + ) text_y = text_y + font_text_one.getsize_multiline(text_one)[1] + 16 - if 'text_two' in texts: + if "text_two" in texts: # Text one (Book subtitle) - text_two = textwrap.fill(texts['text_two'], width=36) - text_layer_draw.multiline_text((0, text_y), text_two, font=font_text_two, fill=TEXT_COLOR) + text_two = textwrap.fill(texts["text_two"], width=36) + text_layer_draw.multiline_text( + (0, text_y), text_two, font=font_text_two, fill=TEXT_COLOR + ) text_y = text_y + font_text_one.getsize_multiline(text_two)[1] + 16 - if 'text_three' in texts: + if "text_three" in texts: # Text three (Book authors) - text_three = textwrap.fill(texts['text_three'], width=36) + text_three = textwrap.fill(texts["text_three"], width=36) text_layer_draw.multiline_text( (0, text_y), text_three, font=font_text_three, fill=TEXT_COLOR ) @@ -183,7 +189,9 @@ def generate_default_inner_img(): return default_cover -def generate_preview_image(texts={}, picture=None, rating=None, show_instance_layer=True): +def generate_preview_image( + texts={}, picture=None, rating=None, show_instance_layer=True +): # Cover try: inner_img_layer = Image.open(picture) @@ -194,7 +202,6 @@ def generate_preview_image(texts={}, picture=None, rating=None, show_instance_la inner_img_layer = generate_default_inner_img() dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR) - # Color if BG_COLOR in ["use_dominant_color_light", "use_dominant_color_dark"]: image_bg_color = "rgb(%s, %s, %s)" % dominant_color @@ -262,7 +269,7 @@ def generate_preview_image(texts={}, picture=None, rating=None, show_instance_la cover_y = math.floor((IMG_HEIGHT - inner_img_layer.height) / 2) # Composite layers - img.paste(inner_img_layer, (margin, cover_y), inner_img_layer.convert('RGBA')) + img.paste(inner_img_layer, (margin, cover_y), inner_img_layer.convert("RGBA")) img.alpha_composite(contents_layer, (content_x, contents_y)) return img @@ -309,14 +316,12 @@ def generate_site_preview_image_task(): logo = path.joinpath("static/images/logo.png") texts = { - 'text_zero': DOMAIN, - 'text_one': site.name, - 'text_three': site.instance_tagline, + "text_zero": DOMAIN, + "text_one": site.name, + "text_three": site.instance_tagline, } - image = generate_preview_image(texts=texts, - picture=logo, - show_instance_layer=False) + image = generate_preview_image(texts=texts, picture=logo, show_instance_layer=False) save_and_cleanup(image, instance=site) @@ -333,14 +338,12 @@ def generate_edition_preview_image_task(book_id): ).aggregate(Avg("rating"))["rating__avg"] texts = { - 'text_one': book.title, - 'text_two': book.subtitle, - 'text_three': book.author_text + "text_one": book.title, + "text_two": book.subtitle, + "text_three": book.author_text, } - image = generate_preview_image(texts=texts, - picture=book.cover, - rating=rating) + image = generate_preview_image(texts=texts, picture=book.cover, rating=rating) save_and_cleanup(image, instance=book) @@ -351,8 +354,8 @@ def generate_user_preview_image_task(user_id): user = models.User.objects.get(id=user_id) texts = { - 'text_one': user.display_name, - 'text_three': "@{}@{}".format(user.localname, DOMAIN) + "text_one": user.display_name, + "text_three": "@{}@{}".format(user.localname, DOMAIN), } if user.avatar: @@ -360,7 +363,6 @@ def generate_user_preview_image_task(user_id): else: avatar = path.joinpath("static/images/default_avi.jpg") - image = generate_preview_image(texts=texts, - picture=avatar) + image = generate_preview_image(texts=texts, picture=avatar) save_and_cleanup(image, instance=user) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index b3ba312f9..86ad5c035 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -44,7 +44,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" PREVIEW_BG_COLOR = "use_dominant_color_light" PREVIEW_IMG_WIDTH = 1200 PREVIEW_IMG_HEIGHT = 630 -PREVIEW_TEXT_COLOR = "#363636" # Change to "#FFF" if you use "use_dominant_color_dark" +PREVIEW_TEXT_COLOR = "#363636" # Change to "#FFF" if you use "use_dominant_color_dark" PREVIEW_DEFAULT_COVER_COLOR = "#002549" # Quick-start development settings - unsuitable for production From 4db8aa85f06859d6070a69865a978073e03b0500 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 14:55:55 +0200 Subject: [PATCH 017/130] Fix migration --- .../migrations/0076_book_preview_image.py | 18 ------------ bookwyrm/migrations/0076_preview_images.py | 28 +++++++++++++++++++ 2 files changed, 28 insertions(+), 18 deletions(-) delete mode 100644 bookwyrm/migrations/0076_book_preview_image.py create mode 100644 bookwyrm/migrations/0076_preview_images.py diff --git a/bookwyrm/migrations/0076_book_preview_image.py b/bookwyrm/migrations/0076_book_preview_image.py deleted file mode 100644 index 5db550507..000000000 --- a/bookwyrm/migrations/0076_book_preview_image.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2 on 2021-05-24 18:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("bookwyrm", "0075_announcement"), - ] - - operations = [ - migrations.AddField( - model_name="book", - name="preview_image", - field=models.ImageField(blank=True, null=True, upload_to="cover_previews/"), - ), - ] diff --git a/bookwyrm/migrations/0076_preview_images.py b/bookwyrm/migrations/0076_preview_images.py new file mode 100644 index 000000000..c2f251df0 --- /dev/null +++ b/bookwyrm/migrations/0076_preview_images.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2 on 2021-05-26 12:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0075_announcement'), + ] + + operations = [ + migrations.AddField( + model_name='book', + name='preview_image', + field=models.ImageField(blank=True, null=True, upload_to='previews/covers/'), + ), + migrations.AddField( + model_name='sitesettings', + name='preview_image', + field=models.ImageField(blank=True, null=True, upload_to='previews/logos/'), + ), + migrations.AddField( + model_name='user', + name='preview_image', + field=models.ImageField(blank=True, null=True, upload_to='previews/avatars/'), + ), + ] From d4fc1b0fdfec0b6b0d21e255bf402a28bf1b7e53 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 15:17:28 +0200 Subject: [PATCH 018/130] Fix line endings --- bookwyrm/static/fonts/public_sans/OFL.txt | 186 +++++++++++----------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/bookwyrm/static/fonts/public_sans/OFL.txt b/bookwyrm/static/fonts/public_sans/OFL.txt index ac793eaaa..0916c2309 100644 --- a/bookwyrm/static/fonts/public_sans/OFL.txt +++ b/bookwyrm/static/fonts/public_sans/OFL.txt @@ -1,93 +1,93 @@ -Copyright (c) 2015, Pablo Impallari, Rodrigo Fuenzalida (Modified by Dan O. Williams and USWDS) (https://github.com/uswds/public-sans) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2015, Pablo Impallari, Rodrigo Fuenzalida (Modified by Dan O. Williams and USWDS) (https://github.com/uswds/public-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. From 5943e6d79acd955ce78f8a7cb3e755dfafd664a7 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 15:18:05 +0200 Subject: [PATCH 019/130] Black --- bookwyrm/migrations/0076_preview_images.py | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/bookwyrm/migrations/0076_preview_images.py b/bookwyrm/migrations/0076_preview_images.py index c2f251df0..8812e9b22 100644 --- a/bookwyrm/migrations/0076_preview_images.py +++ b/bookwyrm/migrations/0076_preview_images.py @@ -6,23 +6,27 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('bookwyrm', '0075_announcement'), + ("bookwyrm", "0075_announcement"), ] operations = [ migrations.AddField( - model_name='book', - name='preview_image', - field=models.ImageField(blank=True, null=True, upload_to='previews/covers/'), + model_name="book", + name="preview_image", + field=models.ImageField( + blank=True, null=True, upload_to="previews/covers/" + ), ), migrations.AddField( - model_name='sitesettings', - name='preview_image', - field=models.ImageField(blank=True, null=True, upload_to='previews/logos/'), + model_name="sitesettings", + name="preview_image", + field=models.ImageField(blank=True, null=True, upload_to="previews/logos/"), ), migrations.AddField( - model_name='user', - name='preview_image', - field=models.ImageField(blank=True, null=True, upload_to='previews/avatars/'), + model_name="user", + name="preview_image", + field=models.ImageField( + blank=True, null=True, upload_to="previews/avatars/" + ), ), ] From 22c13f639c90d6e0eab9f8292f6f46970b60514b Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 15:18:35 +0200 Subject: [PATCH 020/130] Update layout.html --- bookwyrm/templates/user/layout.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/user/layout.html b/bookwyrm/templates/user/layout.html index c1503ec6d..5f09b8c27 100644 --- a/bookwyrm/templates/user/layout.html +++ b/bookwyrm/templates/user/layout.html @@ -1,5 +1,9 @@ {% extends 'layout.html' %} -{% load i18n %}{% load humanize %}{% load utilities %}{% load markdown %}{% load layout %} +{% load i18n %} +{% load humanize %} +{% load utilities %} +{% load markdown %} +{% load layout %} {% block title %}{{ user.display_name }}{% endblock %} From a8ae3c995015b8150607f1081c03c73dec832747 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 15:37:09 +0200 Subject: [PATCH 021/130] Modify inner image position --- bookwyrm/preview_images.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 8b6f90324..13b241b1d 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -17,9 +17,6 @@ from bookwyrm import models, settings from bookwyrm.settings import DOMAIN from bookwyrm.tasks import app -# dev -import logging - IMG_WIDTH = settings.PREVIEW_IMG_WIDTH IMG_HEIGHT = settings.PREVIEW_IMG_HEIGHT BG_COLOR = settings.PREVIEW_BG_COLOR @@ -29,7 +26,8 @@ TRANSPARENT_COLOR = (0, 0, 0, 0) margin = math.floor(IMG_HEIGHT / 10) gutter = math.floor(margin / 2) -inner_img_limits = math.floor(IMG_HEIGHT * 0.8) +inner_img_height = math.floor(IMG_HEIGHT * 0.8) +inner_img_width = math.floor(inner_img_height * 0.7) path = Path(__file__).parent.absolute() font_dir = path.joinpath("static/fonts/public_sans") @@ -172,17 +170,16 @@ def generate_rating_layer(rating, content_width): def generate_default_inner_img(): font_cover = get_font("light", size=28) - cover_width = math.floor(inner_img_limits * 0.7) default_cover = Image.new( - "RGB", (cover_width, inner_img_limits), color=DEFAULT_COVER_COLOR + "RGB", (inner_img_width, inner_img_height), color=DEFAULT_COVER_COLOR ) default_cover_draw = ImageDraw.Draw(default_cover) text = "no image :(" text_dimensions = font_cover.getsize(text) text_coords = ( - math.floor((cover_width - text_dimensions[0]) / 2), - math.floor((inner_img_limits - text_dimensions[1]) / 2), + math.floor((inner_img_width - text_dimensions[0]) / 2), + math.floor((inner_img_height - text_dimensions[1]) / 2), ) default_cover_draw.text(text_coords, text, font=font_cover, fill="white") @@ -195,7 +192,7 @@ def generate_preview_image( # Cover try: inner_img_layer = Image.open(picture) - inner_img_layer.thumbnail((inner_img_limits, inner_img_limits), Image.ANTIALIAS) + inner_img_layer.thumbnail((inner_img_width, inner_img_height), Image.ANTIALIAS) color_thief = ColorThief(picture) dominant_color = color_thief.get_color(quality=1) except: @@ -230,7 +227,9 @@ def generate_preview_image( img = Image.new("RGBA", (IMG_WIDTH, IMG_HEIGHT), color=image_bg_color) # Contents - content_x = margin + inner_img_layer.width + gutter + inner_img_x = margin + inner_img_width - inner_img_layer.width + inner_img_y = math.floor((IMG_HEIGHT - inner_img_layer.height) / 2) + content_x = margin + inner_img_width + gutter content_width = IMG_WIDTH - content_x - margin contents_layer = Image.new( @@ -266,10 +265,8 @@ def generate_preview_image( if contents_y < margin: contents_y = margin - cover_y = math.floor((IMG_HEIGHT - inner_img_layer.height) / 2) - # Composite layers - img.paste(inner_img_layer, (margin, cover_y), inner_img_layer.convert("RGBA")) + img.paste(inner_img_layer, (inner_img_x, inner_img_y), inner_img_layer.convert("RGBA")) img.alpha_composite(contents_layer, (content_x, contents_y)) return img From f7b117e4fb981b081847ffe1eaa346564351af0f Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 15:46:40 +0200 Subject: [PATCH 022/130] Update preview_images.py --- bookwyrm/preview_images.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bookwyrm/preview_images.py b/bookwyrm/preview_images.py index 13b241b1d..875755547 100644 --- a/bookwyrm/preview_images.py +++ b/bookwyrm/preview_images.py @@ -266,7 +266,9 @@ def generate_preview_image( contents_y = margin # Composite layers - img.paste(inner_img_layer, (inner_img_x, inner_img_y), inner_img_layer.convert("RGBA")) + img.paste( + inner_img_layer, (inner_img_x, inner_img_y), inner_img_layer.convert("RGBA") + ) img.alpha_composite(contents_layer, (content_x, contents_y)) return img From 3ea935e7ce6473c1ff9713b86939cf1aa61c4341 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 15:49:08 +0200 Subject: [PATCH 023/130] Update OFL.txt --- bookwyrm/static/fonts/public_sans/OFL.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/static/fonts/public_sans/OFL.txt b/bookwyrm/static/fonts/public_sans/OFL.txt index 0916c2309..ba4ea0b96 100644 --- a/bookwyrm/static/fonts/public_sans/OFL.txt +++ b/bookwyrm/static/fonts/public_sans/OFL.txt @@ -1,4 +1,5 @@ -Copyright (c) 2015, Pablo Impallari, Rodrigo Fuenzalida (Modified by Dan O. Williams and USWDS) (https://github.com/uswds/public-sans) +Copyright (c) 2015, Pablo Impallari, Rodrigo Fuenzalida +(Modified by Dan O. Williams and USWDS) (https://github.com/uswds/public-sans) This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: From 7ea31530269721d7ab2856b3ec5d1186836795da Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 16:57:28 +0200 Subject: [PATCH 024/130] Fix site_path tag --- bookwyrm/context_processors.py | 2 ++ bookwyrm/templates/book/book.html | 4 ++-- bookwyrm/templates/layout.html | 4 ++-- bookwyrm/templates/user/layout.html | 4 ++-- bookwyrm/templatetags/layout.py | 8 -------- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/bookwyrm/context_processors.py b/bookwyrm/context_processors.py index b77c62b02..29775a0b5 100644 --- a/bookwyrm/context_processors.py +++ b/bookwyrm/context_processors.py @@ -1,5 +1,6 @@ """ customize the info available in context for rendering templates """ from bookwyrm import models +from bookwyrm.settings import DOMAIN def site_settings(request): # pylint: disable=unused-argument @@ -7,4 +8,5 @@ def site_settings(request): # pylint: disable=unused-argument return { "site": models.SiteSettings.objects.get(), "active_announcements": models.Announcement.active_announcements(), + "site_path": "https://%s" % DOMAIN, } diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 09d5634bf..02409b389 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -4,8 +4,8 @@ {% block title %}{{ book|book_title }}{% endblock %} {% block opengraph_images %} - - + + {% endblock %} {% block content %} diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index bd3dbf7c1..47bd434e2 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -17,8 +17,8 @@ {% block opengraph_images %} - - + + {% endblock %} diff --git a/bookwyrm/templates/user/layout.html b/bookwyrm/templates/user/layout.html index 5f09b8c27..45b37f763 100644 --- a/bookwyrm/templates/user/layout.html +++ b/bookwyrm/templates/user/layout.html @@ -8,8 +8,8 @@ {% block title %}{{ user.display_name }}{% endblock %} {% block opengraph_images %} - - + + {% endblock %} {% block content %} diff --git a/bookwyrm/templatetags/layout.py b/bookwyrm/templatetags/layout.py index f518808c1..f42f3bda1 100644 --- a/bookwyrm/templatetags/layout.py +++ b/bookwyrm/templatetags/layout.py @@ -1,8 +1,6 @@ """ template filters used for creating the layout""" from django import template, utils -from bookwyrm.settings import DOMAIN - register = template.Library() @@ -11,9 +9,3 @@ def get_lang(): """get current language, strip to the first two letters""" language = utils.translation.get_language() return language[0 : language.find("-")] - - -@register.simple_tag(takes_context=False) -def get_path(): - """get protocol and host""" - return "https://%s" % DOMAIN From e362c8249563c2c2c371b1136481c6b4c8b1a970 Mon Sep 17 00:00:00 2001 From: Joachim Date: Wed, 26 May 2021 17:54:59 +0200 Subject: [PATCH 025/130] Expose static & media paths --- bookwyrm/context_processors.py | 8 ++++++-- bookwyrm/settings.py | 4 ++++ bookwyrm/templates/book/book.html | 4 ++-- bookwyrm/templates/layout.html | 8 ++++---- bookwyrm/templates/user/layout.html | 4 ++-- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/bookwyrm/context_processors.py b/bookwyrm/context_processors.py index 29775a0b5..dcdf615d5 100644 --- a/bookwyrm/context_processors.py +++ b/bookwyrm/context_processors.py @@ -1,6 +1,6 @@ """ customize the info available in context for rendering templates """ from bookwyrm import models -from bookwyrm.settings import DOMAIN +from bookwyrm.settings import SITE_PATH, STATIC_URL, STATIC_PATH, MEDIA_URL, MEDIA_PATH def site_settings(request): # pylint: disable=unused-argument @@ -8,5 +8,9 @@ def site_settings(request): # pylint: disable=unused-argument return { "site": models.SiteSettings.objects.get(), "active_announcements": models.Announcement.active_announcements(), - "site_path": "https://%s" % DOMAIN, + "site_path": SITE_PATH, + "static_url": STATIC_URL, + "media_url": MEDIA_URL, + "static_path": STATIC_PATH, + "media_path": MEDIA_PATH, } diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 86ad5c035..47553c65f 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -182,10 +182,14 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ +SITE_PATH = "https://%s" % DOMAIN + PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) STATIC_URL = "/static/" +STATIC_PATH = "%s/%s" % (SITE_PATH, env("STATIC_ROOT", "static")) STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) MEDIA_URL = "/images/" +MEDIA_PATH = "%s/%s" % (SITE_PATH, env("MEDIA_ROOT", "images")) MEDIA_ROOT = os.path.join(BASE_DIR, env("MEDIA_ROOT", "images")) USER_AGENT = "%s (BookWyrm/%s; +https://%s/)" % ( diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index 02409b389..7b47e32b6 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -4,8 +4,8 @@ {% block title %}{{ book|book_title }}{% endblock %} {% block opengraph_images %} - - + + {% endblock %} {% block content %} diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 47bd434e2..625a224e6 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -8,7 +8,7 @@ - + @@ -17,8 +17,8 @@ {% block opengraph_images %} - - + + {% endblock %} @@ -27,7 +27,7 @@