sketchingpy.pillow_util

Convienence functions for Pillow (PIL).

License:

BSD

  1"""Convienence functions for Pillow (PIL).
  2
  3License:
  4    BSD
  5"""
  6
  7import itertools
  8import math
  9import typing
 10
 11import PIL.Image
 12import PIL.ImageColor
 13import PIL.ImageDraw
 14
 15import sketchingpy.bezier_util
 16import sketchingpy.shape_struct
 17import sketchingpy.state_struct
 18
 19COLOR_MAYBE = typing.Optional[typing.Union[
 20    typing.Tuple[int, int, int],
 21    typing.Tuple[int, int, int, int]
 22]]
 23
 24
 25class PillowUtilImage:
 26    """Wrapper around a native Pillow image with additional metadata."""
 27
 28    def __init__(self, x: float, y: float, width: float, height: float, image: PIL.Image.Image):
 29        """Create a new wrapped pillow image.
 30
 31        Args:
 32            x: The starting x coordinate of this image in pixels.
 33            y: The starting y coordinate of this image in pixels.
 34            width: The current width of this image in pixels.
 35            height: The current height of this image in pixels.
 36            image: The image decorated.
 37        """
 38        self._x = x
 39        self._y = y
 40        self._width = width
 41        self._height = height
 42        self._image = image
 43
 44    def get_x(self) -> float:
 45        """Get the horizontal coordinate of this image (left).
 46
 47        Returns:
 48            The starting x coordinate of this image in pixels.
 49        """
 50        return self._x
 51
 52    def get_y(self) -> float:
 53        """Get the vertical coordinate of this image (top).
 54
 55        Returns:
 56            The starting y coordinate of this image in pixels.
 57        """
 58        return self._y
 59
 60    def get_width(self) -> float:
 61        """Get the horizontal size of this image at time of construction.
 62
 63        Returns:
 64            Width of this image in pixels.
 65        """
 66        return self._width
 67
 68    def get_height(self) -> float:
 69        """Get the vertical size of this image at time of construction.
 70
 71        Returns:
 72            Height of this image in pixels.
 73        """
 74        return self._height
 75
 76    def get_image(self) -> PIL.Image.Image:
 77        """Get the underlying Pillow image.
 78
 79        Returns:
 80            The pillow image that this wraps.
 81        """
 82        return self._image
 83
 84
 85def make_arc_image(min_x: float, min_y: float, width: float, height: float, start_rad: float,
 86    end_rad: float, stroke_enabled: bool, fill_enabled: bool, stroke_color: COLOR_MAYBE,
 87    fill_color: COLOR_MAYBE, stroke_weight: float) -> PillowUtilImage:
 88    """Draw an arc using Pillow.
 89
 90    Args:
 91        min_x: The left coordinate.
 92        min_y: The top coordinate.
 93        width: Width of the arc in pixels.
 94        height: Height of the arc in pixels.
 95        start_rad: Starting angle (radians) of the arc.
 96        end_rad: Ending angle (radians) of the arc.
 97        stroke_enabled: Boolean indicating if the stroke should be drawn.
 98        fill_enabled: Boolean indicating if the fill should be drawn.
 99        stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
100        fill_color: The color as tuple with which the fill should be drawn or None if no fill.
101        stroke_weight: The size of the stroke in pixels.
102
103    Returns:
104        Decorated Pillow image with the drawn arc.
105    """
106    if stroke_enabled:
107        stroke_weight_realized = stroke_weight
108    else:
109        stroke_weight_realized = 0
110
111    width_offset = width + stroke_weight_realized
112    height_offset = height + stroke_weight_realized
113
114    size = (round(width_offset) + 1, round(height_offset) + 1)
115    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
116    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
117
118    bounds = (
119        (0, 0),
120        (width, height)
121    )
122
123    start_deg = math.degrees(start_rad) - 90
124    end_deg = math.degrees(end_rad) - 90
125
126    if fill_enabled and fill_color is not None:
127        target_surface.chord(
128            bounds,
129            start_deg,
130            end_deg,
131            fill=fill_color
132        )
133
134    if stroke_enabled and stroke_color is not None:
135        target_surface.arc(
136            bounds,
137            start_deg,
138            end_deg,
139            fill=stroke_color,
140            width=stroke_weight_realized
141        )
142
143    return PillowUtilImage(
144        min_x,
145        min_y,
146        width_offset,
147        height_offset,
148        target_image
149    )
150
151
152def make_rect_image(min_x: float, min_y: float, width: float, height: float, stroke_enabled: bool,
153    fill_enabled: bool, stroke_color: COLOR_MAYBE, fill_color: COLOR_MAYBE,
154    stroke_weight: float) -> PillowUtilImage:
155    """Draw a rectangle using Pillow.
156
157    Args:
158        min_x: The left coordinate.
159        min_y: The top coordinate.
160        width: Width of the rect in pixels.
161        height: Height of the rect in pixels.
162        stroke_enabled: Boolean indicating if the stroke should be drawn.
163        fill_enabled: Boolean indicating if the fill should be drawn.
164        stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
165        fill_color: The color as tuple with which the fill should be drawn or None if no fill.
166        stroke_weight: The size of the stroke in pixels.
167
168    Returns:
169        Decorated Pillow image with the drawn rect.
170    """
171    if stroke_enabled:
172        stroke_weight_realized = stroke_weight
173    else:
174        stroke_weight_realized = 0
175
176    width_offset = width + math.floor(stroke_weight_realized / 2) * 2
177    height_offset = height + math.floor(stroke_weight_realized / 2) * 2
178
179    size = (round(width_offset) + 1, round(height_offset) + 1)
180    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
181    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
182
183    bounds = (
184        0,
185        0,
186        width_offset,
187        height_offset
188    )
189    target_surface.rectangle(
190        bounds,
191        fill=fill_color if fill_enabled else None,
192        outline=stroke_color if stroke_enabled else None,
193        width=stroke_weight_realized
194    )
195
196    return PillowUtilImage(
197        min_x - round(stroke_weight_realized / 2),
198        min_y - round(stroke_weight_realized / 2),
199        width_offset,
200        height_offset,
201        target_image
202    )
203
204
205def make_ellipse_image(min_x: float, min_y: float, width: float, height: float,
206    stroke_enabled: bool, fill_enabled: bool, stroke_color: COLOR_MAYBE, fill_color: COLOR_MAYBE,
207    stroke_weight: float) -> PillowUtilImage:
208    """Draw a ellipse using Pillow.
209
210    Args:
211        min_x: The left coordinate.
212        min_y: The top coordinate.
213        width: Width of the rect in pixels.
214        height: Height of the rect in pixels.
215        stroke_enabled: Boolean indicating if the stroke should be drawn.
216        fill_enabled: Boolean indicating if the fill should be drawn.
217        stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
218        fill_color: The color as tuple with which the fill should be drawn or None if no fill.
219        stroke_weight: The size of the stroke in pixels.
220
221    Returns:
222        Decorated Pillow image with the drawn ellipse.
223    """
224    if stroke_enabled:
225        stroke_weight_realized = stroke_weight
226    else:
227        stroke_weight_realized = 0
228
229    width_offset = width + math.floor(stroke_weight_realized / 2) * 2
230    height_offset = height + math.floor(stroke_weight_realized / 2) * 2
231
232    size = (round(width_offset) + 1, round(height_offset) + 1)
233    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
234    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
235
236    bounds = (
237        0,
238        0,
239        width_offset,
240        height_offset
241    )
242    target_surface.ellipse(
243        bounds,
244        fill=fill_color if fill_enabled else None,
245        outline=stroke_color if stroke_enabled else None,
246        width=stroke_weight_realized
247    )
248
249    return PillowUtilImage(
250        min_x - round(stroke_weight_realized / 2),
251        min_y - round(stroke_weight_realized / 2),
252        width_offset,
253        height_offset,
254        target_image
255    )
256
257
258class SegmentSimplifier:
259    """Utility to help draw shapes' segments in Pillow."""
260
261    def __init__(self, start_x: float, start_y: float):
262        """Create a new simplifier.
263
264        Args:
265            start_x: The starting x coordinate of the shape.
266            start_y: The starting y coordinate of the shape.
267        """
268        self._previous_x = start_x
269        self._previous_y = start_y
270
271    def simplify(self,
272        segment: sketchingpy.shape_struct.Line) -> typing.Iterable[typing.Iterable[float]]:
273        """Turn a segment into a series of coordinates.
274
275        Simplify a segment into a simple series of x, y coordinates which approximate the underlying
276        shape using a series of straight lines.
277
278        Args:
279            segment: The segment to simplify.
280
281        Returns:
282            Collection of x, y coordinates.
283        """
284        ret_vals: typing.Iterable[typing.Tuple[float, float]] = []
285
286        strategy = segment.get_strategy()
287        if strategy == 'straight':
288            ret_vals = ((segment.get_destination_x(), segment.get_destination_y()),)
289        elif strategy == 'bezier':
290            change_y = abs(segment.get_control_y2() - segment.get_control_y1())
291            change_x = abs(segment.get_control_x2() - segment.get_control_x1())
292
293            num_segs = (change_y**2 + change_x**2) ** 0.5 / 10
294            num_segs_int = int(num_segs)
295
296            bezier_maker = sketchingpy.bezier_util.BezierMaker()
297            bezier_maker.add_point(self._previous_x, self._previous_y)
298            bezier_maker.add_point(segment.get_control_x1(), segment.get_control_y1())
299            bezier_maker.add_point(segment.get_control_x2(), segment.get_control_y2())
300            bezier_maker.add_point(segment.get_destination_x(), segment.get_destination_y())
301
302            ret_vals = bezier_maker.get_points(num_segs_int)
303        else:
304            raise RuntimeError('Unknown segment strategy: ' + strategy)
305
306        self._previous_x = segment.get_destination_x()
307        self._previous_y = segment.get_destination_y()
308
309        return ret_vals
310
311
312def make_shape_image(shape: sketchingpy.shape_struct.Shape, stroke_enabled: bool,
313    fill_enabled: bool, stroke_color: COLOR_MAYBE, fill_color: COLOR_MAYBE,
314    stroke_weight: float) -> PillowUtilImage:
315    """Draw a Sketchingpy shape into a pillow image.
316
317    Args:
318        shape: The shape to be drawn.
319        stroke_enabled: Boolean indicating if the stroke should be drawn.
320        fill_enabled: Boolean indicating if the fill should be drawn.
321        stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
322        fill_color: The color as tuple with which the fill should be drawn or None if no fill.
323        stroke_weight: The size of the stroke in pixels.
324
325    Returns:
326        Decorated Pillow image with the drawn shape.
327    """
328
329    if not shape.get_is_finished():
330        raise RuntimeError('Finish shape before drawing.')
331
332    min_x = shape.get_min_x()
333    max_x = shape.get_max_x()
334    min_y = shape.get_min_y()
335    max_y = shape.get_max_y()
336
337    width = max_x - min_x
338    height = max_y - min_y
339    width_offset = width + stroke_weight * 2
340    height_offset = height + stroke_weight * 2
341
342    size = (round(width_offset) + 1, round(height_offset) + 1)
343    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
344    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
345
346    def adjust_coord(coord):
347        return (
348            coord[0] - min_x + stroke_weight,
349            coord[1] - min_y + stroke_weight
350        )
351
352    start_x = shape.get_start_x()
353    start_y = shape.get_start_y()
354    start_coords = [(start_x, start_y)]
355
356    simplified_segements = []
357    simplifier = SegmentSimplifier(start_x, start_y)
358    for segment in shape.get_segments():
359        simplified_segements.append(simplifier.simplify(segment))
360
361    later_coords = itertools.chain(*simplified_segements)
362    all_coords = itertools.chain(start_coords, later_coords)
363    coords = [adjust_coord(x) for x in all_coords]
364
365    if shape.get_is_closed():
366        target_surface.polygon(coords, fill=fill_color, outline=stroke_color, width=stroke_weight)
367    else:
368        target_surface.line(coords, fill=stroke_color, width=stroke_weight, joint='curve')
369
370    return PillowUtilImage(
371        min_x - stroke_weight,
372        min_y - stroke_weight,
373        width_offset,
374        height_offset,
375        target_image
376    )
377
378
379def make_text_image(x: float, y: float, content: str, font: PIL.ImageFont.ImageFont,
380    stroke_enabled: bool, fill_enabled: bool, stroke: COLOR_MAYBE, fill: COLOR_MAYBE,
381    stroke_weight: float, anchor: str):
382    """Draw text into a pillow image.
383
384    Args:
385        x: The x coordinate of the anchor.
386        y: The y coordinate of the anchor.
387        font: The font (PIL native) to use in drawing the text.
388        stroke_enabled: Boolean indicating if the stroke should be drawn.
389        fill_enabled: Boolean indicating if the fill should be drawn.
390        stroke: The color as tuple with which the stroke should be drawn or None if no stroke.
391        fill: The color as tuple with which the fill should be drawn or None if no fill.
392        stroke_weight: The size of the stroke in pixels.
393        anchor: Anchor string describing vertical and horizontal alignment.
394
395    Returns:
396        Decorated Pillow image with the drawn text.
397    """
398
399    temp_image = PIL.Image.new('RGBA', (1, 1), (255, 255, 255, 0))
400    temp_surface = PIL.ImageDraw.Draw(temp_image, 'RGBA')
401    stroke_weight_int = round(stroke_weight)
402    bounding_box = temp_surface.textbbox(
403        (stroke_weight_int, stroke_weight_int),
404        content,
405        font=font,
406        anchor=anchor,
407        stroke_width=stroke_weight_int
408    )
409
410    start_x = bounding_box[0]
411    end_x = bounding_box[2]
412
413    start_y = bounding_box[1]
414    end_y = bounding_box[3]
415
416    width = end_x - start_x
417    height = end_y - start_y
418
419    width_offset = width + stroke_weight * 2
420    height_offset = height + stroke_weight * 2
421
422    size = (round(width_offset) + 2, round(height_offset) + 1)
423    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
424    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
425
426    if stroke_enabled:
427        target_surface.text(
428            (
429                round(-1 * start_x + stroke_weight + 1),
430                round(-1 * start_y + stroke_weight)
431            ),
432            content,
433            font=font,
434            anchor=anchor,
435            stroke_width=round(stroke_weight),
436            stroke_fill=stroke,
437            fill=(0, 0, 0, 0)
438        )
439
440    if fill_enabled:
441        target_surface.text(
442            (
443                -1 * start_x + stroke_weight + 1,
444                -1 * start_y + stroke_weight
445            ),
446            content,
447            font=font,
448            anchor=anchor,
449            fill=fill
450        )
451
452    return PillowUtilImage(
453        start_x - stroke_weight + x,
454        start_y - stroke_weight + y,
455        width_offset,
456        height_offset,
457        target_image
458    )
COLOR_MAYBE = typing.Union[typing.Tuple[int, int, int], typing.Tuple[int, int, int, int], NoneType]
class PillowUtilImage:
26class PillowUtilImage:
27    """Wrapper around a native Pillow image with additional metadata."""
28
29    def __init__(self, x: float, y: float, width: float, height: float, image: PIL.Image.Image):
30        """Create a new wrapped pillow image.
31
32        Args:
33            x: The starting x coordinate of this image in pixels.
34            y: The starting y coordinate of this image in pixels.
35            width: The current width of this image in pixels.
36            height: The current height of this image in pixels.
37            image: The image decorated.
38        """
39        self._x = x
40        self._y = y
41        self._width = width
42        self._height = height
43        self._image = image
44
45    def get_x(self) -> float:
46        """Get the horizontal coordinate of this image (left).
47
48        Returns:
49            The starting x coordinate of this image in pixels.
50        """
51        return self._x
52
53    def get_y(self) -> float:
54        """Get the vertical coordinate of this image (top).
55
56        Returns:
57            The starting y coordinate of this image in pixels.
58        """
59        return self._y
60
61    def get_width(self) -> float:
62        """Get the horizontal size of this image at time of construction.
63
64        Returns:
65            Width of this image in pixels.
66        """
67        return self._width
68
69    def get_height(self) -> float:
70        """Get the vertical size of this image at time of construction.
71
72        Returns:
73            Height of this image in pixels.
74        """
75        return self._height
76
77    def get_image(self) -> PIL.Image.Image:
78        """Get the underlying Pillow image.
79
80        Returns:
81            The pillow image that this wraps.
82        """
83        return self._image

Wrapper around a native Pillow image with additional metadata.

PillowUtilImage( x: float, y: float, width: float, height: float, image: PIL.Image.Image)
29    def __init__(self, x: float, y: float, width: float, height: float, image: PIL.Image.Image):
30        """Create a new wrapped pillow image.
31
32        Args:
33            x: The starting x coordinate of this image in pixels.
34            y: The starting y coordinate of this image in pixels.
35            width: The current width of this image in pixels.
36            height: The current height of this image in pixels.
37            image: The image decorated.
38        """
39        self._x = x
40        self._y = y
41        self._width = width
42        self._height = height
43        self._image = image

Create a new wrapped pillow image.

Arguments:
  • x: The starting x coordinate of this image in pixels.
  • y: The starting y coordinate of this image in pixels.
  • width: The current width of this image in pixels.
  • height: The current height of this image in pixels.
  • image: The image decorated.
def get_x(self) -> float:
45    def get_x(self) -> float:
46        """Get the horizontal coordinate of this image (left).
47
48        Returns:
49            The starting x coordinate of this image in pixels.
50        """
51        return self._x

Get the horizontal coordinate of this image (left).

Returns:

The starting x coordinate of this image in pixels.

def get_y(self) -> float:
53    def get_y(self) -> float:
54        """Get the vertical coordinate of this image (top).
55
56        Returns:
57            The starting y coordinate of this image in pixels.
58        """
59        return self._y

Get the vertical coordinate of this image (top).

Returns:

The starting y coordinate of this image in pixels.

def get_width(self) -> float:
61    def get_width(self) -> float:
62        """Get the horizontal size of this image at time of construction.
63
64        Returns:
65            Width of this image in pixels.
66        """
67        return self._width

Get the horizontal size of this image at time of construction.

Returns:

Width of this image in pixels.

def get_height(self) -> float:
69    def get_height(self) -> float:
70        """Get the vertical size of this image at time of construction.
71
72        Returns:
73            Height of this image in pixels.
74        """
75        return self._height

Get the vertical size of this image at time of construction.

Returns:

Height of this image in pixels.

def get_image(self) -> PIL.Image.Image:
77    def get_image(self) -> PIL.Image.Image:
78        """Get the underlying Pillow image.
79
80        Returns:
81            The pillow image that this wraps.
82        """
83        return self._image

Get the underlying Pillow image.

Returns:

The pillow image that this wraps.

def make_arc_image( min_x: float, min_y: float, width: float, height: float, start_rad: float, end_rad: float, stroke_enabled: bool, fill_enabled: bool, stroke_color: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], fill_color: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], stroke_weight: float) -> PillowUtilImage:
 86def make_arc_image(min_x: float, min_y: float, width: float, height: float, start_rad: float,
 87    end_rad: float, stroke_enabled: bool, fill_enabled: bool, stroke_color: COLOR_MAYBE,
 88    fill_color: COLOR_MAYBE, stroke_weight: float) -> PillowUtilImage:
 89    """Draw an arc using Pillow.
 90
 91    Args:
 92        min_x: The left coordinate.
 93        min_y: The top coordinate.
 94        width: Width of the arc in pixels.
 95        height: Height of the arc in pixels.
 96        start_rad: Starting angle (radians) of the arc.
 97        end_rad: Ending angle (radians) of the arc.
 98        stroke_enabled: Boolean indicating if the stroke should be drawn.
 99        fill_enabled: Boolean indicating if the fill should be drawn.
100        stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
101        fill_color: The color as tuple with which the fill should be drawn or None if no fill.
102        stroke_weight: The size of the stroke in pixels.
103
104    Returns:
105        Decorated Pillow image with the drawn arc.
106    """
107    if stroke_enabled:
108        stroke_weight_realized = stroke_weight
109    else:
110        stroke_weight_realized = 0
111
112    width_offset = width + stroke_weight_realized
113    height_offset = height + stroke_weight_realized
114
115    size = (round(width_offset) + 1, round(height_offset) + 1)
116    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
117    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
118
119    bounds = (
120        (0, 0),
121        (width, height)
122    )
123
124    start_deg = math.degrees(start_rad) - 90
125    end_deg = math.degrees(end_rad) - 90
126
127    if fill_enabled and fill_color is not None:
128        target_surface.chord(
129            bounds,
130            start_deg,
131            end_deg,
132            fill=fill_color
133        )
134
135    if stroke_enabled and stroke_color is not None:
136        target_surface.arc(
137            bounds,
138            start_deg,
139            end_deg,
140            fill=stroke_color,
141            width=stroke_weight_realized
142        )
143
144    return PillowUtilImage(
145        min_x,
146        min_y,
147        width_offset,
148        height_offset,
149        target_image
150    )

Draw an arc using Pillow.

Arguments:
  • min_x: The left coordinate.
  • min_y: The top coordinate.
  • width: Width of the arc in pixels.
  • height: Height of the arc in pixels.
  • start_rad: Starting angle (radians) of the arc.
  • end_rad: Ending angle (radians) of the arc.
  • stroke_enabled: Boolean indicating if the stroke should be drawn.
  • fill_enabled: Boolean indicating if the fill should be drawn.
  • stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
  • fill_color: The color as tuple with which the fill should be drawn or None if no fill.
  • stroke_weight: The size of the stroke in pixels.
Returns:

Decorated Pillow image with the drawn arc.

def make_rect_image( min_x: float, min_y: float, width: float, height: float, stroke_enabled: bool, fill_enabled: bool, stroke_color: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], fill_color: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], stroke_weight: float) -> PillowUtilImage:
153def make_rect_image(min_x: float, min_y: float, width: float, height: float, stroke_enabled: bool,
154    fill_enabled: bool, stroke_color: COLOR_MAYBE, fill_color: COLOR_MAYBE,
155    stroke_weight: float) -> PillowUtilImage:
156    """Draw a rectangle using Pillow.
157
158    Args:
159        min_x: The left coordinate.
160        min_y: The top coordinate.
161        width: Width of the rect in pixels.
162        height: Height of the rect in pixels.
163        stroke_enabled: Boolean indicating if the stroke should be drawn.
164        fill_enabled: Boolean indicating if the fill should be drawn.
165        stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
166        fill_color: The color as tuple with which the fill should be drawn or None if no fill.
167        stroke_weight: The size of the stroke in pixels.
168
169    Returns:
170        Decorated Pillow image with the drawn rect.
171    """
172    if stroke_enabled:
173        stroke_weight_realized = stroke_weight
174    else:
175        stroke_weight_realized = 0
176
177    width_offset = width + math.floor(stroke_weight_realized / 2) * 2
178    height_offset = height + math.floor(stroke_weight_realized / 2) * 2
179
180    size = (round(width_offset) + 1, round(height_offset) + 1)
181    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
182    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
183
184    bounds = (
185        0,
186        0,
187        width_offset,
188        height_offset
189    )
190    target_surface.rectangle(
191        bounds,
192        fill=fill_color if fill_enabled else None,
193        outline=stroke_color if stroke_enabled else None,
194        width=stroke_weight_realized
195    )
196
197    return PillowUtilImage(
198        min_x - round(stroke_weight_realized / 2),
199        min_y - round(stroke_weight_realized / 2),
200        width_offset,
201        height_offset,
202        target_image
203    )

Draw a rectangle using Pillow.

Arguments:
  • min_x: The left coordinate.
  • min_y: The top coordinate.
  • width: Width of the rect in pixels.
  • height: Height of the rect in pixels.
  • stroke_enabled: Boolean indicating if the stroke should be drawn.
  • fill_enabled: Boolean indicating if the fill should be drawn.
  • stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
  • fill_color: The color as tuple with which the fill should be drawn or None if no fill.
  • stroke_weight: The size of the stroke in pixels.
Returns:

Decorated Pillow image with the drawn rect.

def make_ellipse_image( min_x: float, min_y: float, width: float, height: float, stroke_enabled: bool, fill_enabled: bool, stroke_color: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], fill_color: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], stroke_weight: float) -> PillowUtilImage:
206def make_ellipse_image(min_x: float, min_y: float, width: float, height: float,
207    stroke_enabled: bool, fill_enabled: bool, stroke_color: COLOR_MAYBE, fill_color: COLOR_MAYBE,
208    stroke_weight: float) -> PillowUtilImage:
209    """Draw a ellipse using Pillow.
210
211    Args:
212        min_x: The left coordinate.
213        min_y: The top coordinate.
214        width: Width of the rect in pixels.
215        height: Height of the rect in pixels.
216        stroke_enabled: Boolean indicating if the stroke should be drawn.
217        fill_enabled: Boolean indicating if the fill should be drawn.
218        stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
219        fill_color: The color as tuple with which the fill should be drawn or None if no fill.
220        stroke_weight: The size of the stroke in pixels.
221
222    Returns:
223        Decorated Pillow image with the drawn ellipse.
224    """
225    if stroke_enabled:
226        stroke_weight_realized = stroke_weight
227    else:
228        stroke_weight_realized = 0
229
230    width_offset = width + math.floor(stroke_weight_realized / 2) * 2
231    height_offset = height + math.floor(stroke_weight_realized / 2) * 2
232
233    size = (round(width_offset) + 1, round(height_offset) + 1)
234    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
235    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
236
237    bounds = (
238        0,
239        0,
240        width_offset,
241        height_offset
242    )
243    target_surface.ellipse(
244        bounds,
245        fill=fill_color if fill_enabled else None,
246        outline=stroke_color if stroke_enabled else None,
247        width=stroke_weight_realized
248    )
249
250    return PillowUtilImage(
251        min_x - round(stroke_weight_realized / 2),
252        min_y - round(stroke_weight_realized / 2),
253        width_offset,
254        height_offset,
255        target_image
256    )

Draw a ellipse using Pillow.

Arguments:
  • min_x: The left coordinate.
  • min_y: The top coordinate.
  • width: Width of the rect in pixels.
  • height: Height of the rect in pixels.
  • stroke_enabled: Boolean indicating if the stroke should be drawn.
  • fill_enabled: Boolean indicating if the fill should be drawn.
  • stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
  • fill_color: The color as tuple with which the fill should be drawn or None if no fill.
  • stroke_weight: The size of the stroke in pixels.
Returns:

Decorated Pillow image with the drawn ellipse.

class SegmentSimplifier:
259class SegmentSimplifier:
260    """Utility to help draw shapes' segments in Pillow."""
261
262    def __init__(self, start_x: float, start_y: float):
263        """Create a new simplifier.
264
265        Args:
266            start_x: The starting x coordinate of the shape.
267            start_y: The starting y coordinate of the shape.
268        """
269        self._previous_x = start_x
270        self._previous_y = start_y
271
272    def simplify(self,
273        segment: sketchingpy.shape_struct.Line) -> typing.Iterable[typing.Iterable[float]]:
274        """Turn a segment into a series of coordinates.
275
276        Simplify a segment into a simple series of x, y coordinates which approximate the underlying
277        shape using a series of straight lines.
278
279        Args:
280            segment: The segment to simplify.
281
282        Returns:
283            Collection of x, y coordinates.
284        """
285        ret_vals: typing.Iterable[typing.Tuple[float, float]] = []
286
287        strategy = segment.get_strategy()
288        if strategy == 'straight':
289            ret_vals = ((segment.get_destination_x(), segment.get_destination_y()),)
290        elif strategy == 'bezier':
291            change_y = abs(segment.get_control_y2() - segment.get_control_y1())
292            change_x = abs(segment.get_control_x2() - segment.get_control_x1())
293
294            num_segs = (change_y**2 + change_x**2) ** 0.5 / 10
295            num_segs_int = int(num_segs)
296
297            bezier_maker = sketchingpy.bezier_util.BezierMaker()
298            bezier_maker.add_point(self._previous_x, self._previous_y)
299            bezier_maker.add_point(segment.get_control_x1(), segment.get_control_y1())
300            bezier_maker.add_point(segment.get_control_x2(), segment.get_control_y2())
301            bezier_maker.add_point(segment.get_destination_x(), segment.get_destination_y())
302
303            ret_vals = bezier_maker.get_points(num_segs_int)
304        else:
305            raise RuntimeError('Unknown segment strategy: ' + strategy)
306
307        self._previous_x = segment.get_destination_x()
308        self._previous_y = segment.get_destination_y()
309
310        return ret_vals

Utility to help draw shapes' segments in Pillow.

SegmentSimplifier(start_x: float, start_y: float)
262    def __init__(self, start_x: float, start_y: float):
263        """Create a new simplifier.
264
265        Args:
266            start_x: The starting x coordinate of the shape.
267            start_y: The starting y coordinate of the shape.
268        """
269        self._previous_x = start_x
270        self._previous_y = start_y

Create a new simplifier.

Arguments:
  • start_x: The starting x coordinate of the shape.
  • start_y: The starting y coordinate of the shape.
def simplify( self, segment: sketchingpy.shape_struct.Line) -> Iterable[Iterable[float]]:
272    def simplify(self,
273        segment: sketchingpy.shape_struct.Line) -> typing.Iterable[typing.Iterable[float]]:
274        """Turn a segment into a series of coordinates.
275
276        Simplify a segment into a simple series of x, y coordinates which approximate the underlying
277        shape using a series of straight lines.
278
279        Args:
280            segment: The segment to simplify.
281
282        Returns:
283            Collection of x, y coordinates.
284        """
285        ret_vals: typing.Iterable[typing.Tuple[float, float]] = []
286
287        strategy = segment.get_strategy()
288        if strategy == 'straight':
289            ret_vals = ((segment.get_destination_x(), segment.get_destination_y()),)
290        elif strategy == 'bezier':
291            change_y = abs(segment.get_control_y2() - segment.get_control_y1())
292            change_x = abs(segment.get_control_x2() - segment.get_control_x1())
293
294            num_segs = (change_y**2 + change_x**2) ** 0.5 / 10
295            num_segs_int = int(num_segs)
296
297            bezier_maker = sketchingpy.bezier_util.BezierMaker()
298            bezier_maker.add_point(self._previous_x, self._previous_y)
299            bezier_maker.add_point(segment.get_control_x1(), segment.get_control_y1())
300            bezier_maker.add_point(segment.get_control_x2(), segment.get_control_y2())
301            bezier_maker.add_point(segment.get_destination_x(), segment.get_destination_y())
302
303            ret_vals = bezier_maker.get_points(num_segs_int)
304        else:
305            raise RuntimeError('Unknown segment strategy: ' + strategy)
306
307        self._previous_x = segment.get_destination_x()
308        self._previous_y = segment.get_destination_y()
309
310        return ret_vals

Turn a segment into a series of coordinates.

Simplify a segment into a simple series of x, y coordinates which approximate the underlying shape using a series of straight lines.

Arguments:
  • segment: The segment to simplify.
Returns:

Collection of x, y coordinates.

def make_shape_image( shape: sketchingpy.shape_struct.Shape, stroke_enabled: bool, fill_enabled: bool, stroke_color: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], fill_color: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], stroke_weight: float) -> PillowUtilImage:
313def make_shape_image(shape: sketchingpy.shape_struct.Shape, stroke_enabled: bool,
314    fill_enabled: bool, stroke_color: COLOR_MAYBE, fill_color: COLOR_MAYBE,
315    stroke_weight: float) -> PillowUtilImage:
316    """Draw a Sketchingpy shape into a pillow image.
317
318    Args:
319        shape: The shape to be drawn.
320        stroke_enabled: Boolean indicating if the stroke should be drawn.
321        fill_enabled: Boolean indicating if the fill should be drawn.
322        stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
323        fill_color: The color as tuple with which the fill should be drawn or None if no fill.
324        stroke_weight: The size of the stroke in pixels.
325
326    Returns:
327        Decorated Pillow image with the drawn shape.
328    """
329
330    if not shape.get_is_finished():
331        raise RuntimeError('Finish shape before drawing.')
332
333    min_x = shape.get_min_x()
334    max_x = shape.get_max_x()
335    min_y = shape.get_min_y()
336    max_y = shape.get_max_y()
337
338    width = max_x - min_x
339    height = max_y - min_y
340    width_offset = width + stroke_weight * 2
341    height_offset = height + stroke_weight * 2
342
343    size = (round(width_offset) + 1, round(height_offset) + 1)
344    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
345    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
346
347    def adjust_coord(coord):
348        return (
349            coord[0] - min_x + stroke_weight,
350            coord[1] - min_y + stroke_weight
351        )
352
353    start_x = shape.get_start_x()
354    start_y = shape.get_start_y()
355    start_coords = [(start_x, start_y)]
356
357    simplified_segements = []
358    simplifier = SegmentSimplifier(start_x, start_y)
359    for segment in shape.get_segments():
360        simplified_segements.append(simplifier.simplify(segment))
361
362    later_coords = itertools.chain(*simplified_segements)
363    all_coords = itertools.chain(start_coords, later_coords)
364    coords = [adjust_coord(x) for x in all_coords]
365
366    if shape.get_is_closed():
367        target_surface.polygon(coords, fill=fill_color, outline=stroke_color, width=stroke_weight)
368    else:
369        target_surface.line(coords, fill=stroke_color, width=stroke_weight, joint='curve')
370
371    return PillowUtilImage(
372        min_x - stroke_weight,
373        min_y - stroke_weight,
374        width_offset,
375        height_offset,
376        target_image
377    )

Draw a Sketchingpy shape into a pillow image.

Arguments:
  • shape: The shape to be drawn.
  • stroke_enabled: Boolean indicating if the stroke should be drawn.
  • fill_enabled: Boolean indicating if the fill should be drawn.
  • stroke_color: The color as tuple with which the stroke should be drawn or None if no stroke.
  • fill_color: The color as tuple with which the fill should be drawn or None if no fill.
  • stroke_weight: The size of the stroke in pixels.
Returns:

Decorated Pillow image with the drawn shape.

def make_text_image( x: float, y: float, content: str, font: PIL.ImageFont.ImageFont, stroke_enabled: bool, fill_enabled: bool, stroke: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], fill: Union[Tuple[int, int, int], Tuple[int, int, int, int], NoneType], stroke_weight: float, anchor: str):
380def make_text_image(x: float, y: float, content: str, font: PIL.ImageFont.ImageFont,
381    stroke_enabled: bool, fill_enabled: bool, stroke: COLOR_MAYBE, fill: COLOR_MAYBE,
382    stroke_weight: float, anchor: str):
383    """Draw text into a pillow image.
384
385    Args:
386        x: The x coordinate of the anchor.
387        y: The y coordinate of the anchor.
388        font: The font (PIL native) to use in drawing the text.
389        stroke_enabled: Boolean indicating if the stroke should be drawn.
390        fill_enabled: Boolean indicating if the fill should be drawn.
391        stroke: The color as tuple with which the stroke should be drawn or None if no stroke.
392        fill: The color as tuple with which the fill should be drawn or None if no fill.
393        stroke_weight: The size of the stroke in pixels.
394        anchor: Anchor string describing vertical and horizontal alignment.
395
396    Returns:
397        Decorated Pillow image with the drawn text.
398    """
399
400    temp_image = PIL.Image.new('RGBA', (1, 1), (255, 255, 255, 0))
401    temp_surface = PIL.ImageDraw.Draw(temp_image, 'RGBA')
402    stroke_weight_int = round(stroke_weight)
403    bounding_box = temp_surface.textbbox(
404        (stroke_weight_int, stroke_weight_int),
405        content,
406        font=font,
407        anchor=anchor,
408        stroke_width=stroke_weight_int
409    )
410
411    start_x = bounding_box[0]
412    end_x = bounding_box[2]
413
414    start_y = bounding_box[1]
415    end_y = bounding_box[3]
416
417    width = end_x - start_x
418    height = end_y - start_y
419
420    width_offset = width + stroke_weight * 2
421    height_offset = height + stroke_weight * 2
422
423    size = (round(width_offset) + 2, round(height_offset) + 1)
424    target_image = PIL.Image.new('RGBA', size, (255, 255, 255, 0))
425    target_surface = PIL.ImageDraw.Draw(target_image, 'RGBA')
426
427    if stroke_enabled:
428        target_surface.text(
429            (
430                round(-1 * start_x + stroke_weight + 1),
431                round(-1 * start_y + stroke_weight)
432            ),
433            content,
434            font=font,
435            anchor=anchor,
436            stroke_width=round(stroke_weight),
437            stroke_fill=stroke,
438            fill=(0, 0, 0, 0)
439        )
440
441    if fill_enabled:
442        target_surface.text(
443            (
444                -1 * start_x + stroke_weight + 1,
445                -1 * start_y + stroke_weight
446            ),
447            content,
448            font=font,
449            anchor=anchor,
450            fill=fill
451        )
452
453    return PillowUtilImage(
454        start_x - stroke_weight + x,
455        start_y - stroke_weight + y,
456        width_offset,
457        height_offset,
458        target_image
459    )

Draw text into a pillow image.

Arguments:
  • x: The x coordinate of the anchor.
  • y: The y coordinate of the anchor.
  • font: The font (PIL native) to use in drawing the text.
  • stroke_enabled: Boolean indicating if the stroke should be drawn.
  • fill_enabled: Boolean indicating if the fill should be drawn.
  • stroke: The color as tuple with which the stroke should be drawn or None if no stroke.
  • fill: The color as tuple with which the fill should be drawn or None if no fill.
  • stroke_weight: The size of the stroke in pixels.
  • anchor: Anchor string describing vertical and horizontal alignment.
Returns:

Decorated Pillow image with the drawn text.