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 """
import sys
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):
"""Creates previews for existing objects"""
help = "Generate preview images"
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 os
import textwrap
from colorthief import ColorThief
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageColor
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.uploadedfile import InMemoryUploadedFile
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):
"""Loads custom font"""
if font_name == "light":
font_path = os.path.join(font_dir, "PublicSans-Light.ttf")
if font_name == "regular":
@ -47,6 +49,7 @@ def get_font(font_name, size=28):
def generate_texts_layer(texts, content_width):
"""Adds text for images"""
font_text_zero = get_font("bold", size=20)
font_text_one = get_font("bold", size=48)
font_text_two = get_font("bold", size=40)
@ -66,7 +69,7 @@ def generate_texts_layer(texts, content_width):
try:
text_y = text_y + font_text_zero.getsize_multiline(text_zero)[1] + 16
except:
except (AttributeError, IndexError):
text_y = text_y + 26
if "text_one" in texts and texts["text_one"]:
@ -78,7 +81,7 @@ def generate_texts_layer(texts, content_width):
try:
text_y = text_y + font_text_one.getsize_multiline(text_one)[1] + 16
except:
except (AttributeError, IndexError):
text_y = text_y + 26
if "text_two" in texts and texts["text_two"]:
@ -90,7 +93,7 @@ def generate_texts_layer(texts, content_width):
try:
text_y = text_y + font_text_one.getsize_multiline(text_two)[1] + 16
except:
except (AttributeError, IndexError):
text_y = text_y + 26
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):
"""Places components for instance preview"""
font_instance = get_font("light", size=28)
site = models.SiteSettings.objects.get()
@ -145,57 +149,56 @@ def generate_instance_layer(content_width):
def generate_rating_layer(rating, content_width):
try:
icon_star_full = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-full.png")
)
icon_star_empty = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-empty.png")
)
icon_star_half = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-half.png")
)
"""Places components for rating preview"""
icon_star_full = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-full.png")
)
icon_star_empty = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-empty.png")
)
icon_star_half = Image.open(
os.path.join(settings.STATIC_ROOT, "images/icons/star-half.png")
)
icon_size = 64
icon_margin = 10
icon_size = 64
icon_margin = 10
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_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
)
position_x = 0
position_x = 0
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
for _ 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
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
for _ 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_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
except:
return None
return rating_layer_composite
def generate_default_inner_img():
"""Adds cover image"""
font_cover = get_font("light", size=28)
default_cover = Image.new(
@ -214,16 +217,19 @@ def generate_default_inner_img():
return default_cover
# pylint: disable=too-many-locals
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
try:
inner_img_layer = Image.open(picture)
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:
except: # pylint: disable=bare-except
inner_img_layer = generate_default_inner_img()
dominant_color = ImageColor.getrgb(DEFAULT_COVER_COLOR)
@ -246,7 +252,7 @@ def generate_preview_image(
image_bg_color_hls[2],
)
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:
image_bg_color = BG_COLOR
@ -292,8 +298,7 @@ def generate_preview_image(
# Remove Instance Layer from centering calculations
contents_y = contents_y - math.floor((instance_layer.height + gutter) / 2)
if contents_y < margin:
contents_y = margin
contents_y = max(contents_y, margin)
# Composite layers
img.paste(
@ -305,108 +310,116 @@ def generate_preview_image(
def save_and_cleanup(image, instance=None):
if isinstance(instance, (models.Book, models.User, models.SiteSettings)):
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:
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:
"""Save and close the file"""
if not isinstance(instance, (models.Book, models.User, models.SiteSettings)):
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
def generate_site_preview_image_task():
"""generate preview_image for the website"""
if settings.ENABLE_PREVIEW_IMAGES == True:
site = models.SiteSettings.objects.get()
if not settings.ENABLE_PREVIEW_IMAGES:
return
if site.logo:
logo = site.logo
else:
logo = os.path.join(settings.STATIC_ROOT, "images/logo.png")
site = models.SiteSettings.objects.get()
texts = {
"text_zero": settings.DOMAIN,
"text_one": site.name,
"text_three": site.instance_tagline,
}
if site.logo:
logo = site.logo
else:
logo = os.path.join(settings.STATIC_ROOT, "images/logo.png")
image = generate_preview_image(
texts=texts, picture=logo, show_instance_layer=False
)
texts = {
"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
def generate_edition_preview_image_task(book_id):
"""generate preview_image for a book"""
if settings.ENABLE_PREVIEW_IMAGES == True:
book = models.Book.objects.select_subclasses().get(id=book_id)
if not settings.ENABLE_PREVIEW_IMAGES:
return
rating = models.Review.objects.filter(
privacy="public",
deleted=False,
book__in=[book_id],
).aggregate(Avg("rating"))["rating__avg"]
book = models.Book.objects.select_subclasses().get(id=book_id)
texts = {
"text_one": book.title,
"text_two": book.subtitle,
"text_three": book.author_text,
}
rating = models.Review.objects.filter(
privacy="public",
deleted=False,
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
def generate_user_preview_image_task(user_id):
"""generate preview_image for a book"""
if settings.ENABLE_PREVIEW_IMAGES == True:
user = models.User.objects.get(id=user_id)
if not settings.ENABLE_PREVIEW_IMAGES:
return
texts = {
"text_one": user.display_name,
"text_three": "@{}@{}".format(user.localname, settings.DOMAIN),
}
user = models.User.objects.get(id=user_id)
if user.avatar:
avatar = user.avatar
else:
avatar = os.path.join(settings.STATIC_ROOT, "images/default_avi.jpg")
texts = {
"text_one": user.display_name,
"text_three": "@{}@{}".format(user.localname, settings.DOMAIN),
}
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,
)
import logging
# pylint: disable=unused-argument
# pylint: disable=missing-function-docstring
class PreviewImages(TestCase):
"""every response to a get request, html or json"""