Merge branch 'master' into ofop
[librarian.git] / librarian / cover.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 #
6 import Image, ImageFont, ImageDraw, ImageFilter
7 from librarian import get_resource
8
9
10 class TextBox(object):
11     """Creates an Image with a series of centered strings."""
12
13     SHADOW_X = 3
14     SHADOW_Y = 3
15     SHADOW_BLUR = 3
16
17     def __init__(self, max_width, max_height, padding_x=None, padding_y=None):
18         if padding_x is None:
19             padding_x = self.SHADOW_X + self.SHADOW_BLUR
20         if padding_y is None:
21             padding_y = self.SHADOW_Y + self.SHADOW_BLUR
22
23         self.max_width = max_width
24         self.max_text_width = max_width - 2 * padding_x
25         self.padding_y = padding_y
26         self.height = padding_y
27         self.img = Image.new('RGBA', (max_width, max_height))
28         self.draw = ImageDraw.Draw(self.img)
29         self.shadow_img = None
30         self.shadow_draw = None
31
32     def skip(self, height):
33         """Skips some vertical space."""
34         self.height += height
35
36     def text(self, text, color='#000', font=None, line_height=20, 
37              shadow_color=None, shortener=None):
38         """Writes some centered text."""
39         if shadow_color:
40             if not self.shadow_img:
41                 self.shadow_img = Image.new('RGBA', self.img.size)
42                 self.shadow_draw = ImageDraw.Draw(self.shadow_img)
43         while text:
44             if shortener:
45                 for line in shortener(text):
46                     if text_draw.textsize(line, font=font)[0] <= self.max_text_width:
47                         break
48                 text = ''
49             else:
50                 line = text
51                 line_width = self.draw.textsize(line, font=font)[0]
52                 while line_width > self.max_text_width:
53                     parts = line.rsplit(' ', 1)
54                     if len(parts) == 1:
55                         line_width = self.max_text_width
56                         break
57                     line = parts[0]
58                     line_width = self.draw.textsize(line, font=font)[0]
59
60             line = line.strip() + ' '
61
62             pos_x = (self.max_width - line_width) / 2
63
64             if shadow_color:
65                 self.shadow_draw.text(
66                         (pos_x + self.SHADOW_X, self.height + self.SHADOW_Y),
67                         line, font=font, fill=shadow_color
68                 )
69
70             self.draw.text((pos_x, self.height), line, font=font, fill=color)
71             self.height += line_height
72             # go to next line
73             text = text[len(line):]
74
75     @staticmethod
76     def person_shortener(text):
77         yield text
78         chunks = text.split()
79         n_chunks = len(chunks)
80         # make initials from given names, starting from last
81         for i in range(n_chunks - 2, -1, -1):
82             chunks[i] = chunks[i][0] + '.'
83             yield " ".join(chunks)
84         # remove given names initials, starting from last
85         while len(chunks) > 2:
86             del chunks[1]
87             yield " ".join(chunks)
88
89     @staticmethod
90     def title_shortener(text):
91         yield text
92         chunks = text.split()
93         n_chunks = len(chunks)
94         # remove words, starting from last one
95         while len(chunks) > 1:
96             del chunks[-1]
97             yield " ".join(chunks) + u'…'
98
99     def image(self):
100         """Creates the actual Image object."""
101         image = Image.new('RGBA', (self.max_width,
102                                    self.height + self.padding_y))
103         if self.shadow_img:
104             shadow = self.shadow_img.filter(ImageFilter.BLUR)
105             image.paste(shadow, (0, 0), shadow)
106             image.paste(self.img, (0, 0), self.img)
107         else:
108             image.paste(self.img, (0, 0))
109         return image
110
111
112 class Cover(object):
113     """Abstract base class for cover images generator."""
114     width = 600
115     height = 800
116     background_color = '#fff'
117     background_img = None
118
119     author_align = 'c'
120     author_top = 100
121     author_margin_left = 20
122     author_margin_right = 20
123     author_lineskip = 40
124     author_color = '#000'
125     author_shadow = None
126     author_font = None
127     author_wrap = True
128
129     title_align = 'c'
130     title_top = 100
131     title_margin_left = 20
132     title_margin_right = 20
133     title_lineskip = 54
134     title_color = '#000'
135     title_shadow = None
136     title_font = None
137     title_wrap = True
138
139     logo_bottom = None
140     logo_width = None
141     uses_dc_cover = False
142
143     format = 'JPEG'
144
145     exts = {
146         'JPEG': 'jpg',
147         'PNG': 'png',
148         }
149
150     mime_types = {
151         'JPEG': 'image/jpeg',
152         'PNG': 'image/png',
153         }
154
155     def __init__(self, book_info):
156         self.author = ", ".join(auth.readable() for auth in book_info.authors)
157         self.title = book_info.title
158
159     def pretty_author(self):
160         """Allows for decorating author's name."""
161         return self.author
162
163     def pretty_title(self):
164         """Allows for decorating title."""
165         return self.title
166
167     def image(self):
168         img = Image.new('RGB', (self.width, self.height), self.background_color)
169
170         if self.background_img:
171             background = Image.open(self.background_img)
172             try:
173                 img.paste(background, None, background)
174             except ValueError, e:
175                 img.paste(background)
176             del background
177
178         # WL logo
179         if self.logo_width:
180             logo = Image.open(get_resource('res/wl-logo.png'))
181             logo = logo.resize((self.logo_width, logo.size[1] * self.logo_width / logo.size[0]))
182             img.paste(logo, ((self.width - self.logo_width) / 2, img.size[1] - logo.size[1] - self.logo_bottom))
183
184         top = self.author_top
185         tbox = TextBox(
186             self.width - self.author_margin_left - self.author_margin_right,
187             self.height - top,
188             )
189         author_font = self.author_font or ImageFont.truetype(
190             get_resource('fonts/DejaVuSerif.ttf'), 30)
191         author_shortener = None if self.author_wrap else TextBox.person_shortener 
192         tbox.text(self.pretty_author(), self.author_color, author_font,
193             self.author_lineskip, self.author_shadow, author_shortener)
194         text_img = tbox.image()
195         img.paste(text_img, (self.author_margin_left, top), text_img)
196         
197         top += text_img.size[1] + self.title_top
198         tbox = TextBox(
199             self.width - self.title_margin_left - self.title_margin_right,
200             self.height - top,
201             )
202         title_font = self.author_font or ImageFont.truetype(
203             get_resource('fonts/DejaVuSerif.ttf'), 40)
204         title_shortener = None if self.title_wrap else TextBox.title_shortener 
205         tbox.text(self.pretty_title(), self.title_color, title_font,
206             self.title_lineskip, self.title_shadow, title_shortener)
207         text_img = tbox.image()
208         img.paste(text_img, (self.title_margin_left, top), text_img)
209
210         return img
211
212     def mime_type(self):
213         return self.mime_types[self.format]
214
215     def ext(self):
216         return self.exts[self.format]
217
218     def save(self, *args, **kwargs):
219         return self.image().save(format=self.format, *args, **kwargs)
220
221
222 class WLCover(Cover):
223     """Default Wolne Lektury cover generator."""
224     uses_dc_cover = True
225     author_font = ImageFont.truetype(
226         get_resource('fonts/JunicodeWL-Regular.ttf'), 20)
227     author_lineskip = 30
228     title_font = ImageFont.truetype(
229         get_resource('fonts/DejaVuSerif-Bold.ttf'), 30)
230     title_lineskip = 40
231     title_box_width = 350
232     bar_width = 35
233     background_color = '#444'
234     author_color = '#444'
235     default_background = get_resource('res/cover.png')
236     format = 'JPEG'
237
238     epochs = {
239         u'Starożytność': 0,
240         u'Średniowiecze': 30,
241         u'Renesans': 60,
242         u'Barok': 90,
243         u'Oświecenie': 120,
244         u'Romantyzm': 150,
245         u'Pozytywizm': 180,
246         u'Modernizm': 210,
247         u'Dwudziestolecie międzywojenne': 240,
248         u'Współczesność': 270,
249     }
250
251     def __init__(self, book_info):
252         super(WLCover, self).__init__(book_info)
253         self.kind = book_info.kind
254         self.epoch = book_info.epoch
255         if book_info.cover_url:
256             from urllib2 import urlopen
257             from StringIO import StringIO
258
259             bg_src = urlopen(book_info.cover_url)
260             self.background_img = StringIO(bg_src.read())
261             bg_src.close()
262         else:
263             self.background_img = self.default_background
264
265     def pretty_author(self):
266         return self.author.upper()
267
268     def image(self):
269         from colorsys import hsv_to_rgb
270
271         img = Image.new('RGB', (self.width, self.height), self.background_color)
272         draw = ImageDraw.Draw(img)
273
274         if self.epoch in self.epochs:
275             epoch_color = tuple(int(255 * c) for c in hsv_to_rgb(
276                     float(self.epochs[self.epoch]) / 360, .7, .7))
277         else:
278             epoch_color = '#000'
279         draw.rectangle((0, 0, self.bar_width, self.height), fill=epoch_color)
280
281         if self.background_img:
282             src = Image.open(self.background_img)
283             trg_size = (self.width - self.bar_width, self.height)
284             if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]:
285                 resized = (
286                     trg_size[0],
287                     src.size[1] * trg_size[0] / src.size[0]
288                 )
289                 cut = (resized[1] - trg_size[1]) / 2
290                 src = src.resize(resized)
291                 src = src.crop((0, cut, src.size[0], src.size[1] - cut))
292             else:
293                 resized = (
294                     src.size[0] * trg_size[1] / src.size[1],
295                     trg_size[1],
296                 )
297                 cut = (resized[0] - trg_size[0]) / 2
298                 src = src.resize(resized)
299                 src = src.crop((cut, 0, src.size[0] - cut, src.size[1]))
300             
301             img.paste(src, (self.bar_width, 0))
302             del src
303
304         box = TextBox(self.title_box_width, self.height, padding_y=20)
305         box.text(self.pretty_author(), 
306                  font=self.author_font,
307                  line_height=self.author_lineskip,
308                  color=self.author_color,
309                  shadow_color=self.author_shadow,
310                 )
311
312         box.skip(10)
313         box.draw.line((75, box.height, 275, box.height), 
314                 fill=self.author_color, width=2)
315         box.skip(15)
316
317         box.text(self.pretty_title(),
318                  line_height=self.title_lineskip,
319                  font=self.title_font,
320                  color=epoch_color,
321                  shadow_color=self.title_shadow,
322                 )
323         box_img = box.image()
324
325         if self.kind == 'Liryka':
326             # top
327             box_top = 100
328         elif self.kind == 'Epika':
329             # bottom
330             box_top = self.height - 100 - box_img.size[1]
331         else:
332             # center
333             box_top = (self.height - box_img.size[1]) / 2
334
335         box_left = self.bar_width + (self.width - self.bar_width - 
336                         box_img.size[0]) / 2
337         draw.rectangle((box_left, box_top, 
338             box_left + box_img.size[0], box_top + box_img.size[1]),
339             fill='#fff')
340         img.paste(box_img, (box_left, box_top), box_img)
341
342         return img
343
344
345
346 class VirtualoCover(Cover):
347     width = 600
348     height = 730
349     author_top = 73
350     title_top = 73
351     logo_bottom = 25
352     logo_width = 250
353
354
355 class PrestigioCover(Cover):
356     width = 580
357     height = 783
358     background_img = get_resource('res/cover-prestigio.png')
359
360     author_top = 446
361     author_margin_left = 118
362     author_margin_right = 62
363     author_lineskip = 60
364     author_color = '#fff'
365     author_shadow = '#000'
366     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50)
367
368     title_top = 0
369     title_margin_left = 118
370     title_margin_right = 62
371     title_lineskip = 60
372     title_color = '#fff'
373     title_shadow = '#000'
374     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50)
375
376     def pretty_title(self):
377         return u"„%s”" % self.title
378
379
380 class BookotekaCover(Cover):
381     width = 2140
382     height = 2733
383     background_img = get_resource('res/cover-bookoteka.png')
384
385     author_top = 480
386     author_margin_left = 307
387     author_margin_right = 233
388     author_lineskip = 156
389     author_color = '#d9d919'
390     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 130)
391
392     title_top = 400
393     title_margin_left = 307
394     title_margin_right = 233
395     title_lineskip = 168
396     title_color = '#d9d919'
397     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 140)
398
399     format = 'PNG'
400
401
402 class GandalfCover(Cover):
403     width = 600
404     height = 730
405     background_img = get_resource('res/cover-gandalf.png')
406     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 30)
407     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 40)
408     logo_bottom = 25
409     logo_width = 250
410     format = 'PNG'
411
412
413 class ArtaTechCover(Cover):
414     width = 600
415     height = 800
416     background_img = get_resource('res/cover-arta-tech.jpg')
417     author_top = 132
418     author_margin_left = 235
419     author_margin_right = 23
420     author_align = 'r'
421     author_font = ImageFont.truetype(get_resource('fonts/DroidSans.ttf'), 32)
422     author_color = '#555555'
423     author_wrap = False
424     title_top = 17
425     title_margin_right = 21
426     title_margin_left = 60
427     title_align = 'r'
428     title_font = ImageFont.truetype(get_resource('fonts/EBGaramond-Regular.ttf'), 42)
429     title_color = '#222222'
430     title_wrap = False
431     format = 'JPEG'
432
433     def pretty_author(self):
434         return self.author.upper()
435
436
437 def ImageCover(img):
438     """ a class factory for simple image covers """
439     img = Image.open(img)
440
441     class ImgCover(Cover):
442         def image(self):
443             return img
444
445         @property
446         def format(self):
447             return self.image().format
448
449     return ImgCover