134 lines
3.9 KiB
Python
134 lines
3.9 KiB
Python
|
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)
|