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 for pa in self.pretty_authors():
196 tbox.text(pa, self.author_color, author_font, metr.author_lineskip,
198 text_img = tbox.image()
199 img.paste(text_img, (metr.author_margin_left, top), text_img)
201 top += text_img.size[1] + metr.title_top
203 metr.width - metr.title_margin_left - metr.title_margin_right,
206 title_font = ImageFont.truetype(
207 self.title_font_ttf, metr.title_font_size)
208 tbox.text(self.pretty_title(), self.title_color, title_font,
209 metr.title_lineskip, self.title_shadow)
210 text_img = tbox.image()
211 img.paste(text_img, (metr.title_margin_left, top), text_img)
215 def final_image(self):
217 if self.scale_after != 1:
219 int(round(img.size[0] * self.scale_after)),
220 int(round(img.size[1] * self.scale_after))),
225 return self.mime_types[self.format]
228 return self.exts[self.format]
230 def save(self, *args, **kwargs):
232 'format': self.format,
235 default_kwargs.update(kwargs)
236 return self.final_image().save(*args, **default_kwargs)
238 def output_file(self, *args, **kwargs):
240 self.save(imgstr, *args, **kwargs)
241 return OutputFile.from_bytes(imgstr.getvalue())
244 class WLCover(Cover):
245 """Wolne Lektury cover without logos."""
249 author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
250 author_font_size = 20
252 title_font_ttf = get_resource('fonts/DejaVuSerif-Bold.ttf')
255 title_box_width = 350
258 box_bottom_margin = 100
271 box_position = 'middle'
272 background_color = '#444'
273 author_color = '#444'
274 background_img = get_resource('res/cover.png')
278 u'Starożytność': '#9e3610',
279 u'Średniowiecze': '#564c09',
280 u'Renesans': '#8ca629',
282 u'Oświecenie': '#f2802e',
283 u'Romantyzm': '#db4b16',
284 u'Pozytywizm': '#961060',
285 u'Modernizm': '#7784e0',
286 u'Dwudziestolecie międzywojenne': '#3044cf',
287 u'Współczesność': '#06393d',
290 kind_box_position = {
295 def __init__(self, book_info, format=None, width=None, height=None,
297 super(WLCover, self).__init__(book_info, format=format, width=width,
300 self.box_position = book_info.cover_box_position or \
301 self.kind_box_position.get(book_info.kind, self.box_position)
303 if book_info.cover_bar_color == 'none':
306 self.bar_color = book_info.cover_bar_color or \
307 self.epoch_colors.get(book_info.epoch, self.bar_color)
309 self.title_color = self.epoch_colors.get(book_info.epoch,
313 self.box_top_margin += bleed
314 self.box_bottom_margin += bleed
315 self.bar_width += bleed
317 if book_info.cover_url:
318 url = book_info.cover_url
321 bg_src = URLOpener().open(url)
322 self.background_img = BytesIO(bg_src.read())
325 def pretty_authors(self):
326 return [a.upper() for a in self.authors]
328 def add_box(self, img):
329 if self.box_position == 'none':
332 metr = Metric(self, self.scale)
335 box = TextBox(metr.title_box_width, metr.height,
336 padding_y=metr.box_padding_y)
337 author_font = ImageFont.truetype(
338 self.author_font_ttf, metr.author_font_size)
339 for pa in self.pretty_authors():
340 box.text(pa, font=author_font, line_height=metr.author_lineskip,
341 color=self.author_color, shadow_color=self.author_shadow)
343 box.skip(metr.box_above_line)
345 (metr.box_line_left, box.height, metr.box_line_right, box.height),
346 fill=self.author_color, width=metr.box_line_width
348 box.skip(metr.box_below_line)
351 title_font = ImageFont.truetype(
352 self.title_font_ttf, metr.title_font_size)
353 box.text(self.pretty_title(),
354 line_height=metr.title_lineskip,
356 color=self.title_color,
357 shadow_color=self.title_shadow)
359 box_img = box.image()
362 if self.box_position == 'top':
363 box_top = metr.box_top_margin
364 elif self.box_position == 'bottom':
365 box_top = metr.height - metr.box_bottom_margin - box_img.size[1]
367 box_top = (metr.height - box_img.size[1]) // 2
369 box_left = metr.bar_width + (
370 metr.width - metr.bar_width - box_img.size[0]
373 # Draw the white box.
374 ImageDraw.Draw(img).rectangle(
378 box_left + box_img.size[0],
379 box_top + box_img.size[1]
383 # Paste the contents into the white box.
384 img.paste(box_img, (box_left, box_top), box_img)
387 def add_cut_lines(self, img):
391 metr = Metric(self, self.scale)
392 draw = ImageDraw.Draw(img)
393 for corner_x, corner_y in (
394 (0, 0), (metr.width, 0),
395 (0, metr.height), (metr.width, metr.height)
397 dir_x = 1 if corner_x == 0 else -1
398 dir_y = 1 if corner_y == 0 else -1
399 for offset in (-1, 0, 1):
403 corner_y + dir_y * metr.bleed + offset,
404 corner_x + dir_x * metr.bleed * line_ratio,
405 corner_y + dir_y * metr.bleed + offset
407 fill='black' if offset == 0 else 'white',
412 corner_x + dir_x * metr.bleed + offset,
414 corner_x + dir_x * metr.bleed + offset,
415 corner_y + dir_y * metr.bleed * line_ratio
417 fill='black' if offset == 0 else 'white',
423 metr = Metric(self, self.scale)
424 img = Image.new('RGB', (metr.width, metr.height),
425 self.background_color)
426 draw = ImageDraw.Draw(img)
428 draw.rectangle((0, 0, metr.bar_width, metr.height),
431 if self.background_img:
432 src = Image.open(self.background_img)
433 trg_size = (metr.width - metr.bar_width, metr.height)
434 if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]:
437 int(round(src.size[1] * trg_size[0] / src.size[0]))
439 cut = (resized[1] - trg_size[1]) // 2
440 src = src.resize(resized, Image.ANTIALIAS)
441 src = src.crop((0, cut, src.size[0], src.size[1] - cut))
444 int(round(src.size[0] * trg_size[1] / src.size[1])),
447 cut = (resized[0] - trg_size[0]) // 2
448 src = src.resize(resized, Image.ANTIALIAS)
449 src = src.crop((cut, 0, src.size[0] - cut, src.size[1]))
451 img.paste(src, (metr.bar_width, 0))
454 img = self.add_box(img)
456 # img = self.add_cut_lines(img)
461 class WLNoBoxCover(WLCover):
462 def add_box(self, img):
466 class LogoWLCover(WLCover):
468 gradient_logo_height = 60
469 gradient_logo_margin_right = 30
470 gradient_logo_spacing = 40
471 gradient_color = '#000'
472 gradient_opacity = .6
474 'res/wl-logo-white.png',
475 'res/fnp-logo-white.png',
478 def __init__(self, book_info, *args, **kwargs):
479 super(LogoWLCover, self).__init__(book_info, *args, **kwargs)
480 self.gradient_height += self.bleed
481 self.gradient_logo_margin_right += self.bleed
483 self.additional_cover_logos = [
484 BytesIO(URLOpener().open(cover_logo_url).read())
485 for cover_logo_url in book_info.cover_logo_urls
489 img = super(LogoWLCover, self).image()
490 metr = Metric(self, self.scale)
491 gradient = Image.new(
493 (metr.width - metr.bar_width, metr.gradient_height),
496 gradient_mask = Image.new(
498 (metr.width - metr.bar_width, metr.gradient_height)
500 draw = ImageDraw.Draw(gradient_mask)
501 for line in range(0, metr.gradient_height):
503 (0, line, metr.width - metr.bar_width, line),
505 255 * self.gradient_opacity * line / metr.gradient_height
509 (metr.bar_width, metr.height - metr.gradient_height),
512 cursor = metr.width - metr.gradient_logo_margin_right
515 - metr.gradient_height / 2
516 - metr.gradient_logo_height / 2 - metr.bleed / 2
520 get_resource(logo_path)
521 for logo_path in self.gradient_logos[::-1]
523 logos = logos + self.additional_cover_logos
525 Image.open(logo_bytes).convert('RGBA')
526 for logo_bytes in logos
529 # See if logos fit into the gradient. If not, scale down accordingly.
533 - 2 * metr.gradient_logo_margin_right
536 logo.size[0] * metr.gradient_logo_height / logo.size[1]
540 + (len(logos) - 1) * (metr.gradient_logo_spacing)
543 space_for_logos / taken_space
544 if taken_space > space_for_logos else 1
546 logo_top += int(metr.gradient_logo_height * (1 - logo_scale) / 2)
548 for i, logo in enumerate(logos):
551 int(round(widths[i] * logo_scale)),
552 int(round(metr.gradient_logo_height * logo_scale))
555 cursor -= logo.size[0]
556 img.paste(logo, (cursor, logo_top), mask=logo)
557 cursor -= int(round(metr.gradient_logo_spacing * logo_scale))
562 class EbookpointCover(LogoWLCover):
563 gradient_logo_height = 58
564 gradient_logo_spacing = 25
566 'res/ebookpoint-logo-white.png',
567 'res/wl-logo-white.png',
568 'res/fnp-logo-white.png',
572 class VirtualoCover(Cover):
581 class PrestigioCover(Cover):
584 background_img = get_resource('res/cover-prestigio.png')
587 author_margin_left = 118
588 author_margin_right = 62
590 author_color = '#fff'
591 author_shadow = '#000'
592 author_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
593 author_font_size = 50
596 title_margin_left = 118
597 title_margin_right = 62
600 title_shadow = '#000'
601 title_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
604 def pretty_title(self):
605 return u"„%s”" % self.title
608 class BookotekaCover(Cover):
611 background_img = get_resource('res/cover-bookoteka.png')
614 author_margin_left = 307
615 author_margin_right = 233
616 author_lineskip = 156
617 author_color = '#d9d919'
618 author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
619 author_font_size = 130
622 title_margin_left = 307
623 title_margin_right = 233
625 title_color = '#d9d919'
626 title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
627 title_font_size = 140
632 class GandalfCover(Cover):
635 background_img = get_resource('res/cover-gandalf.png')
636 author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
637 author_font_size = 30
638 title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
645 class KMLUCover(LogoWLCover):
646 gradient_logo_height = 58
647 gradient_logo_spacing = 25
649 'res/kmlu-logo-white.png',
650 'res/wl-logo-white.png',
651 'res/fnp-logo-white.png',
655 class MPWCover(LogoWLCover):
656 gradient_logo_height = 58
657 gradient_logo_spacing = 25
659 'res/mpw-logo-white.png',
660 'res/wl-logo-white.png',
661 'res/fnp-logo-white.png',
665 class AtriumCover(LogoWLCover):
666 gradient_logo_height = 58
667 gradient_logo_spacing = 25
669 'res/atrium-logo.png',
670 'res/wl-logo-white.png',
671 'res/fnp-logo-white.png',
676 'default': LogoWLCover,
679 'atrium': AtriumCover,
683 def make_cover(book_info, *args, **kwargs):
684 if 'cover_class' in kwargs:
685 cover_class_name = kwargs.pop('cover_class')
687 cover_class_name = book_info.cover_class
688 cover_class = COVER_CLASSES[cover_class_name]
689 return cover_class(book_info, *args, **kwargs)