Completed
Push — master ( c6fdd5...503bdf )
by Jace
03:39
created

_generate()   D

Complexity

Conditions 10

Size

Total Lines 74

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 11.5625

Importance

Changes 0
Metric Value
cc 10
dl 0
loc 74
ccs 36
cts 48
cp 0.75
crap 11.5625
rs 4.0449
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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 _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.

1 1
import os
2 1
import hashlib
3 1
import logging
4
5 1
from PIL import Image as ImageFile, ImageFont, ImageDraw
0 ignored issues
show
Configuration introduced by
The import PIL could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
6
7
8 1
log = logging.getLogger(__name__)
9
10
11 1
class Image(object):
12
    """JPEG generated by applying text to a template."""
13
14 1
    def __init__(self, template, text, root=None,
15
                 style=None, font=None, size=None):
16 1
        self.root = root
17 1
        self.template = template
18 1
        self.style = style
19 1
        self.text = text
20 1
        self.font = font
21 1
        self.width = size.get('width') if size else None
22 1
        self.height = size.get('height') if size else None
23
24 1
    @property
25
    def path(self):
26 1
        if not self.root:
27
            return None
28
29 1
        base = os.path.join(self.root, self.template.key, self.text.path)
30 1
        custom = [self.style, self.font, self.width, self.height]
31
32 1
        if any(custom):
33 1
            slug = self.hash(custom)
34 1
            return "{}#{}.img".format(base, slug)
35
        else:
36 1
            return base + ".img"
37
38 1
    @staticmethod
39
    def hash(values):
40 1
        sha = hashlib.md5()
41 1
        for index, value in enumerate(values):
42 1
            sha.update("{}:{}".format(index, value or "").encode('utf-8'))
43 1
        return sha.hexdigest()
44
45 1
    def save(self):
46 1
        data = _generate(
47
            top=self.text.top, bottom=self.text.bottom,
48
            font=self.font.path,
49
            background=self.template.get_path(self.style),
50
            width=self.width, height=self.height,
51
        )
52
53 1
        directory = os.path.dirname(self.path)
54 1
        if not os.path.isdir(directory):
55 1
            os.makedirs(directory)
56
57 1
        log.info("Saving image: %s", self.path)
58 1
        path = data.save(self.path, format=data.format)
59
60 1
        return path
61
62
63
# The following Pillow image functions are based on:
64
# https://github.com/danieldiekmeier/memegenerator
65
66
67 1
def _generate(top, bottom, font, background, width, height):
68
    """Add text to an image and save it."""
69 1
    log.info("Loading background: %s", background)
70 1
    image = ImageFile.open(background)
71 1
    if image.mode not in ('RGB', 'RGBA'):
72 1
        if image.format == 'JPEG':
73 1
            image = image.convert('RGB')
74 1
            image.format = 'JPEG'
75
        else:
76 1
            image = image.convert('RGBA')
77 1
            image.format = 'PNG'
78
79
    # Resize to a maximum height and width
80 1
    ratio = image.size[0] / image.size[1]
81 1
    if width and height:
82
        if width < height * ratio:
83
            dimensions = width, int(width / ratio)
84
        else:
85
            dimensions = int(height * ratio), height
86 1
    elif width:
87
        dimensions = width, int(width / ratio)
88 1
    elif height:
89
        dimensions = int(height * ratio), height
90
    else:
91 1
        dimensions = 800, int(800 / ratio)
92 1
    image = image.resize(dimensions, ImageFile.LANCZOS)
93 1
    image.format = 'PNG'
94
95
    # Draw image
96 1
    draw = ImageDraw.Draw(image)
97
98 1
    max_font_size = int(image.size[1] / 5)
99 1
    min_font_size_single_line = int(image.size[1] / 12)
100 1
    max_text_len = image.size[0] - 20
101 1
    top_font_size, top = _optimize_font_size(font, top, max_font_size,
102
                                             min_font_size_single_line,
103
                                             max_text_len)
104 1
    bottom_font_size, bottom = _optimize_font_size(font, bottom, max_font_size,
105
                                                   min_font_size_single_line,
106
                                                   max_text_len)
107
108 1
    top_font = ImageFont.truetype(font, top_font_size)
109 1
    bottom_font = ImageFont.truetype(font, bottom_font_size)
110
111 1
    top_text_size = draw.multiline_textsize(top, top_font)
112 1
    bottom_text_size = draw.multiline_textsize(bottom, bottom_font)
113
114
    # Find top centered position for top text
115 1
    top_text_position_x = (image.size[0] / 2) - (top_text_size[0] / 2)
116 1
    top_text_position_y = 0
117 1
    top_text_position = (top_text_position_x, top_text_position_y)
118
119
    # Find bottom centered position for bottom text
120 1
    bottom_text_size_x = (image.size[0] / 2) - (bottom_text_size[0] / 2)
121 1
    bottom_text_size_y = image.size[1] - bottom_text_size[1] * (7 / 6)
122 1
    bottom_text_position = (bottom_text_size_x, bottom_text_size_y)
123
124 1
    _draw_outlined_text(draw, top_text_position,
125
                        top, top_font, top_font_size)
126 1
    _draw_outlined_text(draw, bottom_text_position,
127
                        bottom, bottom_font, bottom_font_size)
128
129
    # Pad image if a specific dimension is requested
130 1
    if width and height:
131
        base_width, base_height = image.size
132
        padding = ImageFile.new('RGBA', (width, height), (0, 0, 0))
133
        padding.format = 'PNG'
134
        padding_width, padding_height = padding.size
135
        offset = ((padding_width - base_width) // 2,
136
                  (padding_height - base_height) // 2)
137
        padding.paste(image, offset)
138
        image = padding
139
140 1
    return image
141
142
143 1
def _draw_outlined_text(draw_image, text_position, text, font, font_size):
144
    """Draw white text with black outline on an image."""
145
146
    # Draw black text outlines
147 1
    outline_range = max(1, font_size // 25)
148 1
    for x in range(-outline_range, outline_range + 1):
149 1
        for y in range(-outline_range, outline_range + 1):
150 1
            pos = (text_position[0] + x, text_position[1] + y)
151 1
            draw_image.multiline_text(pos, text, (0, 0, 0),
152
                                      font=font, align='center')
153
154
    # Draw inner white text
155 1
    draw_image.multiline_text(text_position, text, (255, 255, 255),
156
                              font=font, align='center')
157
158
159 1
def _optimize_font_size(font, text, max_font_size, min_font_size,
160
                        max_text_len):
161
    """Calculate the optimal font size to fit text in a given size."""
162
163
    # Check size when using smallest single line font size
164 1
    fontobj = ImageFont.truetype(font, min_font_size)
165 1
    text_size = fontobj.getsize(text)
166
167
    # Calculate font size for text, split if necessary
168 1
    if text_size[0] > max_text_len:
169 1
        phrases = _split(text)
170
    else:
171 1
        phrases = (text,)
172 1
    font_size = max_font_size // len(phrases)
173 1
    for phrase in phrases:
174 1
        font_size = min(_maximize_font_size(font, phrase, max_text_len),
175
                        font_size)
176
177
    # Rebuild text with new lines
178 1
    text = '\n'.join(phrases)
179
180 1
    return font_size, text
181
182
183 1
def _maximize_font_size(font, text, max_size):
184
    """Find the biggest font size that will fit."""
185 1
    font_size = max_size
186
187 1
    fontobj = ImageFont.truetype(font, font_size)
188 1
    text_size = fontobj.getsize(text)
189 1
    while text_size[0] > max_size and font_size > 1:
190 1
        font_size = font_size - 1
191 1
        fontobj = ImageFont.truetype(font, font_size)
192 1
        text_size = fontobj.getsize(text)
193
194 1
    return font_size
195
196
197 1
def _split(text):
198
    """Split a line of text into two similarly sized pieces.
199
200
    >>> _split("Hello, world!")
201
    ('Hello,', 'world!')
202
203
    >>> _split("This is a phrase that can be split.")
204
    ('This is a phrase', 'that can be split.')
205
206
    >>> _split("This_is_a_phrase_that_can_not_be_split.")
207
    ('This_is_a_phrase_that_can_not_be_split.',)
208
209
    """
210 1
    result = (text,)
211
212 1
    if len(text) >= 3 and ' ' in text[1:-1]:  # can split this string
213 1
        space_indices = [i for i in range(len(text)) if text[i] == ' ']
214 1
        space_proximities = [abs(i - len(text) // 2) for i in space_indices]
215 1
        for i, j in zip(space_proximities, space_indices):
216 1
            if i == min(space_proximities):
217 1
                result = (text[:j], text[j + 1:])
218 1
                break
219
220
    return result
221