1 # -*- coding: utf-8 -*-
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
6 from __future__ import unicode_literals
9 from PIL import Image, ImageFont, ImageDraw, ImageFilter
10 from six import BytesIO
11 from librarian import get_resource, OutputFile, URLOpener
15 """Gets metrics from an object, scaling it by a factor."""
16 def __init__(self, obj, scale):
18 self._scale = float(scale)
20 def __getattr__(self, name):
21 src = getattr(self._obj, name)
22 if src and self._scale:
23 return type(src)(self._scale * src)
28 class TextBox(object):
29 """Creates an Image with a series of centered strings."""
35 def __init__(self, max_width, max_height, padding_x=None, padding_y=None):
37 padding_x = self.SHADOW_X + self.SHADOW_BLUR
39 padding_y = self.SHADOW_Y + self.SHADOW_BLUR
41 self.max_width = max_width
42 self.max_text_width = max_width - 2 * padding_x
43 self.padding_y = padding_y
44 self.height = padding_y
45 self.img = Image.new('RGBA', (max_width, max_height))
46 self.draw = ImageDraw.Draw(self.img)
47 self.shadow_img = None
48 self.shadow_draw = None
50 def skip(self, height):
51 """Skips some vertical space."""
54 def text(self, text, color='#000', font=None, line_height=20,
56 """Writes some centered text."""
57 text = re.sub(r'\s+', ' ', text)
59 if not self.shadow_img:
60 self.shadow_img = Image.new('RGBA', self.img.size)
61 self.shadow_draw = ImageDraw.Draw(self.shadow_img)
64 line_width = self.draw.textsize(line, font=font)[0]
65 while line_width > self.max_text_width:
66 parts = line.rsplit(' ', 1)
68 line_width = self.max_text_width
71 line_width = self.draw.textsize(line, font=font)[0]
72 line = line.strip() + ' '
74 pos_x = (self.max_width - line_width) // 2
77 self.shadow_draw.text(
78 (pos_x + self.SHADOW_X, self.height + self.SHADOW_Y),
79 line, font=font, fill=shadow_color
82 self.draw.text((pos_x, self.height), line, font=font, fill=color)
83 self.height += line_height
85 text = text[len(line):]
88 """Creates the actual Image object."""
89 image = Image.new('RGBA', (self.max_width,
90 self.height + self.padding_y))
92 shadow = self.shadow_img.filter(ImageFilter.BLUR)
93 image.paste(shadow, (0, 0), shadow)
94 image.paste(self.img, (0, 0), self.img)
96 image.paste(self.img, (0, 0))
101 """Abstract base class for cover images generator."""
104 background_color = '#fff'
105 background_img = None
108 author_margin_left = 20
109 author_margin_right = 20
111 author_color = '#000'
113 author_font_ttf = get_resource('fonts/DejaVuSerif.ttf')
114 author_font_size = 30
117 title_margin_left = 20
118 title_margin_right = 20
122 title_font_ttf = get_resource('fonts/DejaVuSerif.ttf')
127 uses_dc_cover = False
139 'JPEG': 'image/jpeg',
143 def __init__(self, book_info, format=None, width=None, height=None):
144 self.authors = [auth.readable() for auth in book_info.authors]
145 self.title = book_info.title
146 if format is not None:
149 self.height = int(round(height * self.width / width))
150 scale = max(float(width or 0) / self.width,
151 float(height or 0) / self.height)
155 self.scale_after = scale
157 def pretty_authors(self):
158 """Allows for decorating authors' names."""
159 return [self.authors]
161 def pretty_title(self):
162 """Allows for decorating title."""
166 metr = Metric(self, self.scale)
167 img = Image.new('RGB', (metr.width, metr.height),
168 self.background_color)
170 if self.background_img:
171 background = Image.open(self.background_img)
172 img.paste(background, None, background)
177 logo = Image.open(get_resource('res/wl-logo.png'))
180 int(round(logo.size[1] * metr.logo_width / logo.size[0]))
183 (metr.width - metr.logo_width) // 2,
184 img.size[1] - logo.size[1] - metr.logo_bottom
187 top = metr.author_top
189 metr.width - metr.author_margin_left - metr.author_margin_right,
193 author_font = ImageFont.truetype(
194 self.author_font_ttf, metr.author_font_size,
195 layout_engine=ImageFont.LAYOUT_BASIC)
196 for pa in self.pretty_authors():
197 tbox.text(pa, self.author_color, author_font, metr.author_lineskip,
199 text_img = tbox.image()
200 img.paste(text_img, (metr.author_margin_left, top), text_img)
202 top += text_img.size[1] + metr.title_top
204 metr.width - metr.title_margin_left - metr.title_margin_right,
207 title_font = ImageFont.truetype(
208 self.title_font_ttf, metr.title_font_size,
209 layout_engine=ImageFont.LAYOUT_BASIC)
210 tbox.text(self.pretty_title(), self.title_color, title_font,
211 metr.title_lineskip, self.title_shadow)
212 text_img = tbox.image()
213 img.paste(text_img, (metr.title_margin_left, top), text_img)
217 def final_image(self):
219 if self.scale_after != 1:
221 int(round(img.size[0] * self.scale_after)),
222 int(round(img.size[1] * self.scale_after))),
227 return self.mime_types[self.format]
230 return self.exts[self.format]
232 def save(self, *args, **kwargs):
234 'format': self.format,
237 default_kwargs.update(kwargs)
238 return self.final_image().save(*args, **default_kwargs)
240 def output_file(self, *args, **kwargs):
242 self.save(imgstr, *args, **kwargs)
243 return OutputFile.from_bytes(imgstr.getvalue())
246 class WLCover(Cover):
247 """Wolne Lektury cover without logos."""
251 author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
252 author_font_size = 20
254 title_font_ttf = get_resource('fonts/DejaVuSerif-Bold.ttf')
257 title_box_width = 350
260 box_bottom_margin = 100
273 box_position = 'middle'
274 background_color = '#444'
275 author_color = '#444'
276 background_img = get_resource('res/cover.png')
280 u'Starożytność': '#9e3610',
281 u'Średniowiecze': '#564c09',
282 u'Renesans': '#8ca629',
284 u'Oświecenie': '#f2802e',
285 u'Romantyzm': '#db4b16',
286 u'Pozytywizm': '#961060',
287 u'Modernizm': '#7784e0',
288 u'Dwudziestolecie międzywojenne': '#3044cf',
289 u'Współczesność': '#06393d',
292 kind_box_position = {
297 def __init__(self, book_info, format=None, width=None, height=None,
299 super(WLCover, self).__init__(book_info, format=format, width=width,
302 self.box_position = book_info.cover_box_position or \
303 self.kind_box_position.get(book_info.kind, self.box_position)
305 if book_info.cover_bar_color == 'none':
308 self.bar_color = book_info.cover_bar_color or \
309 self.epoch_colors.get(book_info.epoch, self.bar_color)
311 self.title_color = self.epoch_colors.get(book_info.epoch,
315 self.box_top_margin += bleed
316 self.box_bottom_margin += bleed
317 self.bar_width += bleed
319 if book_info.cover_url:
320 url = book_info.cover_url
323 bg_src = URLOpener().open(url)
324 self.background_img = BytesIO(bg_src.read())
327 def pretty_authors(self):
328 return [a.upper() for a in self.authors]
330 def add_box(self, img):
331 if self.box_position == 'none':
334 metr = Metric(self, self.scale)
337 box = TextBox(metr.title_box_width, metr.height,
338 padding_y=metr.box_padding_y)
339 author_font = ImageFont.truetype(
340 self.author_font_ttf, metr.author_font_size,
341 layout_engine=ImageFont.LAYOUT_BASIC)
342 for pa in self.pretty_authors():
343 box.text(pa, font=author_font, line_height=metr.author_lineskip,
344 color=self.author_color, shadow_color=self.author_shadow)
346 box.skip(metr.box_above_line)
348 (metr.box_line_left, box.height, metr.box_line_right, box.height),
349 fill=self.author_color, width=metr.box_line_width
351 box.skip(metr.box_below_line)
354 title_font = ImageFont.truetype(
355 self.title_font_ttf, metr.title_font_size,
356 layout_engine=ImageFont.LAYOUT_BASIC)
357 box.text(self.pretty_title(),
358 line_height=metr.title_lineskip,
360 color=self.title_color,
361 shadow_color=self.title_shadow)
363 box_img = box.image()
366 if self.box_position == 'bottom' or box_img.size[1] + metr.box_top_margin + metr.box_bottom_margin > metr.height:
367 box_top = metr.height - metr.box_bottom_margin - box_img.size[1]
368 elif self.box_position == 'top':
369 box_top = metr.box_top_margin
371 box_top = (metr.height - box_img.size[1]) // 2
373 box_left = metr.bar_width + (
374 metr.width - metr.bar_width - box_img.size[0]
377 # Draw the white box.
378 ImageDraw.Draw(img).rectangle(
382 box_left + box_img.size[0],
383 box_top + box_img.size[1]
387 # Paste the contents into the white box.
388 img.paste(box_img, (box_left, box_top), box_img)
391 def add_cut_lines(self, img):
395 metr = Metric(self, self.scale)
396 draw = ImageDraw.Draw(img)
397 for corner_x, corner_y in (
398 (0, 0), (metr.width, 0),
399 (0, metr.height), (metr.width, metr.height)
401 dir_x = 1 if corner_x == 0 else -1
402 dir_y = 1 if corner_y == 0 else -1
403 for offset in (-1, 0, 1):
407 corner_y + dir_y * metr.bleed + offset,
408 corner_x + dir_x * metr.bleed * line_ratio,
409 corner_y + dir_y * metr.bleed + offset
411 fill='black' if offset == 0 else 'white',
416 corner_x + dir_x * metr.bleed + offset,
418 corner_x + dir_x * metr.bleed + offset,
419 corner_y + dir_y * metr.bleed * line_ratio
421 fill='black' if offset == 0 else 'white',
427 metr = Metric(self, self.scale)
428 img = Image.new('RGB', (metr.width, metr.height),
429 self.background_color)
430 draw = ImageDraw.Draw(img)
432 draw.rectangle((0, 0, metr.bar_width, metr.height),
435 if self.background_img:
436 src = Image.open(self.background_img)
437 trg_size = (metr.width - metr.bar_width, metr.height)
438 if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]:
441 int(round(src.size[1] * trg_size[0] / src.size[0]))
443 cut = (resized[1] - trg_size[1]) // 2
444 src = src.resize(resized, Image.ANTIALIAS)
445 src = src.crop((0, cut, src.size[0], src.size[1] - cut))
448 int(round(src.size[0] * trg_size[1] / src.size[1])),
451 cut = (resized[0] - trg_size[0]) // 2
452 src = src.resize(resized, Image.ANTIALIAS)
453 src = src.crop((cut, 0, src.size[0] - cut, src.size[1]))
455 img.paste(src, (metr.bar_width, 0))
458 img = self.add_box(img)
460 # img = self.add_cut_lines(img)
465 class WLNoBoxCover(WLCover):
466 def add_box(self, img):
470 class LogoWLCover(WLCover):
472 gradient_logo_height = 60
473 gradient_logo_max_width = 1000
474 gradient_logo_margin_right = 30
475 gradient_logo_spacing = 40
476 gradient_color = '#000'
477 gradient_opacity = .6
479 'res/wl-logo-white.png',
480 'res/fnp-logo-white.png',
483 annotation_height = 10
485 def __init__(self, book_info, *args, **kwargs):
486 super(LogoWLCover, self).__init__(book_info, *args, **kwargs)
487 self.gradient_height += self.bleed
488 self.gradient_logo_margin_right += self.bleed
490 self.additional_cover_logos = [
491 BytesIO(URLOpener().open(cover_logo_url).read())
492 for cover_logo_url in book_info.cover_logo_urls
496 img = super(LogoWLCover, self).image()
497 metr = Metric(self, self.scale)
498 gradient = Image.new(
500 (metr.width - metr.bar_width, metr.gradient_height),
503 gradient_mask = Image.new(
505 (metr.width - metr.bar_width, metr.gradient_height)
507 draw = ImageDraw.Draw(gradient_mask)
508 for line in range(0, metr.gradient_height):
510 (0, line, metr.width - metr.bar_width, line),
512 255 * self.gradient_opacity * line / metr.gradient_height
516 (metr.bar_width, metr.height - metr.gradient_height),
519 cursor = metr.width - metr.gradient_logo_margin_right
522 - metr.gradient_height / 2
523 - metr.gradient_logo_height / 2 - metr.bleed / 2
527 get_resource(logo_path)
528 for logo_path in self.gradient_logos[::-1]
530 logos = logos + self.additional_cover_logos
532 Image.open(logo_bytes).convert('RGBA')
533 for logo_bytes in logos
536 # See if logos fit into the gradient. If not, scale down accordingly.
540 - 2 * metr.gradient_logo_margin_right
544 metr.gradient_logo_max_width,
545 logo.size[0] * metr.gradient_logo_height / logo.size[1]
550 + (len(logos) - 1) * (metr.gradient_logo_spacing)
553 space_for_logos / taken_space
554 if taken_space > space_for_logos else 1
556 logo_top += int(metr.gradient_logo_height * (1 - logo_scale) / 2)
558 for i, logo in enumerate(logos):
561 int(round(widths[i] * logo_scale)),
563 logo.size[1] * widths[i] / logo.size[0] * logo_scale
567 cursor -= logo.size[0]
572 int(round(logo_top + (metr.gradient_logo_height - logo.size[1]) * logo_scale / 2))
576 cursor -= int(round(metr.gradient_logo_spacing * logo_scale))
579 img2 = Image.new('RGBA', (metr.height, metr.height), color=None)
580 draw = ImageDraw.Draw(img2)
581 author_font = ImageFont.truetype(
582 self.author_font_ttf, metr.annotation_height,
583 layout_engine=ImageFont.LAYOUT_BASIC)
584 draw.text((self.annotation_height, self.annotation_height), self.annotation, font=author_font, fill='#FFFFFF')
586 img2 = img2.rotate(90)
589 img.alpha_composite(img2, (0, 0))
590 img = img.convert('RGB')
595 class EbookpointCover(LogoWLCover):
596 gradient_logo_height = 58
597 gradient_logo_spacing = 25
599 'res/ebookpoint-logo-white.png',
600 'res/wl-logo-white.png',
601 'res/fnp-logo-white.png',
605 class VirtualoCover(Cover):
614 class PrestigioCover(Cover):
617 background_img = get_resource('res/cover-prestigio.png')
620 author_margin_left = 118
621 author_margin_right = 62
623 author_color = '#fff'
624 author_shadow = '#000'
625 author_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
626 author_font_size = 50
629 title_margin_left = 118
630 title_margin_right = 62
633 title_shadow = '#000'
634 title_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
637 def pretty_title(self):
638 return u"„%s”" % self.title
641 class BookotekaCover(Cover):
644 background_img = get_resource('res/cover-bookoteka.png')
647 author_margin_left = 307
648 author_margin_right = 233
649 author_lineskip = 156
650 author_color = '#d9d919'
651 author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
652 author_font_size = 130
655 title_margin_left = 307
656 title_margin_right = 233
658 title_color = '#d9d919'
659 title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
660 title_font_size = 140
665 class GandalfCover(Cover):
668 background_img = get_resource('res/cover-gandalf.png')
669 author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
670 author_font_size = 30
671 title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
678 class KMLUCover(LogoWLCover):
679 gradient_logo_height = 58
680 gradient_logo_spacing = 25
682 'res/kmlu-logo-white.png',
683 'res/wl-logo-white.png',
684 'res/fnp-logo-white.png',
688 class MPWCover(LogoWLCover):
689 gradient_logo_height = 58
690 gradient_logo_spacing = 25
692 'res/mpw-logo-white.png',
693 'res/wl-logo-white.png',
694 'res/fnp-logo-white.png',
698 class AtriumCover(LogoWLCover):
699 gradient_logo_height = 58
700 gradient_logo_spacing = 25
702 'res/atrium-logo.png',
703 'res/wl-logo-white.png',
704 'res/fnp-logo-white.png',
708 class BNCover(LogoWLCover):
710 'res/dofinansowano.png',
713 'res/wl-logo-white.png',
715 # annotation = 'Zadanie „Udostępnienie publikacji w formatach cyfrowych” w ramach Narodowego Programu Rozwoju Czytelnictwa. Dofinansowano ze środków Ministra Kultury, Dziedzictwa Narodowego i Sportu.'
719 'default': LogoWLCover,
722 'atrium': AtriumCover,
727 def make_cover(book_info, *args, **kwargs):
728 if 'cover_class' in kwargs:
729 cover_class_name = kwargs.pop('cover_class')
731 cover_class_name = book_info.cover_class
732 cover_class = COVER_CLASSES[cover_class_name]
733 return cover_class(book_info, *args, **kwargs)