Completed
Push — master ( 6d5df2...ce4ca1 )
by Jace
10:16 queued 05:02
created

Image.__init__()   A

Complexity

Conditions 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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