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.
7 from PIL import Image, ImageFont, ImageDraw, ImageFilter
8 from StringIO import StringIO
9 from librarian import get_resource, OutputFile, URLOpener
13 """Gets metrics from an object, scaling it by a factor."""
14 def __init__(self, obj, scale):
16 self._scale = float(scale)
18 def __getattr__(self, name):
19 src = getattr(self._obj, name)
20 if src and self._scale:
21 return type(src)(self._scale * src)
26 class TextBox(object):
27 """Creates an Image with a series of centered strings."""
33 def __init__(self, max_width, max_height, padding_x=None, padding_y=None):
35 padding_x = self.SHADOW_X + self.SHADOW_BLUR
37 padding_y = self.SHADOW_Y + self.SHADOW_BLUR
39 self.max_width = max_width
40 self.max_text_width = max_width - 2 * padding_x
41 self.padding_y = padding_y
42 self.height = padding_y
43 self.img = Image.new('RGBA', (max_width, max_height))
44 self.draw = ImageDraw.Draw(self.img)
45 self.shadow_img = None
46 self.shadow_draw = None
48 def skip(self, height):
49 """Skips some vertical space."""
52 def text(self, text, color='#000', font=None, line_height=20,
54 """Writes some centered text."""
55 text = re.sub(r'\s+', ' ', text)
57 if not self.shadow_img:
58 self.shadow_img = Image.new('RGBA', self.img.size)
59 self.shadow_draw = ImageDraw.Draw(self.shadow_img)
62 line_width = self.draw.textsize(line, font=font)[0]
63 while line_width > self.max_text_width:
64 parts = line.rsplit(' ', 1)
66 line_width = self.max_text_width
69 line_width = self.draw.textsize(line, font=font)[0]
70 line = line.strip() + ' '
72 pos_x = (self.max_width - line_width) / 2
75 self.shadow_draw.text(
76 (pos_x + self.SHADOW_X, self.height + self.SHADOW_Y),
77 line, font=font, fill=shadow_color
80 self.draw.text((pos_x, self.height), line, font=font, fill=color)
81 self.height += line_height
83 text = text[len(line):]
86 """Creates the actual Image object."""
87 image = Image.new('RGBA', (self.max_width,
88 self.height + self.padding_y))
90 shadow = self.shadow_img.filter(ImageFilter.BLUR)
91 image.paste(shadow, (0, 0), shadow)
92 image.paste(self.img, (0, 0), self.img)
94 image.paste(self.img, (0, 0))
99 """Abstract base class for cover images generator."""
102 background_color = '#fff'
103 background_img = None
106 author_margin_left = 20
107 author_margin_right = 20
109 author_color = '#000'
111 author_font_ttf = get_resource('fonts/DejaVuSerif.ttf')
112 author_font_size = 30
115 title_margin_left = 20
116 title_margin_right = 20
120 title_font_ttf = get_resource('fonts/DejaVuSerif.ttf')
125 uses_dc_cover = False
137 'JPEG': 'image/jpeg',
141 def __init__(self, book_info, format=None, width=None, height=None):
142 self.author = ", ".join(auth.readable() for auth in book_info.authors)
143 self.title = book_info.title
144 if format is not None:
147 self.height = height * self.width / width
148 scale = max(float(width or 0) / self.width, float(height or 0) / self.height)
152 self.scale_after = scale
154 def pretty_author(self):
155 """Allows for decorating author's name."""
158 def pretty_title(self):
159 """Allows for decorating title."""
163 metr = Metric(self, self.scale)
164 img = Image.new('RGB', (metr.width, metr.height), self.background_color)
166 if self.background_img:
167 background = Image.open(self.background_img)
168 img.paste(background, None, background)
173 logo = Image.open(get_resource('res/wl-logo.png'))
174 logo = logo.resize((metr.logo_width, logo.size[1] * metr.logo_width / logo.size[0]))
175 img.paste(logo, ((metr.width - metr.logo_width) / 2, img.size[1] - logo.size[1] - metr.logo_bottom))
177 top = metr.author_top
179 metr.width - metr.author_margin_left - metr.author_margin_right,
183 author_font = ImageFont.truetype(
184 self.author_font_ttf, metr.author_font_size)
185 tbox.text(self.pretty_author(), self.author_color, author_font, metr.author_lineskip, self.author_shadow)
186 text_img = tbox.image()
187 img.paste(text_img, (metr.author_margin_left, top), text_img)
189 top += text_img.size[1] + metr.title_top
191 metr.width - metr.title_margin_left - metr.title_margin_right,
194 title_font = ImageFont.truetype(
195 self.title_font_ttf, metr.title_font_size)
196 tbox.text(self.pretty_title(), self.title_color, title_font, metr.title_lineskip, self.title_shadow)
197 text_img = tbox.image()
198 img.paste(text_img, (metr.title_margin_left, top), text_img)
202 def final_image(self):
204 if self.scale_after != 1:
206 int(round(img.size[0] * self.scale_after)),
207 int(round(img.size[1] * self.scale_after))),
212 return self.mime_types[self.format]
215 return self.exts[self.format]
217 def save(self, *args, **kwargs):
219 'format': self.format,
222 default_kwargs.update(kwargs)
223 return self.final_image().save(*args, **default_kwargs)
225 def output_file(self, *args, **kwargs):
227 self.save(imgstr, *args, **kwargs)
228 return OutputFile.from_string(imgstr.getvalue())
231 class WLCover(Cover):
232 """Wolne Lektury cover without logos."""
236 author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
237 author_font_size = 20
239 title_font_ttf = get_resource('fonts/DejaVuSerif-Bold.ttf')
242 title_box_width = 350
245 box_bottom_margin = 100
258 box_position = 'middle'
259 background_color = '#444'
260 author_color = '#444'
261 background_img = get_resource('res/cover.png')
265 u'Starożytność': '#9e3610',
266 u'Średniowiecze': '#564c09',
267 u'Renesans': '#8ca629',
269 u'Oświecenie': '#f2802e',
270 u'Romantyzm': '#db4b16',
271 u'Pozytywizm': '#961060',
272 u'Modernizm': '#7784e0',
273 u'Dwudziestolecie międzywojenne': '#3044cf',
274 u'Współczesność': '#06393d',
277 kind_box_position = {
282 def __init__(self, book_info, format=None, width=None, height=None, bleed=0):
283 super(WLCover, self).__init__(book_info, format=format, width=width, height=height)
285 self.box_position = book_info.cover_box_position or \
286 self.kind_box_position.get(book_info.kind, self.box_position)
288 if book_info.cover_bar_color == 'none':
291 self.bar_color = book_info.cover_bar_color or \
292 self.epoch_colors.get(book_info.epoch, self.bar_color)
294 self.title_color = self.epoch_colors.get(book_info.epoch, self.title_color)
297 self.box_top_margin += bleed
298 self.box_bottom_margin += bleed
299 self.bar_width += bleed
301 if book_info.cover_url:
302 url = book_info.cover_url
305 bg_src = URLOpener().open(url)
306 self.background_img = StringIO(bg_src.read())
309 def pretty_author(self):
310 return self.author.upper()
312 def add_box(self, img):
313 if self.box_position == 'none':
316 metr = Metric(self, self.scale)
319 box = TextBox(metr.title_box_width, metr.height, padding_y=metr.box_padding_y)
320 author_font = ImageFont.truetype(
321 self.author_font_ttf, metr.author_font_size)
322 box.text(self.pretty_author(),
324 line_height=metr.author_lineskip,
325 color=self.author_color,
326 shadow_color=self.author_shadow)
328 box.skip(metr.box_above_line)
329 box.draw.line((metr.box_line_left, box.height, metr.box_line_right, box.height),
330 fill=self.author_color, width=metr.box_line_width)
331 box.skip(metr.box_below_line)
334 title_font = ImageFont.truetype(
335 self.title_font_ttf, metr.title_font_size)
336 box.text(self.pretty_title(),
337 line_height=metr.title_lineskip,
339 color=self.title_color,
340 shadow_color=self.title_shadow)
342 box_img = box.image()
345 if self.box_position == 'top':
346 box_top = metr.box_top_margin
347 elif self.box_position == 'bottom':
348 box_top = metr.height - metr.box_bottom_margin - box_img.size[1]
350 box_top = (metr.height - box_img.size[1]) / 2
352 box_left = metr.bar_width + (metr.width - metr.bar_width - box_img.size[0]) / 2
354 # Draw the white box.
355 ImageDraw.Draw(img).rectangle(
356 (box_left, box_top, box_left + box_img.size[0], box_top + box_img.size[1]), fill='#fff')
357 # Paste the contents into the white box.
358 img.paste(box_img, (box_left, box_top), box_img)
361 def add_cut_lines(self, img):
365 metr = Metric(self, self.scale)
366 draw = ImageDraw.Draw(img)
367 for corner_x, corner_y in ((0, 0), (metr.width, 0), (0, metr.height), (metr.width, metr.height)):
368 dir_x = 1 if corner_x == 0 else -1
369 dir_y = 1 if corner_y == 0 else -1
370 for offset in (-1, 0, 1):
371 draw.line((corner_x, corner_y + dir_y * metr.bleed + offset,
372 corner_x + dir_x * metr.bleed * line_ratio, corner_y + dir_y * metr.bleed + offset),
373 fill='black' if offset == 0 else 'white', width=1)
374 draw.line((corner_x + dir_x * metr.bleed + offset, corner_y,
375 corner_x + dir_x * metr.bleed + offset, corner_y + dir_y * metr.bleed * line_ratio),
376 fill='black' if offset == 0 else 'white', width=1)
380 metr = Metric(self, self.scale)
381 img = Image.new('RGB', (metr.width, metr.height), self.background_color)
382 draw = ImageDraw.Draw(img)
384 draw.rectangle((0, 0, metr.bar_width, metr.height), fill=self.bar_color)
386 if self.background_img:
387 src = Image.open(self.background_img)
388 trg_size = (metr.width - metr.bar_width, metr.height)
389 if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]:
392 src.size[1] * trg_size[0] / src.size[0]
394 cut = (resized[1] - trg_size[1]) / 2
395 src = src.resize(resized, Image.ANTIALIAS)
396 src = src.crop((0, cut, src.size[0], src.size[1] - cut))
399 src.size[0] * trg_size[1] / src.size[1],
402 cut = (resized[0] - trg_size[0]) / 2
403 src = src.resize(resized, Image.ANTIALIAS)
404 src = src.crop((cut, 0, src.size[0] - cut, src.size[1]))
406 img.paste(src, (metr.bar_width, 0))
409 img = self.add_box(img)
411 # img = self.add_cut_lines(img)
416 class WLNoBoxCover(WLCover):
417 def add_box(self, img):
421 class LogoWLCover(WLCover):
423 gradient_logo_height = 60
424 gradient_logo_margin_right = 30
425 gradient_logo_spacing = 40
426 gradient_color = '#000'
427 gradient_opacity = .6
429 'res/wl-logo-white.png',
430 'res/fnp-logo-white.png',
433 def __init__(self, *args, **kwargs):
434 super(LogoWLCover, self).__init__(*args, **kwargs)
435 self.gradient_height += self.bleed
436 self.gradient_logo_margin_right += self.bleed
439 img = super(LogoWLCover, self).image()
440 metr = Metric(self, self.scale)
441 gradient = Image.new('RGBA', (metr.width - metr.bar_width, metr.gradient_height), self.gradient_color)
442 gradient_mask = Image.new('L', (metr.width - metr.bar_width, metr.gradient_height))
443 draw = ImageDraw.Draw(gradient_mask)
444 for line in range(0, metr.gradient_height):
446 (0, line, metr.width - metr.bar_width, line),
447 fill=int(255 * self.gradient_opacity * line / metr.gradient_height))
448 img.paste(gradient, (metr.bar_width, metr.height - metr.gradient_height), mask=gradient_mask)
450 cursor = metr.width - metr.gradient_logo_margin_right
451 logo_top = metr.height - metr.gradient_height / 2 - metr.gradient_logo_height / 2 - metr.bleed / 2
452 for logo_path in self.gradient_logos[::-1]:
453 logo = Image.open(get_resource(logo_path))
455 (logo.size[0] * metr.gradient_logo_height / logo.size[1], metr.gradient_logo_height),
457 cursor -= logo.size[0]
458 img.paste(logo, (cursor, logo_top), mask=logo)
459 cursor -= metr.gradient_logo_spacing
464 class EbookpointCover(LogoWLCover):
465 gradient_logo_height = 58
466 gradient_logo_spacing = 25
468 'res/ebookpoint-logo-white.png',
469 'res/wl-logo-white.png',
470 'res/fnp-logo-white.png',
474 class VirtualoCover(Cover):
483 class PrestigioCover(Cover):
486 background_img = get_resource('res/cover-prestigio.png')
489 author_margin_left = 118
490 author_margin_right = 62
492 author_color = '#fff'
493 author_shadow = '#000'
494 author_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
495 author_font_size = 50
498 title_margin_left = 118
499 title_margin_right = 62
502 title_shadow = '#000'
503 title_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
506 def pretty_title(self):
507 return u"„%s”" % self.title
510 class BookotekaCover(Cover):
513 background_img = get_resource('res/cover-bookoteka.png')
516 author_margin_left = 307
517 author_margin_right = 233
518 author_lineskip = 156
519 author_color = '#d9d919'
520 author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
521 author_font_size = 130
524 title_margin_left = 307
525 title_margin_right = 233
527 title_color = '#d9d919'
528 title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
529 title_font_size = 140
534 class GandalfCover(Cover):
537 background_img = get_resource('res/cover-gandalf.png')
538 author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
539 author_font_size = 30
540 title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
547 class KMLUCover(LogoWLCover):
548 gradient_logo_height = 58
549 gradient_logo_spacing = 25
551 'res/kmlu-logo-white.png',
552 'res/wl-logo-white.png',
553 'res/fnp-logo-white.png',
557 class MPWCover(LogoWLCover):
558 gradient_logo_height = 58
559 gradient_logo_spacing = 25
561 'res/mpw-logo-white.png',
562 'res/wl-logo-white.png',
563 'res/fnp-logo-white.png',
567 class AtriumCover(LogoWLCover):
568 gradient_logo_height = 58
569 gradient_logo_spacing = 25
571 'res/atrium-logo.png',
572 'res/wl-logo-white.png',
573 'res/fnp-logo-white.png',
578 'default': LogoWLCover,
581 'atrium': AtriumCover,
585 def make_cover(book_info, *args, **kwargs):
586 if 'cover_class' in kwargs:
587 cover_class_name = kwargs.pop('cover_class')
589 cover_class_name = book_info.cover_class
590 cover_class = COVER_CLASSES[cover_class_name]
591 return cover_class(book_info, *args, **kwargs)