Completed
Push — master ( e0fd11...3b1942 )
by Jace
21s
created

_draw_outlined_text()   B

Complexity

Conditions 4

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
c 0
b 0
f 0
dl 0
loc 24
ccs 13
cts 13
cp 1
crap 4
rs 8.6845
1 1
import os
2 1
import hashlib
3 1
from contextlib import suppress
4
5 1
from PIL import Image as ImageFile, ImageFont, ImageDraw, ImageFilter
0 ignored issues
show
introduced by
Unable to import 'PIL'
Loading history...
6
import log
7
8 1
9
class Image(object):
10
    """JPEG generated by applying text to a template."""
11 1
12
    def __init__(self, template, text, root=None,
13
                 style=None, font=None, size=None,
14 1
                 watermark="", watermark_font=None):
15
        self.root = root
16
        self.template = template
17 1
        self.style = style
18 1
        self.text = text
19 1
        self.font = font
20 1
        self.width = size.get('width') if size else None
21 1
        self.height = size.get('height') if size else None
22 1
        self.watermark = watermark
23 1
        self.watermark_font = watermark_font
24 1
25 1
    @property
26
    def path(self):
27 1
        if not self.root:
28
            return None
29 1
30
        base = os.path.join(self.root, self.template.key, self.text.path)
31
        custom = [self.style, self.font, self.watermark,
32 1
                  self.width, self.height]
33 1
34
        if any(custom):
35
            slug = self.hash(custom)
36 1
            return "{}#{}.img".format(base, slug)
37 1
        else:
38 1
            return base + ".img"
39
40 1
    @staticmethod
41
    def hash(values):
42 1
        sha = hashlib.md5()
43
        for index, value in enumerate(values):
44 1
            sha.update("{}:{}".format(index, value or "").encode('utf-8'))
45 1
        return sha.hexdigest()
46 1
47 1
    def save(self):
48
        data = _generate(
49 1
            top=self.text.top, bottom=self.text.bottom,
50 1
            font_path=self.font.path,
51
            background=self.template.get_path(self.style),
52
            width=self.width, height=self.height,
53
            watermark=self.watermark,
54
            watermark_font_path=self.watermark_font.path,
55
        )
56
57
        directory = os.path.dirname(self.path)
58
        with suppress(FileExistsError):
59 1
            os.makedirs(directory)
60 1
61 1
        log.info("Saving image: %s", self.path)
62
        path = data.save(self.path, format=data.format)
63 1
64 1
        return path
65
66 1
67
def _generate(top, bottom, font_path, background, width, height,
68
              watermark, watermark_font_path):
69 1
    """Add text to an image and save it."""
70
    log.info("Loading background: %s", background)
71
    background_image = ImageFile.open(background)
72 1
    if background_image.mode not in ('RGB', 'RGBA'):
73 1
        if background_image.format == 'JPEG':
74 1
            background_image = background_image.convert('RGB')
75 1
            background_image.format = 'JPEG'
76 1
        else:
77 1
            background_image = background_image.convert('RGBA')
78
            background_image.format = 'PNG'
79 1
80 1
    # Resize to a maximum height and width
81
    ratio = background_image.size[0] / background_image.size[1]
82
    pad_image = bool(width and height)
83 1
    pad_watermark = True
84 1
    if pad_image:
85 1
        if width < height * ratio:
86 1
            dimensions = width, int(width / ratio)
87
            pad_watermark = False
88 1
        else:
89 1
            dimensions = int(height * ratio), height
90 1
    elif width:
91 1
        dimensions = width, int(width / ratio)
92 1
    elif height:
93
        dimensions = int(height * ratio), height
94 1
    else:
95 1
        dimensions = 600, int(600 / ratio)
96 1
    image = background_image.resize(dimensions, ImageFile.LANCZOS)
97
    image.format = 'PNG'
98
99 1
    # Draw image
100
    draw = ImageDraw.Draw(image)
101 1
102 1
    max_font_size = int(image.size[1] / 9)
103 1
    min_font_size_single_line = int(image.size[1] / 12)
104 1
    max_text_len = image.size[0] - 20
105
    top_font_size, top = _optimize_font_size(
106
        font_path, top, max_font_size,
107
        min_font_size_single_line, max_text_len,
108 1
    )
109
    bottom_font_size, bottom = _optimize_font_size(
110
        font_path, bottom, max_font_size,
111
        min_font_size_single_line, max_text_len,
112
    )
113 1
114 1
    top_font = ImageFont.truetype(font_path, top_font_size)
115
    bottom_font = ImageFont.truetype(font_path, bottom_font_size)
116 1
117 1
    top_text_size = draw.multiline_textsize(top, top_font)
118
    bottom_text_size = draw.multiline_textsize(bottom, bottom_font)
119
120 1
    # Find top centered position for top text
121 1
    top_text_pos_x = (image.size[0] / 2) - (top_text_size[0] / 2)
122 1
    top_text_pos_y = 0
123
    top_text_pos = (top_text_pos_x, top_text_pos_y)
124
125 1
    # Find bottom centered position for bottom text
126 1
    bottom_text_size_x = (image.size[0] / 2) - (bottom_text_size[0] / 2)
127 1
    bottom_text_size_y = image.size[1] - bottom_text_size[1] * (7 / 6)
128 1
    if watermark and pad_watermark:
129 1
        bottom_text_size_y = bottom_text_size_y - 5
130
    bottom_text_pos = (bottom_text_size_x, bottom_text_size_y)
131 1
132
    outline = int(round(max(1, min(dimensions) / 300)))
133 1
    t_outline = min(outline, top_font_size // 20)
134
    b_outline = min(outline, bottom_font_size // 20)
135
    _draw_outlined_text(image, top_text_pos, top, top_font, t_outline)
136
    _draw_outlined_text(image, bottom_text_pos, bottom, bottom_font, b_outline)
137 1
138 1
    # Pad image if a specific dimension is requested
139
    if pad_image:
140
        image = _add_blurred_background(image, background_image, width, height)
141 1
142 1
    # Add watermark
143 1
    if watermark:
144 1
        watermark_font = ImageFont.truetype(watermark_font_path, 11)
145
        watermark_pos = (3, image.size[1] - 15)
146
        _draw_outlined_text(image, watermark_pos, watermark, watermark_font,
147 1
                            alpha=True)
148
149
    return image
150 1
151
152
def _optimize_font_size(font, text, max_font_size, min_font_size,
153
                        max_text_len):
154
    """Calculate the optimal font size to fit text in a given size."""
155 1
156 1
    # Check size when using smallest single line font size
157
    fontobj = ImageFont.truetype(font, min_font_size)
158
    text_size = fontobj.getsize(text)
159 1
160 1
    # Calculate font size for text, split if necessary
161
    if text_size[0] > max_text_len:
162 1
        phrases = _split(text)
163 1
    else:
164 1
        phrases = (text,)
165 1
    font_size = max_font_size
166
    for phrase in phrases:
167
        font_size = min(_maximize_font_size(font, phrase, max_text_len),
168
                        font_size)
169 1
170
    # Rebuild text with new lines
171 1
    text = '\n'.join(phrases)
172
173
    return font_size, text
174 1
175
176
def _draw_outlined_text(image, text_pos, text, font, width=1, alpha=False):
177
    """Draw white text with black outline on an image."""
178 1
    if alpha:
179 1
        black_alpha = 170
180 1
        white_alpha = 128
181 1
    else:
182 1
        black_alpha = 255
183
        white_alpha = 240
184
185
    overlay = ImageFile.new('RGBA', image.size)
186 1
    draw = ImageDraw.ImageDraw(overlay, 'RGBA')
187
188
    # Draw black text outlines
189
    for x in range(-width, 1 + width):
190 1
        for y in range(-width, 1 + width):
191
            pos = (text_pos[0] + x, text_pos[1] + y)
192 1
            draw.multiline_text(pos, text, (0, 0, 0, black_alpha),
193
                                font=font, align='center')
194 1
195 1
    # Draw inner white text
196 1
    draw.multiline_text(text_pos, text, (255, 255, 255, white_alpha),
197 1
                        font=font, align='center')
198 1
199
    image.paste(overlay, None, overlay)
200
201 1
202 1
def _add_blurred_background(foreground, background, width, height):
203
    """Add a blurred background to match the requested dimensions."""
204 1
    base_width, base_height = foreground.size
205
206 1
    border_width = min(width, base_width + 2)
207 1
    border_height = min(height, base_height + 2)
208
    border_dimensions = border_width, border_height
209 1
    border = ImageFile.new('RGB', border_dimensions)
210 1
    border.paste(foreground, ((border_width - base_width) // 2,
211
                              (border_height - base_height) // 2))
212 1
213
    padded_dimensions = (width, height)
214 1
    padded = background.resize(padded_dimensions, ImageFile.LANCZOS)
215
216
    darkened = padded.point(lambda p: p * 0.4)
217 1
218
    blurred = darkened.filter(ImageFilter.GaussianBlur(5))
219 1
    blurred.format = 'PNG'
220
221 1
    blurred_width, blurred_height = blurred.size
222 1
    offset = ((blurred_width - border_width) // 2,
223 1
              (blurred_height - border_height) // 2)
224 1
    blurred.paste(border, offset)
225 1
226 1
    return blurred
227
228 1
229
def _maximize_font_size(font, text, max_size):
230
    """Find the biggest font size that will fit."""
231 1
    font_size = max_size
232
233
    fontobj = ImageFont.truetype(font, font_size)
234
    text_size = fontobj.getsize(text)
235
    while text_size[0] > max_size and font_size > 1:
236
        font_size = font_size - 1
237
        fontobj = ImageFont.truetype(font, font_size)
238
        text_size = fontobj.getsize(text)
239
240
    return font_size
241
242
243
def _split(text):
244 1
    """Split a line of text into two similarly sized pieces.
245
246 1
    >>> _split("Hello, world!")
247 1
    ('Hello,', 'world!')
248 1
249 1
    >>> _split("This is a phrase that can be split.")
250 1
    ('This is a phrase', 'that can be split.')
251 1
252 1
    >>> _split("This_is_a_phrase_that_can_not_be_split.")
253
    ('This_is_a_phrase_that_can_not_be_split.',)
254 1
255
    """
256
    result = (text,)
257
258
    if len(text) >= 3 and ' ' in text[1:-1]:  # can split this string
259
        space_indices = [i for i in range(len(text)) if text[i] == ' ']
260
        space_proximities = [abs(i - len(text) // 2) for i in space_indices]
261
        for i, j in zip(space_proximities, space_indices):
262
            if i == min(space_proximities):
263
                result = (text[:j], text[j + 1:])
264
                break
265
266
    return result
267