new cover scheme; Cover accepts BookInfo now; e-books include cover attribution,...
[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 = book_info.author.readable()
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
196     epochs = {
197         u'Starożytność': 0,
198         u'Średniowiecze': 30,
199         u'Renesans': 60,
200         u'Barok': 90,
201         u'Oświecenie': 120,
202         u'Romantyzm': 150,
203         u'Pozytywizm': 180,
204         u'Modernizm': 210,
205         u'Dwudziestolecie międzywojenne': 240,
206         u'Współczesność': 270,
207     }
208
209     def __init__(self, book_info):
210         super(WLCover, self).__init__(book_info)
211         self.kind = book_info.kind
212         self.epoch = book_info.epoch
213         if book_info.cover_url:
214             from urllib2 import urlopen
215             from StringIO import StringIO
216
217             bg_src = urlopen(book_info.cover_url)
218             self.background_img = StringIO(bg_src.read())
219             bg_src.close()
220
221     def pretty_author(self):
222         return self.author.upper()
223
224     def image(self):
225         from colorsys import hsv_to_rgb
226
227         img = Image.new('RGB', (self.width, self.height), self.background_color)
228         draw = ImageDraw.Draw(img)
229
230         if self.epoch in self.epochs:
231             epoch_color = tuple(int(255 * c) for c in hsv_to_rgb(
232                     float(self.epochs[self.epoch]) / 360, .7, .7))
233         else:
234             epoch_color = '#000'
235         draw.rectangle((0, 0, self.bar_width, self.height), fill=epoch_color)
236
237         if self.background_img:
238             src = Image.open(self.background_img)
239             trg_size = (self.width - self.bar_width, self.height)
240             if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]:
241                 resized = (
242                     trg_size[0],
243                     src.size[1] * trg_size[0] / src.size[0]
244                 )
245                 cut = (resized[1] - trg_size[1]) / 2
246                 src = src.resize(resized)
247                 src = src.crop((0, cut, src.size[0], src.size[1] - cut))
248             else:
249                 resized = (
250                     src.size[0] * trg_size[1] / src.size[1],
251                     trg_size[1],
252                 )
253                 cut = (resized[0] - trg_size[0]) / 2
254                 src = src.resize(resized)
255                 src = src.crop((cut, 0, src.size[0] - cut, src.size[1]))
256             
257             img.paste(src, (self.bar_width, 0))
258             del src
259
260         box = TextBox(self.title_box_width, self.height, padding_y=20)
261         box.text(self.pretty_author(), 
262                  font=self.author_font,
263                  line_height=self.author_lineskip,
264                  color=self.author_color,
265                  shadow_color=self.author_shadow,
266                 )
267
268         box.skip(10)
269         box.draw.line((75, box.height, 275, box.height), 
270                 fill=self.author_color, width=2)
271         box.skip(15)
272
273         box.text(self.pretty_title(),
274                  line_height=self.title_lineskip,
275                  font=self.title_font,
276                  color=epoch_color,
277                  shadow_color=self.title_shadow,
278                 )
279         box_img = box.image()
280
281         if self.kind == 'Liryka':
282             # top
283             box_top = 100
284         elif self.kind == 'Epika':
285             # bottom
286             box_top = self.height - 100 - box_img.size[1]
287         else:
288             # center
289             box_top = (self.height - box_img.size[1]) / 2
290
291         box_left = self.bar_width + (self.width - self.bar_width - 
292                         box_img.size[0]) / 2
293         draw.rectangle((box_left, box_top, 
294             box_left + box_img.size[0], box_top + box_img.size[1]),
295             fill='#fff')
296         img.paste(box_img, (box_left, box_top), box_img)
297
298         return img
299
300
301
302 class VirtualoCover(Cover):
303     width = 600
304     height = 730
305     author_top = 73
306     title_top = 73
307     logo_bottom = 25
308     logo_width = 250
309
310
311 class PrestigioCover(Cover):
312     width = 580
313     height = 783
314     background_img = get_resource('res/cover-prestigio.png')
315
316     author_top = 446
317     author_margin_left = 118
318     author_margin_right = 62
319     author_lineskip = 60
320     author_color = '#fff'
321     author_shadow = '#000'
322     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50)
323
324     title_top = 0
325     title_margin_left = 118
326     title_margin_right = 62
327     title_lineskip = 60
328     title_color = '#fff'
329     title_shadow = '#000'
330     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50)
331
332     def pretty_title(self):
333         return u"„%s”" % self.title
334
335
336 class BookotekaCover(Cover):
337     width = 2140
338     height = 2733
339     background_img = get_resource('res/cover-bookoteka.png')
340
341     author_top = 480
342     author_margin_left = 307
343     author_margin_right = 233
344     author_lineskip = 156
345     author_color = '#d9d919'
346     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 130)
347
348     title_top = 400
349     title_margin_left = 307
350     title_margin_right = 233
351     title_lineskip = 168
352     title_color = '#d9d919'
353     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 140)
354
355     format = 'PNG'
356
357
358 class GandalfCover(Cover):
359     width = 600
360     height = 730
361     background_img = get_resource('res/cover-gandalf.png')
362     author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 30)
363     title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 40)
364     logo_bottom = 25
365     logo_width = 250
366     format = 'PNG'