bookwyrm/bookwyrm/preview_images.py

134 lines
3.9 KiB
Python
Raw Normal View History

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)