b81bd64ffd
by making the handler's queue an init parameter.
192 lines
6.7 KiB
Python
192 lines
6.7 KiB
Python
import re
|
|
import textwrap
|
|
|
|
from os import path, rename
|
|
from shutil import copy2
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
from .handler import Handler
|
|
from .metadata import MetadataHandler
|
|
from ..exceptions import ValidationException
|
|
|
|
@Handler.register
|
|
class ThumbnailHandler(Handler):
|
|
"""
|
|
This class handles presentation thumbnails.
|
|
|
|
If the 'thumb' field is present in the jobspec, does one of the following:
|
|
- Saves the uploaded thumb to the package
|
|
- Generates a thumb on falsy 'thumb' value
|
|
|
|
In order to accept a job as valid, the fields 'pkg_id' and 'thumb'
|
|
must exist.
|
|
If 'thumb' has a truthy value, 'upload_dir' must also exist and 'thumb'
|
|
must point to a valid file under 'upload_dir'
|
|
"""
|
|
@classmethod
|
|
def wants(cls, jobspec, existing_package):
|
|
if 'thumb' in jobspec:
|
|
return cls._validate(jobspec, existing_package)
|
|
return False
|
|
|
|
@classmethod
|
|
def _validate(cls, jobspec, existing_package):
|
|
super()._validate(jobspec, existing_package)
|
|
if jobspec['thumb']:
|
|
if 'upload_dir' not in jobspec:
|
|
raise ValidationException("upload_dir missing")
|
|
if not path.isfile(path.join(jobspec['upload_dir'],
|
|
jobspec['thumb'])):
|
|
raise ValidationException("invalid thumb value")
|
|
return True
|
|
|
|
@classmethod
|
|
def apply_after(cls, handlerclass):
|
|
if issubclass(handlerclass, MetadataHandler):
|
|
return True
|
|
return False
|
|
|
|
def _handle(self, jobspec, existing_package, tempdir):
|
|
if jobspec['thumb']:
|
|
def apply_func(package):
|
|
filename = path.basename(jobspec['thumb'])
|
|
copy2(path.join(jobspec['upload_dir'],
|
|
jobspec['thumb']),
|
|
path.join(package.basedir, filename))
|
|
package['thumb'] = filename
|
|
else:
|
|
def apply_func(package):
|
|
title = package['title']['sv']
|
|
description = package['description']
|
|
courses = ', '.join(sorted(
|
|
[course['designation'] for course in package['courses']]))
|
|
presenters = ', '.join(sorted(
|
|
[self.ldap.get_name(uid)
|
|
for uid in package['presenters']]))
|
|
extras = []
|
|
if courses:
|
|
extras.append(courses)
|
|
if presenters:
|
|
extras.append(presenters)
|
|
extra = ': '.join(extras)
|
|
|
|
thumb = make_thumb(tempdir,
|
|
self.config['baseimage'],
|
|
self.config['textcolor'],
|
|
title,
|
|
description,
|
|
extra)
|
|
rename(path.join(tempdir, thumb),
|
|
path.join(package.basedir, thumb))
|
|
package['thumb'] = thumb
|
|
|
|
return apply_func
|
|
|
|
|
|
def make_thumb(tempdir, baseimg, textcolor,
|
|
title, description, extra):
|
|
"""
|
|
Generates a thumbnail image.
|
|
"""
|
|
|
|
thumbname = 'thumb.png'
|
|
font = 'verdana.ttf'
|
|
titlesize_max = 150
|
|
descrsize_max = 80
|
|
extrasize_max = 60
|
|
titlebox = (1/9, 0.3,
|
|
8/9, 0.5)
|
|
descrbox = (1/9, 0.55,
|
|
8/9, 0.8)
|
|
|
|
with Image.open(baseimg) as im:
|
|
draw = ImageDraw.Draw(im)
|
|
|
|
# Size and draw title
|
|
s_titlebox = scale_box(titlebox, im.size)
|
|
(t1x, t1y, t2x, t2y) = s_titlebox
|
|
title_size = max_size(draw, s_titlebox, fix_outliers(title),
|
|
font, titlesize_max)
|
|
draw.text((t1x, t2y), title, fill=textcolor, anchor='ls',
|
|
font=ImageFont.truetype(font, title_size))
|
|
|
|
# Size and draw description
|
|
s_descrbox = scale_box(descrbox, im.size)
|
|
(d1x, d1y, d2x, d2y) = s_descrbox
|
|
descr = textwrap.fill(description)
|
|
descr_size = max_size(draw, s_descrbox, descr, font, descrsize_max)
|
|
f = ImageFont.truetype(font, descr_size)
|
|
draw.multiline_text((d1x, d1y), descr, fill=textcolor, font=f)
|
|
|
|
# Size and draw extra
|
|
if extra:
|
|
# Get effective size of the description text
|
|
(b1x, b1y, b2x, b2y) = draw.textbbox((d1x, d1y),
|
|
descr,
|
|
font=f)
|
|
# Create a preliminary bounding box for extra
|
|
# Same width as description box, immediately underneath
|
|
# extending to bottom of image
|
|
extrabox = (d1x, b2y,
|
|
d2x, im.height)
|
|
# Make sure the text fits in the box
|
|
extra_size = max_size(draw, extrabox, extra,
|
|
font, extrasize_max)
|
|
# Offset the extra box by one line height
|
|
extrabox = (extrabox[0], extrabox[1] + extra_size,
|
|
extrabox[2], extrabox[3])
|
|
(p1x, p1y, p2x, p2y) = extrabox
|
|
f = ImageFont.truetype(font, extra_size)
|
|
# Draw it
|
|
draw.text((p1x, p1y), extra, fill=textcolor,
|
|
font=f, anchor='lt')
|
|
|
|
im.save(path.join(tempdir, thumbname))
|
|
return thumbname
|
|
|
|
def scale_box(inbox, imgsize):
|
|
"""
|
|
Scales the relative measurements in inbox to imgsize
|
|
"""
|
|
(i1x, i1y, i2x, i2y) = inbox
|
|
(w, h) = imgsize
|
|
return (i1x*w, i1y*h,
|
|
i2x*w, i2y*h)
|
|
|
|
def max_size(draw, inbox, text, font, size):
|
|
"""
|
|
Returns the maximum font size for text that will fit in inbox.
|
|
|
|
Starts at size and shrinks the text in 5pt increments until it fits.
|
|
"""
|
|
anchor='lt'
|
|
if '\n' in text:
|
|
anchor=None
|
|
f = ImageFont.truetype(font, size)
|
|
(i1x, i1y, i2x, i2y) = inbox
|
|
(b1x, b1y, b2x, b2y) = draw.textbbox((i1x, i1y), text,
|
|
font=f, anchor=anchor)
|
|
if (b1x < i1x or b1y < i1y or
|
|
b2x > i2x or b2y > i2y):
|
|
return max_size(draw, inbox, text, font, size-5)
|
|
return size
|
|
|
|
def fix_outliers(text):
|
|
"""
|
|
Replaces characters that extend up/down past the top line and/or the
|
|
baseline, in order to get consistent bounding box information for any
|
|
string. Replacements attempt to stick close to the look of the
|
|
initial string.
|
|
|
|
For details on the reference lines, see:
|
|
https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html
|
|
"""
|
|
modifiers = [('[ÖQ]', 'O'),
|
|
('[ÅÄ]', 'A'),
|
|
('y', 'v'),
|
|
('[qpg]', 'o'),
|
|
('j', 'i')]
|
|
for (pat, rep) in modifiers:
|
|
text = re.sub(pat, rep, text)
|
|
return text
|