Fixes.
[librarian.git] / src / librarian / covers / utils / textbox.py
1 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
3 #
4 import PIL.Image
5 import PIL.ImageDraw
6
7
8 def split_words(text):
9     words = []
10     conj = False
11     for word in text.split():
12         if conj:
13             words[-1] += ' ' + word
14         else:
15             words.append(word)
16         conj = len(word.lstrip('(').lstrip('[')) == 1
17     return words
18
19
20
21 def text_with_tracking(draw, tracking, pos, text, fill=None, font=None):
22     x, y = pos
23     for c in text:
24         # TODO: adjust for kerning?
25         width = font.getlength(c)
26         draw.text((x, y), c, fill=fill, font=font)
27         x += width + tracking
28
29
30 class DoesNotFit(Exception):
31     pass
32
33
34 class TextBox:
35     def __init__(self, width, height, texts,
36                  font, lines, leading, tracking,
37                  align_h, align_v):
38         self.width = width
39         self.height = height
40         self.texts = texts
41         self.font = font
42         self.lines = lines
43         self.leading = leading
44         self.tracking = tracking
45         self.align_h = align_h
46         self.align_v = align_v
47
48         self.margin_top = self.font.getbbox('l')[1]
49
50         self.glue = self.get_length(' ')
51         
52         groups = [
53             (self.get_length(word), word)
54             for word in self.texts
55         ]
56
57         self.grouping = self.find_grouping(groups, self.lines, self.glue)
58         if self.grouping is None:
59             raise DoesNotFit()
60
61     def get_length(self, text):
62         return self.font.getlength(text) + self.tracking * len(text)
63         
64     def as_pil_image(self, color):
65         img = PIL.Image.new('RGBA', (self.width, self.height + 2 * self.margin_top))
66         draw = PIL.ImageDraw.ImageDraw(img)
67
68         font_letter_height = self.font.getmetrics()[0] - self.margin_top
69
70         y = self.align_v * (self.height - ((self.lines - 1) * self.leading + font_letter_height))
71         for group in self.grouping:
72             x = (self.width - group[0] + self.tracking) * self.align_h
73             self.align_h *  - group[0] / 2
74             for s, w in group[1]:
75                 text_with_tracking(
76                     draw, self.tracking, (x, y),
77                     w, fill=color, font=self.font
78                 )
79                 x += s + self.glue
80             y += self.leading
81
82         return img
83
84     def find_grouping(self, groups, ngroups, glue):
85         best = None
86         best_var = None
87         if not groups:
88             return []
89
90         mean = sum(g[0] for g in groups) + (len(groups) - ngroups) * glue
91         if mean > self.width * ngroups:
92             return None
93
94         for grouping in self.all_groupings(groups, ngroups, glue):
95             if max(g[0] for g in grouping) > self.width:
96                 continue
97             var = sum((g[0] - mean) ** 2 for g in grouping)
98             if best is None or best_var > var:
99                 best, best_var = grouping, var
100
101         return best
102
103     def all_groupings(self, groups, ngroups, glue):
104         if len(groups) == 1:
105             if ngroups == 1:
106                 yield [(groups[0][0], groups)]
107             return
108         next_groups = groups[1:]
109         for grouping in self.all_groupings(next_groups, ngroups, glue):
110             yield [
111                 (
112                     groups[0][0] + glue + grouping[0][0],
113                     [groups[0]] + grouping[0][1]
114                 )
115             ] + grouping[1:]
116         if ngroups > 1:
117             for grouping in self.all_groupings(next_groups, ngroups - 1, glue):
118                 yield [
119                     (groups[0][0], [groups[0]])
120                 ] + grouping