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