lightier cover
[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):
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             line = text
45             line_width = self.draw.textsize(line, font=font)[0]
46             while line_width > self.max_text_width:
47                 parts = line.rsplit(' ', 1)
48                 if len(parts) == 1:
49                     line_width = self.max_text_width
50                     break
51                 line = parts[0]
52                 line_width = self.draw.textsize(line, font=font)[0]
53             line = line.strip() + ' '
54
55             pos_x = (self.max_width - line_width) / 2
56
57             if shadow_color:
58                 self.shadow_draw.text(
59                         (pos_x + self.SHADOW_X, self.height + self.SHADOW_Y),
60                         line, font=font, fill=shadow_color
61                 )
62
63             self.draw.text((pos_x, self.height), line, font=font, fill=color)
64             self.height += line_height
65             # go to next line
66             text = text[len(line):]
67
68     def image(self):
69         """Creates the actual Image object."""
70         image = Image.new('RGBA', (self.max_width,
71                                    self.height + self.padding_y))
72         if self.shadow_img:
73             shadow = self.shadow_img.filter(ImageFilter.BLUR)
74             image.paste(shadow, (0, 0), shadow)
75             image.paste(self.img, (0, 0), self.img)
76         else:
77             image.paste(self.img, (0, 0))
78         return image
79
80
81 class Cover(object):
82     """Abstract base class for cover images generator."""
83     width = 600
84     height = 800
85     background_color = '#fff'
86     background_img = None
87
88     author_top = 100
89     author_margin_left = 20
90     author_margin_right = 20
91     author_lineskip = 40
92     author_color = '#000'
93     author_shadow = None
94     author_font = None
95
96     title_top = 100
97     title_margin_left = 20
98     title_margin_right = 20
99     title_lineskip = 54
100     title_color = '#000'
101     title_shadow = None
102     title_font = None
103
104     logo_bottom = None
105     logo_width = None
106     uses_dc_cover = False
107
108     format = 'JPEG'
109
110     exts = {
111         'JPEG': 'jpg',
112         'PNG': 'png',
113         }
114
115     mime_types = {
116         'JPEG': 'image/jpeg',
117         'PNG': 'image/png',
118         }
119
120     def __init__(self, book_info):
121         self.author = ", ".join(auth.readable() for auth in book_info.authors)
122         self.title = book_info.title
123
124     def pretty_author(self):
125         """Allows for decorating author's name."""
126         return self.author
127
128     def pretty_title(self):
129         """Allows for decorating title."""
130         return self.title
131
132     def image(self):
133         img = Image.new('RGB', (self.width, self.height), self.background_color)
134
135         if self.background_img:
136             background = Image.open(self.background_img)
137             img.paste(background, None, background)
138             del background
139
140         # WL logo
141         if self.logo_width:
142             logo = Image.open(get_resource('res/wl-logo.png'))
143             logo = logo.resize((self.logo_width, logo.size[1] * self.logo_width / logo.size[0]))
144             img.paste(logo, ((self.width - self.logo_width) / 2, img.size[1] - logo.size[1] - self.logo_bottom))
145
146         top = self.author_top
147         tbox = TextBox(
148             self.width - self.author_margin_left - self.author_margin_right,
149             self.height - top,
150             )
151         author_font = self.author_font or ImageFont.truetype(
152             get_resource('fonts/DejaVuSerif.ttf'), 30)
153         tbox.text(self.pretty_author(), self.author_color, author_font,
154             self.author_lineskip, self.author_shadow)
155         text_img = tbox.image()
156         img.paste(text_img, (self.author_margin_left, top), text_img)
157         
158         top += text_img.size[1] + self.title_top
159         tbox = TextBox(
160             self.width - self.title_margin_left - self.title_margin_right,
161             self.height - top,
162             )
163         title_font = self.author_font or ImageFont.truetype(
164             get_resource('fonts/DejaVuSerif.ttf'), 40)
165         tbox.text(self.pretty_title(), self.title_color, title_font,
166             self.title_lineskip, self.title_shadow)
167         text_img = tbox.image()
168         img.paste(text_img, (self.title_margin_left, top), text_img)
169
170         return img
171
172     def mime_type(self):
173         return self.mime_types[self.format]
174
175     def ext(self):
176         return self.exts[self.format]
177
178     def save(self, *args, **kwargs):
179         return self.image().save(format=self.format, *args, **kwargs)
180
181
182 class WLCover(Cover):
183     """Default Wolne Lektury cover generator."""
184     uses_dc_cover = True
185     author_font = ImageFont.truetype(
186         get_resource('fonts/JunicodeWL-Regular.ttf'), 20)
187     author_lineskip = 30
188     title_font = ImageFont.truetype(
189         get_resource('fonts/DejaVuSerif-Bold.ttf'), 30)
190     title_lineskip = 40
191     title_box_width = 350
192     bar_width = 35
193     background_color = '#444'
194     author_color = '#444'
195     default_background = get_resource('res/webtreatsetc-5647576127-ccby.png')
196     format = 'JPEG'
197
198     epochs = {
199         u'Starożytność': 0,
200         u'Średniowiecze': 30,
201         u'Renesans': 60,
202         u'Barok': 90,
203         u'Oświecenie': 120,
204         u'Romantyzm': 150,
205         u'Pozytywizm': 180,
206         u'Modernizm': 210,
207         u'Dwudziestolecie międzywojenne': 240,
208         u'Współczesność': 270,
209     }
210
211     def __init__(self, book_info):
212         super(WLCover, self).__init__(book_info)
213         self.kind = book_info.kind
214         self.epoch = book_info.epoch
215         if book_info.cover_url:
216             from urllib2 import urlopen
217             from StringIO import StringIO
218
219             bg_src = urlopen(book_info.cover_url)
220             self.background_img = StringIO(bg_src.read())
221             bg_src.close()
222         else:
223             self.background_img = self.default_background
224
225     def pretty_author(self):
226         return self.author.upper()
227
228     def image(self):
229         from colorsys import hsv_to_rgb
230
231         img = Image.new('RGB', (self.width, self.height), self.background_color)
232         draw = ImageDraw.Draw(img)
233
234         if self.epoch in self.epochs:
235             epoch_color = tuple(int(255 * c) for c in hsv_to_rgb(
236                     float(self.epochs[self.epoch]) / 360, .7, .7))
237         else:
238             epoch_color = '#000'
239         draw.rectangle((0, 0, self.bar_width, self.height), fill=epoch_color)
240
241         if self.background_img:
242             src = Image.open(self.background_img)
243             trg_size = (self.width - self.bar_width, self.height)
244             if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]:
245                 resized = (
246                     trg_size[0],
247                     src.size[1] * trg_size[0] / src.size[0]
248                 )
249                 cut = (resized[1] - trg_size[1]) / 2
250                 src = src.resize(resized)
251                 src = src.crop((0, cut, src.size[0], src.size[1] - cut))
252             else:
253                 resized = (
254                     src.size[0] * trg_size[1] / src.size[1],
255                     trg_size[1],
256                 )
257                 cut = (resized[0] - trg_size[0]) / 2
258                 src = src.resize(resized)
259                 src = src.crop((cut, 0, src.size[0] - cut, src.size[1]))
260             
261             img.paste(src, (self.bar_width, 0))
262             del src
263
264         box = TextBox(self.title_box_width, self.height, padding_y=20)
265         box.text(self.pretty_author(), 
266                  font=self.author_font,
267                  line_height=self.author_lineskip,
268                  color=self.author_color,
269                  shadow_color=self.author_shadow,
270                 )
271
272         box.skip(10)
273         box.draw.line((75, box.height, 275, box.height), 
274                 fill=self.author_color, width=2)
275         box.skip(15)
276
277         box.text(self.pretty_title(),
278                  line_height=self.title_lineskip,
279                  font=self.title_font,
280                  color=epoch_color,
281                  shadow_color=self.title_shadow,
282                 )
283         box_img = box.image()
284
285         if self.kind == 'Liryka':
286             # top
287             box_top = 100
288         elif self.kind == 'Epika':
289             # bottom
290             box_top = self.height - 100 - box_img.size[1]
291         else:
292             # center
293             box_top = (self.height - box_img.size[1]) / 2
294
295         box_left = self.bar_width + (self.width - self.bar_width - 
296                         box_img.size[0]) / 2
297         draw.rectangle((box_left, box_top, 
298             box_left + box_img.size[0], box_top + box_img.size[1]),
299             fill='#fff')
300         img.paste(box_img, (box_left, box_top), box_img)
301
302         return img
303
304
305
306 class VirtualoCover(Cover):
307     width = 600
308     height = 730
309     author_top = 73
310     title_top = 73
311     logo_bottom = 25
312     logo_width = 250
313
314
315 class PrestigioCover(Cover):
316     width = 580
317     height = 783
318     background_img = get_resource('res/cover-prestigio.png')
319
320     author_top = 446
321     author_margin_left = 118
322     author_margin_right = 62
323     author_lineskip = 60
324     author_color = '#fff'
325     author_shadow = '#000'
326     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50)
327
328     title_top = 0
329     title_margin_left = 118
330     title_margin_right = 62
331     title_lineskip = 60
332     title_color = '#fff'
333     title_shadow = '#000'
334     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50)
335
336     def pretty_title(self):
337         return u"„%s”" % self.title
338
339
340 class BookotekaCover(Cover):
341     width = 2140
342     height = 2733
343     background_img = get_resource('res/cover-bookoteka.png')
344
345     author_top = 480
346     author_margin_left = 307
347     author_margin_right = 233
348     author_lineskip = 156
349     author_color = '#d9d919'
350     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 130)
351
352     title_top = 400
353     title_margin_left = 307
354     title_margin_right = 233
355     title_lineskip = 168
356     title_color = '#d9d919'
357     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 140)
358
359     format = 'PNG'
360
361
362 class GandalfCover(Cover):
363     width = 600
364     height = 730
365     background_img = get_resource('res/cover-gandalf.png')
366     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 30)
367     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 40)
368     logo_bottom = 25
369     logo_width = 250
370     format = 'PNG'