Linting fixes for preview image code

This commit is contained in:
Mouse Reeve 2021-06-18 15:08:36 -07:00
parent a20c4d583c
commit d8d6f57373
3 changed files with 146 additions and 133 deletions

View file

@ -1,12 +1,12 @@
""" Generate preview images """ """ Generate preview images """
import sys
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from bookwyrm import activitystreams, models, settings, preview_images from bookwyrm import models, preview_images
# pylint: disable=line-too-long
class Command(BaseCommand): class Command(BaseCommand):
"""Creates previews for existing objects"""
help = "Generate preview images" help = "Generate preview images"
def add_arguments(self, parser): def add_arguments(self, parser):

View file

@ -1,13 +1,14 @@
import colorsys """ Generate social media preview images for twitter/mastodon/etc """
import math import math
import os import os
import textwrap import textwrap
from colorthief import ColorThief
from io import BytesIO from io import BytesIO
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageColor
from uuid import uuid4 from uuid import uuid4
import colorsys
from colorthief import ColorThief
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageColor
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db.models import Avg from django.db.models import Avg
@ -31,6 +32,7 @@ font_dir = os.path.join(settings.STATIC_ROOT, "fonts/public_sans")
def get_font(font_name, size=28): def get_font(font_name, size=28):
"""Loads custom font"""
if font_name == "light": if font_name == "light":
font_path = os.path.join(font_dir, "PublicSans-Light.ttf") font_path = os.path.join(font_dir, "PublicSans-Light.ttf")
if font_name == "regular": if font_name == "regular":
@ -47,6 +49,7 @@ def get_font(font_name, size=28):
def generate_texts_layer(texts, content_width): def generate_texts_layer(texts, content_width):
"""Adds text for images"""
font_text_zero = get_font("bold", size=20) font_text_zero = get_font("bold", size=20)
font_text_one = get_font("bold", size=48) font_text_one = get_font("bold", size=48)
font_text_two = get_font("bold", size=40) font_text_two = get_font("bold", size=40)
@ -66,7 +69,7 @@ def generate_texts_layer(texts, content_width):
try: try:
text_y = text_y + font_text_zero.getsize_multiline(text_zero)[1] + 16 text_y = text_y + font_text_zero.getsize_multiline(text_zero)[1] + 16
except: except (AttributeError, IndexError):
text_y = text_y + 26 text_y = text_y + 26
if "text_one" in texts and texts["text_one"]: if "text_one" in texts and texts["text_one"]:
@ -78,7 +81,7 @@ def generate_texts_layer(texts, content_width):
try: try:
text_y = text_y + font_text_one.getsize_multiline(text_one)[1] + 16 text_y = text_y + font_text_one.getsize_multiline(text_one)[1] + 16
except: except (AttributeError, IndexError):
text_y = text_y + 26 text_y = text_y + 26
if "text_two" in texts and texts["text_two"]: if "text_two" in texts and texts["text_two"]:
@ -90,7 +93,7 @@ def generate_texts_layer(texts, content_width):
try: try:
text_y = text_y + font_text_one.getsize_multiline(text_two)[1] + 16 text_y = text_y + font_text_one.getsize_multiline(text_two)[1] + 16
except: except (AttributeError, IndexError):
text_y = text_y + 26 text_y = text_y + 26
if "text_three" in texts and texts["text_three"]: if "text_three" in texts and texts["text_three"]:
@ -105,6 +108,7 @@ def generate_texts_layer(texts, content_width):
def generate_instance_layer(content_width): def generate_instance_layer(content_width):
"""Places components for instance preview"""
font_instance = get_font("light", size=28) font_instance = get_font("light", size=28)
site = models.SiteSettings.objects.get() site = models.SiteSettings.objects.get()
@ -145,57 +149,56 @@ def generate_instance_layer(content_width):
def generate_rating_layer(rating, content_width): def generate_rating_layer(rating, content_width):
try: """Places components for rating preview"""
icon_star_full = Image.open( icon_star_full = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-full.png") os.path.join(settings.STATIC_ROOT, "images/icons/star-full.png")
) )
icon_star_empty = Image.open( icon_star_empty = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-empty.png") os.path.join(settings.STATIC_ROOT, "images/icons/star-empty.png")
) )
icon_star_half = Image.open( icon_star_half = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-half.png") os.path.join(settings.STATIC_ROOT, "images/icons/star-half.png")
) )
icon_size = 64 icon_size = 64
icon_margin = 10 icon_margin = 10
rating_layer_base = Image.new( rating_layer_base = Image.new(
"RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR "RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR
) )
rating_layer_color = Image.new( rating_layer_color = Image.new(
"RGBA", (content_width, icon_size), color=TEXT_COLOR "RGBA", (content_width, icon_size), color=TEXT_COLOR
) )
rating_layer_mask = Image.new( rating_layer_mask = Image.new(
"RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR "RGBA", (content_width, icon_size), color=TRANSPARENT_COLOR
) )
position_x = 0 position_x = 0
for r in range(math.floor(rating)): for _ in range(math.floor(rating)):
rating_layer_mask.alpha_composite(icon_star_full, (position_x, 0)) rating_layer_mask.alpha_composite(icon_star_full, (position_x, 0))
position_x = position_x + icon_size + icon_margin position_x = position_x + icon_size + icon_margin
if math.floor(rating) != math.ceil(rating): if math.floor(rating) != math.ceil(rating):
rating_layer_mask.alpha_composite(icon_star_half, (position_x, 0)) rating_layer_mask.alpha_composite(icon_star_half, (position_x, 0))
position_x = position_x + icon_size + icon_margin position_x = position_x + icon_size + icon_margin
for r in range(5 - math.ceil(rating)): for _ in range(5 - math.ceil(rating)):
rating_layer_mask.alpha_composite(icon_star_empty, (position_x, 0)) rating_layer_mask.alpha_composite(icon_star_empty, (position_x, 0))
position_x = position_x + icon_size + icon_margin position_x = position_x + icon_size + icon_margin
rating_layer_mask = rating_layer_mask.getchannel("A") rating_layer_mask = rating_layer_mask.getchannel("A")
rating_layer_mask = ImageOps.invert(rating_layer_mask) rating_layer_mask = ImageOps.invert(rating_layer_mask)
rating_layer_composite = Image.composite( rating_layer_composite = Image.composite(
rating_layer_base, rating_layer_color, rating_layer_mask rating_layer_base, rating_layer_color, rating_layer_mask
) )
return rating_layer_composite return rating_layer_composite
except:
return None
def generate_default_inner_img(): def generate_default_inner_img():
"""Adds cover image"""
font_cover = get_font("light", size=28) font_cover = get_font("light", size=28)
default_cover = Image.new( default_cover = Image.new(
@ -214,16 +217,19 @@ def generate_default_inner_img():
return default_cover return default_cover
# pylint: disable=too-many-locals
def generate_preview_image( def generate_preview_image(
texts={}, picture=None, rating=None, show_instance_layer=True texts=None, picture=None, rating=None, show_instance_layer=True
): ):
"""Puts everything together"""
texts = texts or {}
# Cover # Cover
try: try:
inner_img_layer = Image.open(picture) inner_img_layer = Image.open(picture)
inner_img_layer.thumbnail((inner_img_width, inner_img_height), Image.ANTIALIAS) inner_img_layer.thumbnail((inner_img_width, inner_img_height), Image.ANTIALIAS)
color_thief = ColorThief(picture) color_thief = ColorThief(picture)
dominant_color = color_thief.get_color(quality=1) dominant_color = color_thief.get_color(quality=1)
except: except: # pylint: disable=bare-except
inner_img_layer = generate_default_inner_img() inner_img_layer = generate_default_inner_img()
dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR) dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR)
@ -246,7 +252,7 @@ def generate_preview_image(
image_bg_color_hls[2], image_bg_color_hls[2],
) )
image_bg_color = tuple( image_bg_color = tuple(
[math.ceil(x * 255) for x in colorsys.hls_to_rgb(*image_bg_color_hls)] math.ceil(x * 255) for x in colorsys.hls_to_rgb(*image_bg_color_hls)
) )
else: else:
image_bg_color = BG_COLOR image_bg_color = BG_COLOR
@ -292,8 +298,7 @@ def generate_preview_image(
# Remove Instance Layer from centering calculations # Remove Instance Layer from centering calculations
contents_y = contents_y - math.floor((instance_layer.height + gutter) / 2) contents_y = contents_y - math.floor((instance_layer.height + gutter) / 2)
if contents_y < margin: contents_y = max(contents_y, margin)
contents_y = margin
# Composite layers # Composite layers
img.paste( img.paste(
@ -305,108 +310,116 @@ def generate_preview_image(
def save_and_cleanup(image, instance=None): def save_and_cleanup(image, instance=None):
if isinstance(instance, (models.Book, models.User, models.SiteSettings)): """Save and close the file"""
file_name = "%s-%s.jpg" % (str(instance.id), str(uuid4())) if not isinstance(instance, (models.Book, models.User, models.SiteSettings)):
image_buffer = BytesIO()
try:
try:
old_path = instance.preview_image.path
except ValueError:
old_path = ""
# Save
image.save(image_buffer, format="jpeg", quality=75)
instance.preview_image = InMemoryUploadedFile(
ContentFile(image_buffer.getvalue()),
"preview_image",
file_name,
"image/jpg",
image_buffer.tell(),
None,
)
save_without_broadcast = isinstance(instance, (models.Book, models.User))
if save_without_broadcast:
result = instance.save(broadcast=False)
else:
instance.save()
# Clean up old file after saving
if os.path.exists(old_path):
os.remove(old_path)
finally:
image_buffer.close()
return True
else:
return False return False
file_name = "%s-%s.jpg" % (str(instance.id), str(uuid4()))
image_buffer = BytesIO()
try:
try:
old_path = instance.preview_image.path
except ValueError:
old_path = ""
# Save
image.save(image_buffer, format="jpeg", quality=75)
instance.preview_image = InMemoryUploadedFile(
ContentFile(image_buffer.getvalue()),
"preview_image",
file_name,
"image/jpg",
image_buffer.tell(),
None,
)
save_without_broadcast = isinstance(instance, (models.Book, models.User))
if save_without_broadcast:
instance.save(broadcast=False)
else:
instance.save()
# Clean up old file after saving
if os.path.exists(old_path):
os.remove(old_path)
finally:
image_buffer.close()
return True
# pylint: disable=invalid-name
@app.task @app.task
def generate_site_preview_image_task(): def generate_site_preview_image_task():
"""generate preview_image for the website""" """generate preview_image for the website"""
if settings.ENABLE_PREVIEW_IMAGES == True: if not settings.ENABLE_PREVIEW_IMAGES:
site = models.SiteSettings.objects.get() return
if site.logo: site = models.SiteSettings.objects.get()
logo = site.logo
else:
logo = os.path.join(settings.STATIC_ROOT, "images/logo.png")
texts = { if site.logo:
"text_zero": settings.DOMAIN, logo = site.logo
"text_one": site.name, else:
"text_three": site.instance_tagline, logo = os.path.join(settings.STATIC_ROOT, "images/logo.png")
}
image = generate_preview_image( texts = {
texts=texts, picture=logo, show_instance_layer=False "text_zero": settings.DOMAIN,
) "text_one": site.name,
"text_three": site.instance_tagline,
}
save_and_cleanup(image, instance=site) image = generate_preview_image(
texts=texts, picture=logo, show_instance_layer=False
)
save_and_cleanup(image, instance=site)
# pylint: disable=invalid-name
@app.task @app.task
def generate_edition_preview_image_task(book_id): def generate_edition_preview_image_task(book_id):
"""generate preview_image for a book""" """generate preview_image for a book"""
if settings.ENABLE_PREVIEW_IMAGES == True: if not settings.ENABLE_PREVIEW_IMAGES:
book = models.Book.objects.select_subclasses().get(id=book_id) return
rating = models.Review.objects.filter( book = models.Book.objects.select_subclasses().get(id=book_id)
privacy="public",
deleted=False,
book__in=[book_id],
).aggregate(Avg("rating"))["rating__avg"]
texts = { rating = models.Review.objects.filter(
"text_one": book.title, privacy="public",
"text_two": book.subtitle, deleted=False,
"text_three": book.author_text, book__in=[book_id],
} ).aggregate(Avg("rating"))["rating__avg"]
image = generate_preview_image(texts=texts, picture=book.cover, rating=rating) texts = {
"text_one": book.title,
"text_two": book.subtitle,
"text_three": book.author_text,
}
save_and_cleanup(image, instance=book) image = generate_preview_image(texts=texts, picture=book.cover, rating=rating)
save_and_cleanup(image, instance=book)
@app.task @app.task
def generate_user_preview_image_task(user_id): def generate_user_preview_image_task(user_id):
"""generate preview_image for a book""" """generate preview_image for a book"""
if settings.ENABLE_PREVIEW_IMAGES == True: if not settings.ENABLE_PREVIEW_IMAGES:
user = models.User.objects.get(id=user_id) return
texts = { user = models.User.objects.get(id=user_id)
"text_one": user.display_name,
"text_three": "@{}@{}".format(user.localname, settings.DOMAIN),
}
if user.avatar: texts = {
avatar = user.avatar "text_one": user.display_name,
else: "text_three": "@{}@{}".format(user.localname, settings.DOMAIN),
avatar = os.path.join(settings.STATIC_ROOT, "images/default_avi.jpg") }
image = generate_preview_image(texts=texts, picture=avatar) if user.avatar:
avatar = user.avatar
else:
avatar = os.path.join(settings.STATIC_ROOT, "images/default_avi.jpg")
save_and_cleanup(image, instance=user) image = generate_preview_image(texts=texts, picture=avatar)
save_and_cleanup(image, instance=user)

View file

@ -18,9 +18,9 @@ from bookwyrm.preview_images import (
save_and_cleanup, save_and_cleanup,
) )
import logging
# pylint: disable=unused-argument
# pylint: disable=missing-function-docstring
class PreviewImages(TestCase): class PreviewImages(TestCase):
"""every response to a get request, html or json""" """every response to a get request, html or json"""