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 LogoWLCover(WLCover):
 
 418     gradient_logo_height = 60
 
 419     gradient_logo_margin_right = 30
 
 420     gradient_logo_spacing = 40
 
 421     gradient_color = '#000'
 
 422     gradient_opacity = .6
 
 424         'res/wl-logo-white.png',
 
 425         'res/fnp-logo-white.png',
 
 428     def __init__(self, *args, **kwargs):
 
 429         super(LogoWLCover, self).__init__(*args, **kwargs)
 
 430         self.gradient_height += self.bleed
 
 431         self.gradient_logo_margin_right += self.bleed
 
 434         img = super(LogoWLCover, self).image()
 
 435         metr = Metric(self, self.scale)
 
 436         gradient = Image.new('RGBA', (metr.width - metr.bar_width, metr.gradient_height), self.gradient_color)
 
 437         gradient_mask = Image.new('L', (metr.width - metr.bar_width, metr.gradient_height))
 
 438         draw = ImageDraw.Draw(gradient_mask)
 
 439         for line in range(0, metr.gradient_height):
 
 441                 (0, line, metr.width - metr.bar_width, line),
 
 442                 fill=int(255 * self.gradient_opacity * line / metr.gradient_height))
 
 443         img.paste(gradient, (metr.bar_width, metr.height - metr.gradient_height), mask=gradient_mask)
 
 445         cursor = metr.width - metr.gradient_logo_margin_right
 
 446         logo_top = metr.height - metr.gradient_height / 2 - metr.gradient_logo_height / 2 - metr.bleed / 2
 
 447         for logo_path in self.gradient_logos[::-1]:
 
 448             logo = Image.open(get_resource(logo_path))
 
 450                 (logo.size[0] * metr.gradient_logo_height / logo.size[1], metr.gradient_logo_height),
 
 452             cursor -= logo.size[0]
 
 453             img.paste(logo, (cursor, logo_top), mask=logo)
 
 454             cursor -= metr.gradient_logo_spacing
 
 459 class EbookpointCover(LogoWLCover):
 
 460     gradient_logo_height = 58
 
 461     gradient_logo_spacing = 25
 
 463         'res/ebookpoint-logo-white.png',
 
 464         'res/wl-logo-white.png',
 
 465         'res/fnp-logo-white.png',
 
 469 class VirtualoCover(Cover):
 
 478 class PrestigioCover(Cover):
 
 481     background_img = get_resource('res/cover-prestigio.png')
 
 484     author_margin_left = 118
 
 485     author_margin_right = 62
 
 487     author_color = '#fff'
 
 488     author_shadow = '#000'
 
 489     author_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
 
 490     author_font_size = 50
 
 493     title_margin_left = 118
 
 494     title_margin_right = 62
 
 497     title_shadow = '#000'
 
 498     title_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
 
 501     def pretty_title(self):
 
 502         return u"„%s”" % self.title
 
 505 class BookotekaCover(Cover):
 
 508     background_img = get_resource('res/cover-bookoteka.png')
 
 511     author_margin_left = 307
 
 512     author_margin_right = 233
 
 513     author_lineskip = 156
 
 514     author_color = '#d9d919'
 
 515     author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
 
 516     author_font_size = 130
 
 519     title_margin_left = 307
 
 520     title_margin_right = 233
 
 522     title_color = '#d9d919'
 
 523     title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
 
 524     title_font_size = 140
 
 529 class GandalfCover(Cover):
 
 532     background_img = get_resource('res/cover-gandalf.png')
 
 533     author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
 
 534     author_font_size = 30
 
 535     title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
 
 542 DefaultEbookCover = LogoWLCover