more rights flexibility
[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
236     epochs = {
237         u'Starożytność': 0,
238         u'Średniowiecze': 30,
239         u'Renesans': 60,
240         u'Barok': 90,
241         u'Oświecenie': 120,
242         u'Romantyzm': 150,
243         u'Pozytywizm': 180,
244         u'Modernizm': 210,
245         u'Dwudziestolecie międzywojenne': 240,
246         u'Współczesność': 270,
247     }
248
249     def __init__(self, book_info):
250         super(WLCover, self).__init__(book_info)
251         self.kind = book_info.kind
252         self.epoch = book_info.epoch
253         if book_info.cover_url:
254             from urllib2 import urlopen
255             from StringIO import StringIO
256
257             bg_src = urlopen(book_info.cover_url)
258             self.background_img = StringIO(bg_src.read())
259             bg_src.close()
260
261     def pretty_author(self):
262         return self.author.upper()
263
264     def image(self):
265         from colorsys import hsv_to_rgb
266
267         img = Image.new('RGB', (self.width, self.height), self.background_color)
268         draw = ImageDraw.Draw(img)
269
270         if self.epoch in self.epochs:
271             epoch_color = tuple(int(255 * c) for c in hsv_to_rgb(
272                     float(self.epochs[self.epoch]) / 360, .7, .7))
273         else:
274             epoch_color = '#000'
275         draw.rectangle((0, 0, self.bar_width, self.height), fill=epoch_color)
276
277         if self.background_img:
278             src = Image.open(self.background_img)
279             trg_size = (self.width - self.bar_width, self.height)
280             if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]:
281                 resized = (
282                     trg_size[0],
283                     src.size[1] * trg_size[0] / src.size[0]
284                 )
285                 cut = (resized[1] - trg_size[1]) / 2
286                 src = src.resize(resized)
287                 src = src.crop((0, cut, src.size[0], src.size[1] - cut))
288             else:
289                 resized = (
290                     src.size[0] * trg_size[1] / src.size[1],
291                     trg_size[1],
292                 )
293                 cut = (resized[0] - trg_size[0]) / 2
294                 src = src.resize(resized)
295                 src = src.crop((cut, 0, src.size[0] - cut, src.size[1]))
296             
297             img.paste(src, (self.bar_width, 0))
298             del src
299
300         box = TextBox(self.title_box_width, self.height, padding_y=20)
301         box.text(self.pretty_author(), 
302                  font=self.author_font,
303                  line_height=self.author_lineskip,
304                  color=self.author_color,
305                  shadow_color=self.author_shadow,
306                 )
307
308         box.skip(10)
309         box.draw.line((75, box.height, 275, box.height), 
310                 fill=self.author_color, width=2)
311         box.skip(15)
312
313         box.text(self.pretty_title(),
314                  line_height=self.title_lineskip,
315                  font=self.title_font,
316                  color=epoch_color,
317                  shadow_color=self.title_shadow,
318                 )
319         box_img = box.image()
320
321         if self.kind == 'Liryka':
322             # top
323             box_top = 100
324         elif self.kind == 'Epika':
325             # bottom
326             box_top = self.height - 100 - box_img.size[1]
327         else:
328             # center
329             box_top = (self.height - box_img.size[1]) / 2
330
331         box_left = self.bar_width + (self.width - self.bar_width - 
332                         box_img.size[0]) / 2
333         draw.rectangle((box_left, box_top, 
334             box_left + box_img.size[0], box_top + box_img.size[1]),
335             fill='#fff')
336         img.paste(box_img, (box_left, box_top), box_img)
337
338         return img
339
340
341
342 class VirtualoCover(Cover):
343     width = 600
344     height = 730
345     author_top = 73
346     title_top = 73
347     logo_bottom = 25
348     logo_width = 250
349
350
351 class PrestigioCover(Cover):
352     width = 580
353     height = 783
354     background_img = get_resource('res/cover-prestigio.png')
355
356     author_top = 446
357     author_margin_left = 118
358     author_margin_right = 62
359     author_lineskip = 60
360     author_color = '#fff'
361     author_shadow = '#000'
362     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50)
363
364     title_top = 0
365     title_margin_left = 118
366     title_margin_right = 62
367     title_lineskip = 60
368     title_color = '#fff'
369     title_shadow = '#000'
370     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50)
371
372     def pretty_title(self):
373         return u"„%s”" % self.title
374
375
376 class BookotekaCover(Cover):
377     width = 2140
378     height = 2733
379     background_img = get_resource('res/cover-bookoteka.png')
380
381     author_top = 480
382     author_margin_left = 307
383     author_margin_right = 233
384     author_lineskip = 156
385     author_color = '#d9d919'
386     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 130)
387
388     title_top = 400
389     title_margin_left = 307
390     title_margin_right = 233
391     title_lineskip = 168
392     title_color = '#d9d919'
393     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 140)
394
395     format = 'PNG'
396
397
398 class GandalfCover(Cover):
399     width = 600
400     height = 730
401     background_img = get_resource('res/cover-gandalf.png')
402     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 30)
403     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 40)
404     logo_bottom = 25
405     logo_width = 250
406     format = 'PNG'
407
408
409 class ArtaTechCover(Cover):
410     width = 600
411     height = 800
412     background_img = get_resource('res/cover-arta-tech.jpg')
413     author_top = 132
414     author_margin_left = 235
415     author_margin_right = 23
416     author_align = 'r'
417     author_font = ImageFont.truetype(get_resource('fonts/DroidSans.ttf'), 32)
418     author_color = '#555555'
419     author_wrap = False
420     title_top = 17
421     title_margin_right = 21
422     title_margin_left = 60
423     title_align = 'r'
424     title_font = ImageFont.truetype(get_resource('fonts/EBGaramond-Regular.ttf'), 42)
425     title_color = '#222222'
426     title_wrap = False
427     format = 'JPEG'
428
429     def pretty_author(self):
430         return self.author.upper()
431
432
433 def ImageCover(img):
434     """ a class factory for simple image covers """
435     img = Image.open(img)
436
437     class ImgCover(Cover):
438         def image(self):
439             return img
440
441         @property
442         def format(self):
443             return self.image().format
444
445     return ImgCover