Passed
Push — master ( 19f245...563a46 )
by Jace
01:59
created

memegen.domain.image._generate()   C

Complexity

Conditions 11

Size

Total Lines 83
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 48
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 60
nop 8
dl 0
loc 83
rs 5.089
c 0
b 0
f 0
ccs 48
cts 48
cp 1
crap 11

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like memegen.domain.image._generate() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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):
0 ignored issues
show
introduced by
Class 'Image' inherits from object, can be safely removed from bases in python3
Loading history...
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