mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-22 08:07:14 +00:00
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?
This commit is contained in:
parent
ea0f7ff925
commit
d1737b44bd
10 changed files with 268 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -27,3 +27,6 @@
|
|||
|
||||
#nginx
|
||||
nginx/default.conf
|
||||
|
||||
#macOS
|
||||
**/.DS_Store
|
||||
|
|
21
bookwyrm/migrations/0076_book_preview_image.py
Normal file
21
bookwyrm/migrations/0076_book_preview_image.py
Normal file
|
@ -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/"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
133
bookwyrm/preview_images.py
Normal file
133
bookwyrm/preview_images.py
Normal file
|
@ -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)
|
|
@ -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/
|
||||
|
||||
|
|
93
bookwyrm/static/fonts/public_sans/OFL.txt
Normal file
93
bookwyrm/static/fonts/public_sans/OFL.txt
Normal file
|
@ -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.
|
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Bold.ttf
Normal file
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Bold.ttf
Normal file
Binary file not shown.
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Light.ttf
Normal file
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Light.ttf
Normal file
Binary file not shown.
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Regular.ttf
Normal file
BIN
bookwyrm/static/fonts/public_sans/PublicSans-Regular.ttf
Normal file
Binary file not shown.
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue