0e9b079ac3213d9ad9219c12fef8985a77e2682e
[librarian.git] / src / 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 from __future__ import unicode_literals
7
8 import re
9 from PIL import Image, ImageFont, ImageDraw, ImageFilter
10 from six import BytesIO
11 from librarian import get_resource, OutputFile, URLOpener
12
13
14 class Metric(object):
15     """Gets metrics from an object, scaling it by a factor."""
16     def __init__(self, obj, scale):
17         self._obj = obj
18         self._scale = float(scale)
19
20     def __getattr__(self, name):
21         src = getattr(self._obj, name)
22         if src and self._scale:
23             return type(src)(self._scale * src)
24         else:
25             return src
26
27
28 class TextBox(object):
29     """Creates an Image with a series of centered strings."""
30
31     SHADOW_X = 3
32     SHADOW_Y = 3
33     SHADOW_BLUR = 3
34
35     def __init__(self, max_width, max_height, padding_x=None, padding_y=None, bar_width=0, bar_color=None):
36         if padding_x is None:
37             padding_x = self.SHADOW_X + self.SHADOW_BLUR
38         if padding_y is None:
39             padding_y = self.SHADOW_Y + self.SHADOW_BLUR
40
41         self.max_width = max_width
42         self.bar_width = bar_width
43         self.bar_color = bar_color
44         self.max_text_width = max_width - 2 * padding_x - bar_width
45         self.padding_x = padding_x
46         self.padding_y = padding_y
47         self.height = padding_y
48         self.img = Image.new('RGBA', (max_width, max_height))
49         self.draw = ImageDraw.Draw(self.img)
50         self.shadow_img = None
51         self.shadow_draw = None
52
53     def skip(self, height):
54         """Skips some vertical space."""
55         self.height += height
56
57     def text(self, text, color='#000', font=None, line_height=20,
58              shadow_color=None, centering=True):
59         """Writes some centered text."""
60         text = re.sub(r'\s+', ' ', text)
61         if shadow_color:
62             if not self.shadow_img:
63                 self.shadow_img = Image.new('RGBA', self.img.size)
64                 self.shadow_draw = ImageDraw.Draw(self.shadow_img)
65         while text:
66             line = text
67             line_width = self.draw.textsize(line, font=font)[0]
68             while line_width > self.max_text_width:
69                 parts = line.rsplit(' ', 1)
70                 if len(parts) == 1:
71                     line_width = self.max_text_width
72                     break
73                 line = parts[0]
74
75                 if len(line) > 2 and line[-2] == ' ':
76                     line = line[:-2]
77
78                 line_width = self.draw.textsize(line, font=font)[0]
79             line = line.strip() + ' '
80
81             if centering:
82                 pos_x = (self.max_width - line_width) // 2
83             else:
84                 pos_x = self.bar_width + self.padding_x
85
86             if shadow_color:
87                 self.shadow_draw.text(
88                         (pos_x + self.SHADOW_X, self.height + self.SHADOW_Y),
89                         line, font=font, fill=shadow_color
90                 )
91
92             self.draw.text((pos_x, self.height), line, font=font, fill=color)
93             self.height += line_height
94             # go to next line
95             text = text[len(line):]
96
97     def image(self):
98         """Creates the actual Image object."""
99         image = Image.new('RGBA', (self.max_width,
100                                    int(round(self.height + self.padding_y))))
101
102         if self.shadow_img:
103             shadow = self.shadow_img.filter(ImageFilter.BLUR)
104             image.paste(shadow, (0, 0), shadow)
105             image.paste(self.img, (0, 0), self.img)
106         else:
107             image.paste(self.img, (0, 0))
108         return image
109
110
111 class Cover(object):
112     """Abstract base class for cover images generator."""
113     width = 600
114     height = 800
115     background_color = '#fff'
116     background_img = None
117
118     author_top = 100
119     author_margin_left = 20
120     author_margin_right = 20
121     author_lineskip = 40
122     author_color = '#000'
123     author_shadow = None
124     author_font_ttf = get_resource('fonts/DejaVuSerif.ttf')
125     author_font_size = 30
126
127     title_top = 100
128     title_margin_left = 20
129     title_margin_right = 20
130     title_lineskip = 54
131     title_color = '#000'
132     title_shadow = None
133     title_font_ttf = get_resource('fonts/DejaVuSerif.ttf')
134     title_font_size = 40
135
136     logo_bottom = None
137     logo_width = None
138     uses_dc_cover = False
139
140     format = 'JPEG'
141     scale = 1
142     scale_after = 1
143
144     exts = {
145         'JPEG': 'jpg',
146         'PNG': 'png',
147         }
148
149     mime_types = {
150         'JPEG': 'image/jpeg',
151         'PNG': 'image/png',
152         }
153
154     def __init__(self, book_info, format=None, width=None, height=None, cover_logo=None):
155         self.book_info = book_info
156         self.authors = [auth.readable() for auth in book_info.authors]
157         self.title = book_info.title
158         if format is not None:
159             self.format = format
160         self.set_size(width, height)
161
162     def set_size(self, width, height):
163         if width and height:
164             self.height = int(round(height * self.width / width))
165         scale = max(float(width or 0) / self.width,
166                     float(height or 0) / self.height)
167         if scale >= 1:
168             self.scale = scale
169         elif scale:
170             self.scale_after = scale
171
172     def pretty_authors(self):
173         """Allows for decorating authors' names."""
174         return self.authors
175
176     def pretty_title(self):
177         """Allows for decorating title."""
178         return self.title
179
180     def image(self):
181         metr = Metric(self, self.scale)
182         img = Image.new('RGB', (metr.width, metr.height),
183                         self.background_color)
184
185         if self.background_img:
186             background = Image.open(self.background_img)
187             img.paste(background, None, background)
188             del background
189
190         # WL logo
191         if metr.logo_width:
192             logo = Image.open(get_resource('res/wl-logo.png'))
193             logo = logo.resize((
194                 metr.logo_width,
195                 int(round(logo.size[1] * metr.logo_width / logo.size[0]))
196             ))
197             img.paste(logo, (
198                 (metr.width - metr.logo_width) // 2,
199                 img.size[1] - logo.size[1] - metr.logo_bottom
200             ))
201
202         top = metr.author_top
203         tbox = TextBox(
204             metr.width - metr.author_margin_left - metr.author_margin_right,
205             metr.height - top,
206             )
207
208         author_font = ImageFont.truetype(
209             self.author_font_ttf, metr.author_font_size,
210             layout_engine=ImageFont.LAYOUT_BASIC)
211         for pa in self.pretty_authors():
212             tbox.text(pa, self.author_color, author_font, metr.author_lineskip,
213                       self.author_shadow)
214         text_img = tbox.image()
215         img.paste(text_img, (metr.author_margin_left, top), text_img)
216
217         top += text_img.size[1] + metr.title_top
218         tbox = TextBox(
219             metr.width - metr.title_margin_left - metr.title_margin_right,
220             metr.height - top,
221             )
222         title_font = ImageFont.truetype(
223             self.title_font_ttf, metr.title_font_size,
224             layout_engine=ImageFont.LAYOUT_BASIC)
225         tbox.text(self.pretty_title(), self.title_color, title_font,
226                   metr.title_lineskip, self.title_shadow)
227         text_img = tbox.image()
228         img.paste(text_img, (metr.title_margin_left, top), text_img)
229
230         return img
231
232     def final_image(self):
233         img = self.image()
234         if self.scale_after != 1:
235             img = img.resize((
236                     int(round(img.size[0] * self.scale_after)),
237                     int(round(img.size[1] * self.scale_after))),
238                 Image.ANTIALIAS)
239         return img
240
241     def mime_type(self):
242         return self.mime_types[self.format]
243
244     def ext(self):
245         return self.exts[self.format]
246
247     def save(self, *args, **kwargs):
248         default_kwargs = {
249                 'format': self.format,
250                 'quality': 95,
251         }
252         default_kwargs.update(kwargs)
253         return self.final_image().save(*args, **default_kwargs)
254
255     def output_file(self, *args, **kwargs):
256         imgstr = BytesIO()
257         self.save(imgstr, *args, **kwargs)
258         return OutputFile.from_bytes(imgstr.getvalue())
259
260
261 class WLCover(Cover):
262     """Wolne Lektury cover without logos."""
263     width = 600
264     height = 833
265     uses_dc_cover = True
266     author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
267     author_font_size = 20
268     author_lineskip = 30
269     author_centering = True
270     title_font_ttf = get_resource('fonts/DejaVuSerif-Bold.ttf')
271     title_font_size = 30
272     title_lineskip = 40
273     title_box_width = 350
274     title_centering = True
275
276     box_top_margin = 100
277     box_bottom_margin = 100
278     box_padding_y = 20
279     box_above_line = 10
280     box_below_line = 15
281     box_line_left = 75
282     box_line_right = 275
283     box_line_width = 2
284     box_padding_x = 0
285     box_bar_width = 0
286
287     logo_top = 15
288     logo_width = 140
289
290     bar_width = 35
291     bar_color = '#000'
292     box_position = 'middle'
293     background_color = '#444'
294     author_color = '#444'
295     background_img = get_resource('res/cover.png')
296     background_top = False
297     format = 'JPEG'
298
299     epoch_colors = {
300         u'Starożytność': '#9e3610',
301         u'Średniowiecze': '#564c09',
302         u'Renesans': '#8ca629',
303         u'Barok': '#a6820a',
304         u'Oświecenie': '#f2802e',
305         u'Romantyzm': '#db4b16',
306         u'Pozytywizm': '#961060',
307         u'Modernizm': '#7784e0',
308         u'Dwudziestolecie międzywojenne': '#3044cf',
309         u'Współczesność': '#06393d',
310     }
311     set_title_color = True
312
313     kind_box_position = {
314         u'Liryka': 'top',
315         u'Epika': 'bottom',
316     }
317
318     def __init__(self, book_info, format=None, width=None, height=None,
319                  bleed=0, cover_logo=None):
320         super(WLCover, self).__init__(book_info, format=format, width=width,
321                                       height=height)
322
323         self.slug = book_info.url.slug
324         # Set box position.
325         self.box_position = book_info.cover_box_position or \
326             self.kind_box_position.get(book_info.kind, self.box_position)
327         # Set bar color.
328         if book_info.cover_bar_color == 'none':
329             self.bar_width = 0
330         else:
331             self.bar_color = book_info.cover_bar_color or \
332                 self.get_variable_color(book_info) or self.bar_color
333         # Set title color.
334         if self.set_title_color:
335             self.title_color = self.get_variable_color(book_info) or self.title_color
336
337         self.bleed = bleed
338         self.box_top_margin += bleed
339         self.box_bottom_margin += bleed
340         self.bar_width += bleed
341
342         if book_info.cover_url:
343             url = book_info.cover_url
344             bg_src = None
345             while True:
346                 try:
347                     if bg_src is None:
348                         import requests
349                         bg_src = requests.get(url, timeout=5)
350                     self.background_img = BytesIO(bg_src.content)
351                     bg_src.close()
352                 except Exception as e:
353                     bg_src = None
354                     print(e)
355                     import time
356                     time.sleep(1)
357                 else:
358                     break
359
360     def get_variable_color(self, book_info):
361         return self.epoch_colors.get(book_info.epoch, None)
362
363     def pretty_authors(self):
364         return [a.upper() for a in self.authors]
365
366     def add_box(self, img):
367         if self.box_position == 'none':
368             return img
369
370         metr = Metric(self, self.scale)
371
372         # Write author name.
373         box = TextBox(metr.title_box_width - 2 * self.bleed, metr.height,
374                       padding_y=metr.box_padding_y,
375                       padding_x=metr.box_padding_x,
376                       bar_width=metr.box_bar_width,
377                       bar_color=self.bar_color,
378                       )
379         author_font = ImageFont.truetype(
380             self.author_font_ttf, metr.author_font_size,
381             layout_engine=ImageFont.LAYOUT_BASIC)
382         for pa in self.pretty_authors():
383             box.text(pa, font=author_font, line_height=metr.author_lineskip,
384                      color=self.author_color, shadow_color=self.author_shadow,
385                      centering=self.author_centering
386                      )
387
388         box.skip(metr.box_above_line)
389         box.draw.line(
390             (metr.box_line_left, box.height, metr.box_line_right, box.height),
391             fill=self.author_color, width=metr.box_line_width
392         )
393         box.skip(metr.box_below_line)
394
395         # Write title.
396         title_font = ImageFont.truetype(
397             self.title_font_ttf, metr.title_font_size,
398             layout_engine=ImageFont.LAYOUT_BASIC)
399         box.text(self.pretty_title(),
400                  line_height=metr.title_lineskip,
401                  font=title_font,
402                  color=self.title_color,
403                  shadow_color=self.title_shadow,
404                  centering=self.title_centering
405                  )
406
407         box_img = box.image()
408
409         # Find box position.
410         if self.box_position == 'bottom' or box_img.size[1] + metr.box_top_margin + metr.box_bottom_margin > metr.height:
411             box_top = metr.height - metr.box_bottom_margin - box_img.size[1]
412         elif self.box_position == 'top':
413             box_top = metr.box_top_margin
414         else:   # Middle.
415             box_top = (metr.height - box_img.size[1]) // 2
416
417         box_left = metr.bar_width + (
418             metr.width - metr.bar_width - box_img.size[0] - self.bleed
419         ) // 2
420
421         # Draw the white box.
422         img_draw = ImageDraw.Draw(img)
423         img_draw.rectangle(
424             (
425                 box_left,
426                 box_top,
427                 box_left + box_img.size[0],
428                 box_top + box_img.size[1]
429             ),
430             fill='#fff'
431         )
432         # Paste the contents into the white box.
433         img.paste(box_img, (box_left, box_top), box_img)
434         if self.box_bar_width:
435             img_draw.rectangle(
436                 (
437                     box_left,
438                     box_top,
439                     box_left + metr.box_bar_width,
440                     box_top + box_img.size[1]
441                 ),
442                 fill=self.bar_color
443             )
444         return img
445
446     def add_cut_lines(self, img):
447         line_ratio = 0.5
448         if self.bleed == 0:
449             return img
450         metr = Metric(self, self.scale)
451         draw = ImageDraw.Draw(img)
452         for corner_x, corner_y in (
453                 (0, 0), (metr.width, 0),
454                 (0, metr.height), (metr.width, metr.height)
455                 ):
456             dir_x = 1 if corner_x == 0 else -1
457             dir_y = 1 if corner_y == 0 else -1
458             for offset in (-1, 0, 1):
459                 draw.line(
460                     (
461                         corner_x,
462                         corner_y + dir_y * metr.bleed + offset,
463                         corner_x + dir_x * metr.bleed * line_ratio,
464                         corner_y + dir_y * metr.bleed + offset
465                     ),
466                     fill='black' if offset == 0 else 'white',
467                     width=1
468                 )
469                 draw.line(
470                     (
471                         corner_x + dir_x * metr.bleed + offset,
472                         corner_y,
473                         corner_x + dir_x * metr.bleed + offset,
474                         corner_y + dir_y * metr.bleed * line_ratio
475                     ),
476                     fill='black' if offset == 0 else 'white',
477                     width=1
478                 )
479         return img
480
481     def image(self):
482         metr = Metric(self, self.scale)
483         img = Image.new('RGB', (metr.width, metr.height),
484                         self.background_color)
485         draw = ImageDraw.Draw(img)
486
487         draw.rectangle((0, 0, metr.bar_width, metr.height),
488                        fill=self.bar_color)
489
490         if self.background_img:
491             src = Image.open(self.background_img)
492             trg_size = (metr.width - metr.bar_width, metr.height)
493             if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]:
494                 resized = (
495                     trg_size[0],
496                     int(round(src.size[1] * trg_size[0] / src.size[0]))
497                 )
498                 if self.background_top:
499                     cut = 0
500                 else:
501                     cut = (resized[1] - trg_size[1]) // 2
502                 src = src.resize(resized, Image.ANTIALIAS)
503                 src = src.crop((0, cut, src.size[0], src.size[1] - cut))
504             else:
505                 resized = (
506                     int(round(src.size[0] * trg_size[1] / src.size[1])),
507                     trg_size[1],
508                 )
509                 cut = (resized[0] - trg_size[0]) // 2
510                 src = src.resize(resized, Image.ANTIALIAS)
511                 src = src.crop((cut, 0, src.size[0] - cut, src.size[1]))
512
513             img.paste(src, (metr.bar_width, 0))
514             del src
515
516         img = self.add_box(img)
517
518         # img = self.add_cut_lines(img)
519
520         return img
521
522
523 class WLNoBoxCover(WLCover):
524     def add_box(self, img):
525         return img
526
527
528 class LogoWLCover(WLCover):
529     gradient_height = 90
530     gradient_easing = 90
531     gradient_logo_height = 57
532     gradient_logo_max_width = 200
533     gradient_logo_margin_right = 30
534     gradient_logo_spacing = 40
535     gradient_color = '#000'
536     gradient_opacity = .6
537     gradient_logos = [
538         'res/wl-logo-white.png',
539         'res/fnp-logo-white.png',
540     ]
541     annotation = None
542     annotation_height = 10
543
544     logos_right = True
545     gradient_logo_centering = False
546
547     qrcode = None
548
549
550     def __init__(self, book_info, *args, cover_logo=None, **kwargs):
551         super(LogoWLCover, self).__init__(book_info, *args, **kwargs)
552         self.gradient_height += self.bleed
553         self.gradient_logo_margin_right += self.bleed
554
555         self.additional_cover_logos = [
556             BytesIO(URLOpener().open(cover_logo_url).read())
557             for cover_logo_url in book_info.cover_logo_urls
558         ]
559         self.end_cover_logos = []
560         if cover_logo:
561             self.additional_cover_logos.append(
562                     open(cover_logo, 'rb')
563                     )
564
565     @property
566     def has_gradient_logos(self):
567         return self.gradient_logos or self.additional_cover_logos or self.end_cover_logos or self.annotation or self.qrcode is not None
568
569     def add_gradient_logos(self, img, metr):
570         gradient = Image.new(
571             'RGBA',
572             (metr.width - metr.bar_width, metr.gradient_height),
573             self.gradient_color
574         )
575         gradient_mask = Image.new(
576             'L',
577             (metr.width - metr.bar_width, metr.gradient_height)
578         )
579         draw = ImageDraw.Draw(gradient_mask)
580         for line in range(0, metr.gradient_easing):
581             draw.line(
582                 (0, line, metr.width - metr.bar_width, line),
583                 fill=int(
584                     255 * self.gradient_opacity * line / metr.gradient_easing
585                 )
586             )
587         draw.rectangle((
588             0, metr.gradient_easing,
589             metr.width - metr.bar_width, metr.gradient_height
590         ), fill=int(255 * self.gradient_opacity))
591             
592             
593         img.paste(gradient,
594                   (metr.bar_width, metr.height - metr.gradient_height),
595                   mask=gradient_mask)
596
597         if self.logos_right:
598             cursor = metr.width - metr.gradient_logo_margin_right
599         else:
600             cursor = metr.gradient_logo_margin_right
601         logo_top = int(
602             metr.height
603             - metr.gradient_height / 2
604             - metr.gradient_logo_height / 2 - metr.bleed
605         )
606
607         logos = [
608             get_resource(logo_path)
609             for logo_path in self.gradient_logos
610         ]
611
612         logos = self.additional_cover_logos + logos + self.end_cover_logos
613
614         logos = [
615             Image.open(logo_bytes).convert('RGBA')
616             for logo_bytes in logos
617         ]
618
619         if self.qrcode is not None:
620             import qrcode
621             logos.append(qrcode.make(f'https://wolnelektury.pl/katalog/lektura/{self.slug}/?{self.qrcode}'))
622
623         if self.logos_right:
624             logos.reverse()
625
626         # See if logos fit into the gradient. If not, scale down accordingly.
627         space_for_logos = (
628             metr.width
629             - metr.bar_width
630             - 2 * metr.gradient_logo_margin_right + self.bleed
631         )
632         widths = [
633             min(
634                 metr.gradient_logo_max_width,
635                 logo.size[0] * metr.gradient_logo_height / logo.size[1]
636             )
637             for logo in logos]
638         taken_space = sum(widths) + (len(logos) - 1) * metr.gradient_logo_spacing
639         if taken_space > space_for_logos:
640             logo_scale = space_for_logos / taken_space
641         else:
642             logo_scale = 1
643             if self.gradient_logo_centering:
644                 cursor += int((space_for_logos - taken_space) / 2)
645         logo_scale = (
646             space_for_logos / taken_space
647             if taken_space > space_for_logos else 1
648         )
649         #logo_top += int(metr.gradient_logo_height * (1 - logo_scale) / 2)
650
651         for i, logo in enumerate(logos):
652             if i == -1:
653                 L_scale = 1
654             else:
655                 L_scale = logo_scale
656             L_top = logo_top + int(metr.gradient_logo_height * (1 - L_scale) / 2)
657
658             logo = logo.resize(
659                 (
660                     int(round(widths[i] * L_scale)),
661                     int(round(
662                         logo.size[1] * widths[i] / logo.size[0] * L_scale
663                     ))
664                 ),
665                 Image.ANTIALIAS)
666             if self.logos_right:
667                 cursor -= logo.size[0]
668
669             img.paste(
670                 logo,
671                 (
672                     cursor,
673                     L_top
674                     #int(round(logo_top + (metr.gradient_logo_height - logo.size[1]) * L_scale / 2))
675                 ),
676                 mask=logo
677             )
678             if self.logos_right:
679                 cursor -= int(round(metr.gradient_logo_spacing * logo_scale))
680             else:
681                 cursor += int(round(metr.gradient_logo_spacing * logo_scale)) + logo.size[0]
682
683         if self.annotation:
684             img2 = Image.new('RGBA', (metr.height, metr.height), color=None)
685             draw = ImageDraw.Draw(img2)
686             author_font = ImageFont.truetype(
687                 self.author_font_ttf, metr.annotation_height,
688                 layout_engine=ImageFont.LAYOUT_BASIC)
689             draw.text((self.annotation_height, self.annotation_height), self.annotation, font=author_font, fill='#FFFFFF')
690             img2.show()
691             img2 = img2.rotate(90)
692             img2.show()
693             img.putalpha(0)
694             img.alpha_composite(img2, (0, 0))
695             img = img.convert('RGB')
696
697         return img
698     
699     def image(self):
700         img = super(LogoWLCover, self).image()
701         metr = Metric(self, self.scale)
702         if self.has_gradient_logos:
703             img = self.add_gradient_logos(img, metr)
704         return img
705
706
707 class LegimiCover(LogoWLCover):
708     width = 210
709     height = 297
710     bar_width = 0
711     # Other bar
712
713     author_font_ttf = get_resource('fonts/Lora-Regular.ttf')
714     author_font_size = 15
715     author_lineskip = 19.5
716     author_centering = False
717     title_font_ttf = get_resource('fonts/Lora-Bold.ttf')
718     title_font_size = 15
719     title_lineskip = 19.5
720     title_centering = False
721     
722     title_box_width = 210
723
724     box_bottom_margin = 20
725     box_padding_x = 20
726     box_padding_y = 10   # do baseline
727     box_above_line = 6
728     box_below_line = 0
729     box_line_left = 0
730     box_line_right = 0
731
732     box_line_width = 0
733
734     box_bar_width = 20
735
736     #logo_top = 15
737     #logo_width = 140
738
739     bar_color = '#000'
740     box_position = 'bottom'
741     background_color = '#444'
742     author_color = '#000'
743     title_color = '#000'
744     set_title_color = False
745
746     kind_box_position = {}
747
748     box_bottom_margin_logos_add = 10
749     gradient_height = 30
750     gradient_easing = 0
751     gradient_logo_height = 20
752     gradient_logo_max_width = 200
753     gradient_logo_margin_right = 10
754     gradient_logo_spacing = 20
755     gradient_color = '#000'
756     gradient_opacity = .5
757     gradient_logos = [
758         'res/wl-logo-white.png',
759     ]
760     logos_right = False
761     gradient_logo_centering = True
762     background_top = True
763
764     genre_colors = {
765         'Artykuł': '#bf001a',
766         'Artykuł naukowy': '#bf001a',
767         'Dziennik': '#bf001a',
768         'Esej': '#bf001a',
769         'Felieton': '#bf001a',
770         'Kronika': '#bf001a',
771         'List': '#bf001a',
772         'Manifest': '#bf001a',
773         'Odczyt': '#bf001a',
774         'Pamiętnik': '#bf001a',
775         'Poradnik': '#bf001a',
776         'Praca naukowa': '#bf001a',
777         'Publicystyka': '#bf001a',
778         'Reportaż': '#bf001a',
779         'Reportaż podróżniczy': '#bf001a',
780         'Rozprawa': '#bf001a',
781         'Rozprawa polityczna': '#bf001a',
782         'Traktat': '#bf001a',
783     }
784     kind_colors = {
785         'Epika': '#9bbb2b',
786         'Liryka': '#3e626f',
787         'Dramat': '#ecbe00',
788     }
789
790     def __init__(self, *args, **kwargs):
791         super().__init__(*args, **kwargs)
792         if self.has_gradient_logos:
793             self.box_bottom_margin += self.box_bottom_margin_logos_add
794
795     def pretty_authors(self):
796         return self.authors
797
798     def get_variable_color(self, book_info):
799         for genre in book_info.genres:
800             if genre in self.genre_colors:
801                 return self.genre_colors[genre]
802         for kind in book_info.kinds:
803             if kind in self.kind_colors:
804                 return self.kind_colors[kind]
805
806 class LegimiCornerCover(LegimiCover):
807     gradient_logos = []
808     corner_width = 120
809     corner_image = get_resource('res/book-band.png')
810     
811     def image(self):
812         image = super().image()
813         metr = Metric(self, self.scale)
814
815         if self.corner_image:
816             corner = Image.open(self.corner_image).convert('RGBA')
817             corner = corner.resize(
818                 (
819                     int(round(metr.corner_width)),
820                     int(round(corner.size[1] / corner.size[0] * metr.corner_width))
821                 )
822             )
823             image.paste(corner, (
824                 metr.width - int(round(metr.corner_width)),
825                 0,
826             ), corner)
827         return image
828
829 class LegimiAudiobookCover(LegimiCornerCover):
830     corner_width = 82.5
831     corner_image = get_resource('res/audiobook-sticker.png')
832     height = 210
833
834
835 class EbookpointCover(LogoWLCover):
836     gradient_logo_height = 58
837     gradient_logo_spacing = 25
838     gradient_logos = [
839         'res/ebookpoint-logo-white.png',
840         'res/wl-logo-white.png',
841         'res/fnp-logo-white.png',
842     ]
843
844
845 class VirtualoCover(Cover):
846     width = 600
847     height = 730
848     author_top = 73
849     title_top = 73
850     logo_bottom = 25
851     logo_width = 250
852
853
854 class PrestigioCover(Cover):
855     width = 580
856     height = 783
857     background_img = get_resource('res/cover-prestigio.png')
858
859     author_top = 446
860     author_margin_left = 118
861     author_margin_right = 62
862     author_lineskip = 60
863     author_color = '#fff'
864     author_shadow = '#000'
865     author_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
866     author_font_size = 50
867
868     title_top = 0
869     title_margin_left = 118
870     title_margin_right = 62
871     title_lineskip = 60
872     title_color = '#fff'
873     title_shadow = '#000'
874     title_font_ttf = get_resource('fonts/JunicodeWL-Italic.ttf')
875     title_font_size = 50
876
877     def pretty_title(self):
878         return u"„%s”" % self.title
879
880
881 class BookotekaCover(Cover):
882     width = 2140
883     height = 2733
884     background_img = get_resource('res/cover-bookoteka.png')
885
886     author_top = 480
887     author_margin_left = 307
888     author_margin_right = 233
889     author_lineskip = 156
890     author_color = '#d9d919'
891     author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
892     author_font_size = 130
893
894     title_top = 400
895     title_margin_left = 307
896     title_margin_right = 233
897     title_lineskip = 168
898     title_color = '#d9d919'
899     title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
900     title_font_size = 140
901
902     format = 'PNG'
903
904
905 class GandalfCover(Cover):
906     width = 600
907     height = 730
908     background_img = get_resource('res/cover-gandalf.png')
909     author_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
910     author_font_size = 30
911     title_font_ttf = get_resource('fonts/JunicodeWL-Regular.ttf')
912     title_font_size = 40
913     logo_bottom = 25
914     logo_width = 250
915     format = 'PNG'
916
917
918 class KMLUCover(LogoWLCover):
919     gradient_logo_height = 58
920     gradient_logo_spacing = 25
921     gradient_logos = [
922         'res/kmlu-logo-white.png',
923         'res/wl-logo-white.png',
924         'res/fnp-logo-white.png',
925     ]
926
927
928 class MPWCover(LogoWLCover):
929     gradient_logo_height = 57
930     gradient_logo_spacing = 25
931     gradient_logos = [
932         'res/mpw-logo-white.png',
933         'res/wl-logo-white.png',
934         'res/fnp-logo-white.png',
935     ]
936
937
938 class AtriumCover(LogoWLCover):
939     gradient_logo_height = 58
940     gradient_logo_spacing = 25
941     gradient_logos = [
942         'res/atrium-logo.png',
943         'res/wl-logo-white.png',
944         'res/fnp-logo-white.png',
945     ]
946
947
948 class BNCover(LogoWLCover):
949     gradient_logos = [
950         'res/dofinansowano.png',
951         'res/MKIDN.jpg',
952         'res/BN.png',
953         'res/wl-logo-white.png',
954     ]
955 #    annotation = 'Zadanie „Udostępnienie publikacji w formatach cyfrowych” w ramach Narodowego Programu Rozwoju Czytelnictwa. Dofinansowano ze środków Ministra Kultury, Dziedzictwa Narodowego i Sportu.'
956
957
958 class FactoryCover(LogoWLCover):
959     gradient_logos = [
960         'res/factory.jpg',
961         'res/wl-logo-white.png',
962     ]
963     qrcode = 'pk_campaign=factory22'
964     format = 'PNG'
965
966     def __init__(self, *args, **kwargs):
967         kwargs.setdefault('bleed', 10)
968         return super().__init__(*args, **kwargs)
969
970     def ext(self):
971         return 'pdf'
972
973     def output_file(self, *args, **kwargs):
974         imgfile = super().output_file(*args, **kwargs)
975         import subprocess
976         import tempfile
977         with tempfile.TemporaryDirectory(prefix='factory-', dir='.') as d:
978             import os
979             import shutil
980             with open(d + '/cover.png', 'wb') as f:
981                 f.write(imgfile.get_bytes())
982             shutil.copy(get_resource('res/factory-cover.svg'), d)
983             subprocess.call(['inkscape', f'--export-filename={d}/cover.pdf', d + '/factory-cover.svg'])
984             with open(d + '/cover.pdf', 'rb') as f:
985                 return OutputFile.from_bytes(f.read())
986
987
988
989 from librarian.covers.marquise import MarquiseCover, LabelMarquiseCover
990
991 COVER_CLASSES = {
992     'legacy': LogoWLCover,
993     'kmlu': KMLUCover,
994     'mpw': MPWCover,
995     'atrium': AtriumCover,
996     'bn': BNCover,
997     'legimi': LegimiCover,
998     'legimi-corner': LegimiCornerCover,
999     'legimi-audiobook': LegimiAudiobookCover,
1000     'factory': FactoryCover,
1001     'm': MarquiseCover,
1002     'm-label': LabelMarquiseCover,
1003     'default': MarquiseCover,
1004 }
1005
1006
1007 def make_cover(book_info, *args, **kwargs):
1008     if 'cover_class' in kwargs:
1009         cover_class_name = kwargs.pop('cover_class')
1010     else:
1011         cover_class_name = book_info.cover_class
1012     cover_class = COVER_CLASSES[cover_class_name]
1013     return cover_class(book_info, *args, **kwargs)