fix font
[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 import bidi
7
8
9 def split_words(text):
10     words = []
11     conj = False
12     for word in text.split():
13         if conj:
14             words[-1] += ' ' + word
15         else:
16             words.append(word)
17         conj = len(word.lstrip('(').lstrip('[')) == 1
18     return words
19
20
21 class DoesNotFit(Exception):
22     pass
23
24
25 class TextBox:
26     def __init__(self, width, height, texts,
27                  font, lines, leading, tracking,
28                  align_h, align_v,
29                  font_fallbacks=None):
30         self.width = width
31         self.height = height
32         self.texts = texts
33         self.font = font
34         self.font_fallbacks = font_fallbacks
35         self.lines = lines
36         self.leading = leading
37         self.tracking = tracking
38         self.align_h = align_h
39         self.align_v = align_v
40
41         self.margin_top = self.font.getbbox('l')[1]
42
43         self.glue = self.get_length(' ')
44         
45         groups = [
46             (self.get_length(word), word)
47             for word in self.texts
48         ]
49
50         self.grouping = self.find_grouping(groups, self.lines, self.glue)
51         if self.grouping is None:
52             raise DoesNotFit()
53
54     def get_font_for_char(self, c):
55         if self.font_fallbacks:
56             for char_range, font in self.font_fallbacks.items():
57                 if char_range[0] <= c <= char_range[1]:
58                     return font
59         return self.font
60
61     def get_length(self, text):
62         text = bidi.get_display(text, base_dir='L')
63         groups = []
64         for c in text:
65             font = self.get_font_for_char(c)
66             if groups and font is groups[-1][0]:
67                 groups[-1][1] += c
68             else:
69                 groups.append([font, c])
70
71         return sum(
72             font.getlength(t)
73             for (font, t) in groups
74         ) + self.tracking * len(text)
75
76     def text_with_tracking(self, draw, pos, text, fill=None):
77         x, y = pos
78         for c in bidi.get_display(text, base_dir='L'):
79             cfont = self.get_font_for_char(c)
80             width = cfont.getlength(c)
81             draw.text((x, y), c, fill=fill, font=cfont)
82             x += width + self.tracking
83
84     def as_pil_image(self, color):
85         img = PIL.Image.new('RGBA', (self.width, self.height + 2 * self.margin_top))
86         draw = PIL.ImageDraw.ImageDraw(img)
87
88         font_letter_height = self.font.getmetrics()[0] - self.margin_top
89
90         y = self.align_v * (self.height - ((self.lines - 1) * self.leading + font_letter_height))
91         for group in self.grouping:
92             x = (self.width - group[0] + self.tracking) * self.align_h
93             self.align_h *  - group[0] / 2
94             for s, w in group[1]:
95                 self.text_with_tracking(
96                     draw, (x, y),
97                     w, fill=color
98                 )
99                 x += s + self.glue
100             y += self.leading
101
102         return img
103
104     def find_grouping(self, groups, ngroups, glue):
105         best = None
106         best_var = None
107         if not groups:
108             return []
109
110         mean = sum(g[0] for g in groups) + (len(groups) - ngroups) * glue
111         if mean > self.width * ngroups:
112             return None
113
114         for grouping in self.all_groupings(groups, ngroups, glue):
115             if max(g[0] for g in grouping) > self.width:
116                 continue
117             var = sum((g[0] - mean) ** 2 for g in grouping)
118             if best is None or best_var > var:
119                 best, best_var = grouping, var
120
121         return best
122
123     def all_groupings(self, groups, ngroups, glue):
124         if len(groups) == 1:
125             if ngroups == 1:
126                 yield [(groups[0][0], groups)]
127             return
128         next_groups = groups[1:]
129         for grouping in self.all_groupings(next_groups, ngroups, glue):
130             yield [
131                 (
132                     groups[0][0] + glue + grouping[0][0],
133                     [groups[0]] + grouping[0][1]
134                 )
135             ] + grouping[1:]
136         if ngroups > 1:
137             for grouping in self.all_groupings(next_groups, ngroups - 1, glue):
138                 yield [
139                     (groups[0][0], [groups[0]])
140                 ] + grouping