sketchingpy.pillow_struct

Structures to support Pillow-based operations.

License:

BSD

  1"""Structures to support Pillow-based operations.
  2
  3License:
  4    BSD
  5"""
  6
  7import math
  8import typing
  9
 10import PIL.Image
 11import PIL.ImageDraw
 12
 13import sketchingpy.abstracted
 14import sketchingpy.const
 15import sketchingpy.state_struct
 16import sketchingpy.transform
 17
 18COLOR_TUPLE = typing.Union[typing.Tuple[int, int, int], typing.Tuple[int, int, int, int]]
 19
 20
 21class Rect:
 22    """Simple structure describing a region in a sketch."""
 23
 24    def __init__(self, x: float, y: float, width: float, height: float):
 25        """Create a new region.
 26
 27        Args:
 28            x: The x coordinate for the left side of the rectangle.
 29            y: The y coordinate for the top of the rectangle.
 30            width: Horizontal size of the rectangle in pixels.
 31            height: Vertical size of the rectangle in pixels.
 32        """
 33        self._x = x
 34        self._y = y
 35        self._width = width
 36        self._height = height
 37
 38    def get_x(self) -> float:
 39        """Get the starting x coordinate of this region.
 40
 41        Returns:
 42            The x coordinate for the left side of the rectangle.
 43        """
 44        return self._x
 45
 46    def get_y(self) -> float:
 47        """Get the starting y coordinate of this region.
 48
 49        Returns:
 50            The y coordinate for the top of the rectangle.
 51        """
 52        return self._y
 53
 54    def set_x(self, x: float):
 55        """"Set the starting x coordinate of this region.
 56
 57        Args:
 58            x: The x coordinate for the left side of the rectangle.
 59        """
 60        self._x = x
 61
 62    def set_y(self, y: float):
 63        """Set the starting y coordinate of this region.
 64
 65        Args:
 66            y: The y coordinate for the top of the rectangle.
 67        """
 68        self._y = y
 69
 70    def get_width(self) -> float:
 71        """Get the width of this region.
 72
 73        Returns:
 74            Horizontal size of the rectangle in pixels.
 75        """
 76        return self._width
 77
 78    def get_height(self) -> float:
 79        """Get the height of this region.
 80
 81        Returns;
 82            Vertical size of the rectangle in pixels.
 83        """
 84        return self._height
 85
 86    def get_center_x(self) -> float:
 87        """Get the middle x coordinate of this region.
 88
 89        Returns:
 90            Center horizontal coordinate of this region.
 91        """
 92        return self.get_x() + self.get_width() / 2
 93
 94    def get_center_y(self) -> float:
 95        """Get the middle y coordinate of this region.
 96
 97        Returns:
 98            Center vertical coordinate of this region.
 99        """
100        return self.get_y() + self.get_height() / 2
101
102    def set_center_x(self, x: float):
103        """Move this region by setting its center horizontal coordinate.
104
105        Args:
106            x: The x coordinate that should be the new center of the region.
107        """
108        new_x = x - self.get_width() / 2
109        self.set_x(new_x)
110
111    def set_center_y(self, y: float):
112        """Move this region by setting its center vertical coordinate.
113
114        Args:
115            y: The y coordinate that should be the new center of the region.
116        """
117        new_y = y - self.get_height() / 2
118        self.set_y(new_y)
119
120
121class WritableImage:
122    """Decorator around a Pillow image which can be written to."""
123
124    def __init__(self, image: PIL.Image.Image, drawable: PIL.ImageDraw.ImageDraw):
125        """Create a new writable image record.
126
127        Args:
128            image: The Pillow image that isn't writable.
129            drawable: The version of image which can be written to.
130        """
131        self._image = image
132        self._drawable = drawable
133
134    def get_image(self) -> PIL.Image.Image:
135        """Get the Pillow image.
136
137        Returns:
138            The Pillow image that isn't writable.
139        """
140        return self._image
141
142    def get_drawable(self) -> PIL.ImageDraw.ImageDraw:
143        """Get the version of the image which can be written to.
144
145        Returns:
146            The version of image which can be written to.
147        """
148        return self._drawable
149
150
151class TransformedDrawable:
152    """Interface for a transformed drawable component after transformation."""
153
154    def get_with_offset(self, x: float, y: float) -> 'TransformedDrawable':
155        """Get a new version of this same object but with a horizontal and vertical offset.
156
157        Args:
158            x: The horizontal offset in pixels.
159            y: The vertical offset in pixels.
160
161        Returns:
162            A copy of this drawable component but with a positional translation applied.
163        """
164        raise NotImplementedError('Use implementor.')
165
166    def transform(self, transformer: sketchingpy.transform.Transformer) -> 'TransformedDrawable':
167        """Get a new version of this same object but with a transformation applied.
168
169        Args:
170            transformer: Transformation matrix to apply.
171
172        Returns:
173            A copy of this drawable component but with a transformation applied.
174        """
175        raise NotImplementedError('Use implementor.')
176
177    def draw(self, target: WritableImage):
178        """Draw this component.
179
180        Args:
181            target: The image on which to draw this component.
182        """
183        raise NotImplementedError('Use implementor.')
184
185
186class TransformedWritable(TransformedDrawable):
187    """A writable image after transformation."""
188
189    def __init__(self, writable: WritableImage, native_x: float, native_y: float):
190        """Create a new record of a writable which is pre-transformed.
191
192        Args:
193            writable: The writable after transformation.
194            native_x: The horizontal position of where the image should be drawn.
195            native_y: The vertical position of where the image should be drawn.
196        """
197        self._writable = writable
198        self._native_x = native_x
199        self._native_y = native_y
200
201    def get_writable(self) -> WritableImage:
202        """Get the writable image.
203
204        Returns:
205            The image which has been pre-transformed.
206        """
207        return self._writable
208
209    def get_x(self) -> float:
210        """Get the intended x coordinate where this image should be drawn.
211
212        Returns:
213            The horizontal position of where the image should be drawn.
214        """
215        return self._native_x
216
217    def get_y(self) -> float:
218        """Get the intended y coordinate where this image should be drawn.
219
220        Returns:
221            The vertical position of where the image should be drawn.
222        """
223        return self._native_y
224
225    def get_with_offset(self, x: float, y: float) -> TransformedDrawable:
226        return TransformedWritable(self._writable, self._native_x + x, self._native_y + y)
227
228    def transform(self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
229        return get_transformed(
230            transformer,
231            self._writable.get_image(),
232            self._native_x,
233            self._native_y
234        )
235
236    def draw(self, target: WritableImage):
237        subject = self._writable.get_image()
238        native_pos = (int(self._native_x), int(self._native_y))
239        if subject.mode == 'RGB':
240            target.get_image().paste(subject, native_pos)
241        else:
242            target.get_image().paste(subject, native_pos, subject)
243
244
245class TransformedLine(TransformedDrawable):
246    """A pre-transformed simple two point line."""
247
248    def __init__(self, x1: float, y1: float, x2: float, y2: float, stroke: COLOR_TUPLE,
249        weight: float):
250        """Create a new record of a pre-transformed line.
251
252        Args:
253            x1: The first x coordinate.
254            y1: The first y coordinate.
255            x2: The second x coordinate.
256            y2: The second y coordinate.
257            stroke: The color with which to draw this line.
258            weight: The stroke weight to use when drawing.
259        """
260        self._x1 = x1
261        self._y1 = y1
262        self._x2 = x2
263        self._y2 = y2
264        self._stroke = stroke
265        self._weight = weight
266
267    def get_with_offset(self, x: float, y: float) -> TransformedDrawable:
268        return TransformedLine(
269            self._x1 + x,
270            self._y1 + y,
271            self._x2 + x,
272            self._y2 + y,
273            self._stroke,
274            self._weight
275        )
276
277    def transform(self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
278        point_1 = transformer.transform(self._x1, self._y1)
279        point_2 = transformer.transform(self._x2, self._y2)
280        weight = self._weight * point_1.get_scale()
281        return TransformedLine(
282            point_1.get_x(),
283            point_1.get_y(),
284            point_2.get_x(),
285            point_2.get_y(),
286            self._stroke,
287            weight
288        )
289
290    def draw(self, target: WritableImage):
291        target.get_drawable().line(
292            (
293                (self._x1, self._y1),
294                (self._x2, self._y2)
295            ),
296            fill=self._stroke,
297            width=self._weight
298        )
299
300
301class TransformedClear(TransformedDrawable):
302    """A pre-transformed clear operation."""
303
304    def __init__(self, color: COLOR_TUPLE):
305        """Create a record of a clear operation.
306
307        Args:
308            color: The color with which to clear.
309        """
310        self._color = color
311
312    def get_with_offset(self, x: float, y: float) -> TransformedDrawable:
313        return self
314
315    def transform(self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
316        return self
317
318    def draw(self, target: WritableImage):
319        image = target.get_image()
320        size = image.size
321        rect = (0, 0, size[0], size[1])
322        target.get_drawable().rectangle(rect, fill=self._color, width=0)
323
324
325def build_rect_with_mode(x1: float, y1: float, x2: float, y2: float,
326    native_mode: int) -> Rect:
327    """Build a rect with a mode of coordinate specification.
328
329    Args:
330        x1: The left or center x depending on mode.
331        y1: The top or center y depending on mode.
332        x2: The right or type of width depending on mode.
333        y2: The bottom or type of height depending on mode.
334        native_mode: The mode with which the coordinates were provided.
335
336    Returns:
337        Rect which interprets the given coordinates.
338    """
339    if native_mode == sketchingpy.const.CENTER:
340        start_x = x1 - math.floor(x2 / 2)
341        start_y = y1 - math.floor(y2 / 2)
342        width = x2
343        height = y2
344    elif native_mode == sketchingpy.const.RADIUS:
345        start_x = x1 - x2
346        start_y = y1 - y2
347        width = x2 * 2
348        height = y2 * 2
349    elif native_mode == sketchingpy.const.CORNER:
350        start_x = x1
351        start_y = y1
352        width = x2
353        height = y2
354    elif native_mode == sketchingpy.const.CORNERS:
355        (x1, y1, x2, y2) = sketchingpy.abstracted.reorder_coords(x1, y1, x2, y2)
356        start_x = x1
357        start_y = y1
358        width = x2 - x1
359        height = y2 - y1
360    else:
361        raise RuntimeError('Unknown mode: ' + str(native_mode))
362
363    return Rect(start_x, start_y, width, height)
364
365
366class Macro:
367    """Buffer-like object which keeps track of operations instead of the resulting raster."""
368
369    def __init__(self, width: float, height: float):
370        """Build a new macro record.
371
372        Args:
373            width: The horizontal size in pixels of the inteded area of drawing.
374            height: The vertical size in pixels of the inteded area of drawing.
375        """
376        self._width = width
377        self._height = height
378        self._elements: typing.List[TransformedDrawable] = []
379
380    def append(self, target: TransformedDrawable):
381        """Add a new drawable to this macro.
382
383        Args:
384            target: New element to add to this macro.
385        """
386        self._elements.append(target)
387
388    def get(self) -> typing.List[TransformedDrawable]:
389        """Get the elements in this macro.
390
391        Returns:
392            Operations for this macro.
393        """
394        return self._elements
395
396    def get_width(self) -> float:
397        """Get the width of the intended drawing area for this macro.
398
399        Returns:
400            Horizontal size of drawing area.
401        """
402        return self._width
403
404    def get_height(self) -> float:
405        """Get the vertical of the intended drawing area for this macro.
406
407        Returns:
408            Vertical size of drawing area.
409        """
410        return self._height
411
412
413def zero_rect(rect: Rect) -> Rect:
414    """Make a copy of a given rect but where the x and y coordinates are set to zero.
415
416    Args:
417        rect: The rect to put at 0, 0.
418
419    Returns:
420        Copy of the input rect set at 0, 0.
421    """
422    return Rect(0, 0, rect.get_width(), rect.get_height())
423
424
425def get_transformed(transformer: sketchingpy.transform.Transformer, surface: PIL.Image.Image,
426    x: float, y: float) -> TransformedWritable:
427    """Convert an image to a pre-transformed writable.
428
429    Args:
430        transformer: The transformation to pre-apply.
431        surface: The image on which to apply the transformation.
432        x: The intended horizontal draw location of the given position within the given
433            transformation.
434        y: The intended vertical draw location of the given position within the given
435            transformation.
436
437    Returns:
438        Writable with the given transformation pre-applied.
439    """
440    start_rect = Rect(x, y, surface.width, surface.height)
441
442    transformed_center = transformer.transform(
443        start_rect.get_center_x(),
444        start_rect.get_center_y()
445    )
446
447    has_scale = transformed_center.get_scale() != 1
448    has_rotation = transformed_center.get_rotation() != 0
449    has_content_transform = has_scale or has_rotation
450    if has_content_transform:
451        angle = transformed_center.get_rotation()
452        angle_transform = math.degrees(angle)
453        scale = transformed_center.get_scale()
454        surface = surface.rotate(angle_transform, expand=True)
455        surface = surface.resize((
456            int(surface.width * scale),
457            int(surface.height * scale)
458        ))
459
460    end_rect = Rect(x, y, surface.width, surface.height)
461    end_rect.set_center_x(transformed_center.get_x())
462    end_rect.set_center_y(transformed_center.get_y())
463
464    return TransformedWritable(
465        WritableImage(surface, PIL.ImageDraw.Draw(surface)),
466        end_rect.get_x(),
467        end_rect.get_y()
468    )
469
470
471def get_retransformed(transformer: sketchingpy.transform.Transformer,
472    target: TransformedWritable) -> TransformedWritable:
473    """Convert a transformed writable to a further pre-transformed writable.
474
475    Args:
476        transformer: The transformation to pre-apply.
477        target: The transformed writable to re-transform.
478
479    Returns:
480        Writable with the given transformation pre-applied.
481    """
482    return target.transform(transformer)  # type: ignore
COLOR_TUPLE = typing.Union[typing.Tuple[int, int, int], typing.Tuple[int, int, int, int]]
class Rect:
 22class Rect:
 23    """Simple structure describing a region in a sketch."""
 24
 25    def __init__(self, x: float, y: float, width: float, height: float):
 26        """Create a new region.
 27
 28        Args:
 29            x: The x coordinate for the left side of the rectangle.
 30            y: The y coordinate for the top of the rectangle.
 31            width: Horizontal size of the rectangle in pixels.
 32            height: Vertical size of the rectangle in pixels.
 33        """
 34        self._x = x
 35        self._y = y
 36        self._width = width
 37        self._height = height
 38
 39    def get_x(self) -> float:
 40        """Get the starting x coordinate of this region.
 41
 42        Returns:
 43            The x coordinate for the left side of the rectangle.
 44        """
 45        return self._x
 46
 47    def get_y(self) -> float:
 48        """Get the starting y coordinate of this region.
 49
 50        Returns:
 51            The y coordinate for the top of the rectangle.
 52        """
 53        return self._y
 54
 55    def set_x(self, x: float):
 56        """"Set the starting x coordinate of this region.
 57
 58        Args:
 59            x: The x coordinate for the left side of the rectangle.
 60        """
 61        self._x = x
 62
 63    def set_y(self, y: float):
 64        """Set the starting y coordinate of this region.
 65
 66        Args:
 67            y: The y coordinate for the top of the rectangle.
 68        """
 69        self._y = y
 70
 71    def get_width(self) -> float:
 72        """Get the width of this region.
 73
 74        Returns:
 75            Horizontal size of the rectangle in pixels.
 76        """
 77        return self._width
 78
 79    def get_height(self) -> float:
 80        """Get the height of this region.
 81
 82        Returns;
 83            Vertical size of the rectangle in pixels.
 84        """
 85        return self._height
 86
 87    def get_center_x(self) -> float:
 88        """Get the middle x coordinate of this region.
 89
 90        Returns:
 91            Center horizontal coordinate of this region.
 92        """
 93        return self.get_x() + self.get_width() / 2
 94
 95    def get_center_y(self) -> float:
 96        """Get the middle y coordinate of this region.
 97
 98        Returns:
 99            Center vertical coordinate of this region.
100        """
101        return self.get_y() + self.get_height() / 2
102
103    def set_center_x(self, x: float):
104        """Move this region by setting its center horizontal coordinate.
105
106        Args:
107            x: The x coordinate that should be the new center of the region.
108        """
109        new_x = x - self.get_width() / 2
110        self.set_x(new_x)
111
112    def set_center_y(self, y: float):
113        """Move this region by setting its center vertical coordinate.
114
115        Args:
116            y: The y coordinate that should be the new center of the region.
117        """
118        new_y = y - self.get_height() / 2
119        self.set_y(new_y)

Simple structure describing a region in a sketch.

Rect(x: float, y: float, width: float, height: float)
25    def __init__(self, x: float, y: float, width: float, height: float):
26        """Create a new region.
27
28        Args:
29            x: The x coordinate for the left side of the rectangle.
30            y: The y coordinate for the top of the rectangle.
31            width: Horizontal size of the rectangle in pixels.
32            height: Vertical size of the rectangle in pixels.
33        """
34        self._x = x
35        self._y = y
36        self._width = width
37        self._height = height

Create a new region.

Arguments:
  • x: The x coordinate for the left side of the rectangle.
  • y: The y coordinate for the top of the rectangle.
  • width: Horizontal size of the rectangle in pixels.
  • height: Vertical size of the rectangle in pixels.
def get_x(self) -> float:
39    def get_x(self) -> float:
40        """Get the starting x coordinate of this region.
41
42        Returns:
43            The x coordinate for the left side of the rectangle.
44        """
45        return self._x

Get the starting x coordinate of this region.

Returns:

The x coordinate for the left side of the rectangle.

def get_y(self) -> float:
47    def get_y(self) -> float:
48        """Get the starting y coordinate of this region.
49
50        Returns:
51            The y coordinate for the top of the rectangle.
52        """
53        return self._y

Get the starting y coordinate of this region.

Returns:

The y coordinate for the top of the rectangle.

def set_x(self, x: float):
55    def set_x(self, x: float):
56        """"Set the starting x coordinate of this region.
57
58        Args:
59            x: The x coordinate for the left side of the rectangle.
60        """
61        self._x = x

"Set the starting x coordinate of this region.

Arguments:
  • x: The x coordinate for the left side of the rectangle.
def set_y(self, y: float):
63    def set_y(self, y: float):
64        """Set the starting y coordinate of this region.
65
66        Args:
67            y: The y coordinate for the top of the rectangle.
68        """
69        self._y = y

Set the starting y coordinate of this region.

Arguments:
  • y: The y coordinate for the top of the rectangle.
def get_width(self) -> float:
71    def get_width(self) -> float:
72        """Get the width of this region.
73
74        Returns:
75            Horizontal size of the rectangle in pixels.
76        """
77        return self._width

Get the width of this region.

Returns:

Horizontal size of the rectangle in pixels.

def get_height(self) -> float:
79    def get_height(self) -> float:
80        """Get the height of this region.
81
82        Returns;
83            Vertical size of the rectangle in pixels.
84        """
85        return self._height

Get the height of this region.

Returns; Vertical size of the rectangle in pixels.

def get_center_x(self) -> float:
87    def get_center_x(self) -> float:
88        """Get the middle x coordinate of this region.
89
90        Returns:
91            Center horizontal coordinate of this region.
92        """
93        return self.get_x() + self.get_width() / 2

Get the middle x coordinate of this region.

Returns:

Center horizontal coordinate of this region.

def get_center_y(self) -> float:
 95    def get_center_y(self) -> float:
 96        """Get the middle y coordinate of this region.
 97
 98        Returns:
 99            Center vertical coordinate of this region.
100        """
101        return self.get_y() + self.get_height() / 2

Get the middle y coordinate of this region.

Returns:

Center vertical coordinate of this region.

def set_center_x(self, x: float):
103    def set_center_x(self, x: float):
104        """Move this region by setting its center horizontal coordinate.
105
106        Args:
107            x: The x coordinate that should be the new center of the region.
108        """
109        new_x = x - self.get_width() / 2
110        self.set_x(new_x)

Move this region by setting its center horizontal coordinate.

Arguments:
  • x: The x coordinate that should be the new center of the region.
def set_center_y(self, y: float):
112    def set_center_y(self, y: float):
113        """Move this region by setting its center vertical coordinate.
114
115        Args:
116            y: The y coordinate that should be the new center of the region.
117        """
118        new_y = y - self.get_height() / 2
119        self.set_y(new_y)

Move this region by setting its center vertical coordinate.

Arguments:
  • y: The y coordinate that should be the new center of the region.
class WritableImage:
122class WritableImage:
123    """Decorator around a Pillow image which can be written to."""
124
125    def __init__(self, image: PIL.Image.Image, drawable: PIL.ImageDraw.ImageDraw):
126        """Create a new writable image record.
127
128        Args:
129            image: The Pillow image that isn't writable.
130            drawable: The version of image which can be written to.
131        """
132        self._image = image
133        self._drawable = drawable
134
135    def get_image(self) -> PIL.Image.Image:
136        """Get the Pillow image.
137
138        Returns:
139            The Pillow image that isn't writable.
140        """
141        return self._image
142
143    def get_drawable(self) -> PIL.ImageDraw.ImageDraw:
144        """Get the version of the image which can be written to.
145
146        Returns:
147            The version of image which can be written to.
148        """
149        return self._drawable

Decorator around a Pillow image which can be written to.

WritableImage(image: PIL.Image.Image, drawable: PIL.ImageDraw.ImageDraw)
125    def __init__(self, image: PIL.Image.Image, drawable: PIL.ImageDraw.ImageDraw):
126        """Create a new writable image record.
127
128        Args:
129            image: The Pillow image that isn't writable.
130            drawable: The version of image which can be written to.
131        """
132        self._image = image
133        self._drawable = drawable

Create a new writable image record.

Arguments:
  • image: The Pillow image that isn't writable.
  • drawable: The version of image which can be written to.
def get_image(self) -> PIL.Image.Image:
135    def get_image(self) -> PIL.Image.Image:
136        """Get the Pillow image.
137
138        Returns:
139            The Pillow image that isn't writable.
140        """
141        return self._image

Get the Pillow image.

Returns:

The Pillow image that isn't writable.

def get_drawable(self) -> PIL.ImageDraw.ImageDraw:
143    def get_drawable(self) -> PIL.ImageDraw.ImageDraw:
144        """Get the version of the image which can be written to.
145
146        Returns:
147            The version of image which can be written to.
148        """
149        return self._drawable

Get the version of the image which can be written to.

Returns:

The version of image which can be written to.

class TransformedDrawable:
152class TransformedDrawable:
153    """Interface for a transformed drawable component after transformation."""
154
155    def get_with_offset(self, x: float, y: float) -> 'TransformedDrawable':
156        """Get a new version of this same object but with a horizontal and vertical offset.
157
158        Args:
159            x: The horizontal offset in pixels.
160            y: The vertical offset in pixels.
161
162        Returns:
163            A copy of this drawable component but with a positional translation applied.
164        """
165        raise NotImplementedError('Use implementor.')
166
167    def transform(self, transformer: sketchingpy.transform.Transformer) -> 'TransformedDrawable':
168        """Get a new version of this same object but with a transformation applied.
169
170        Args:
171            transformer: Transformation matrix to apply.
172
173        Returns:
174            A copy of this drawable component but with a transformation applied.
175        """
176        raise NotImplementedError('Use implementor.')
177
178    def draw(self, target: WritableImage):
179        """Draw this component.
180
181        Args:
182            target: The image on which to draw this component.
183        """
184        raise NotImplementedError('Use implementor.')

Interface for a transformed drawable component after transformation.

def get_with_offset( self, x: float, y: float) -> TransformedDrawable:
155    def get_with_offset(self, x: float, y: float) -> 'TransformedDrawable':
156        """Get a new version of this same object but with a horizontal and vertical offset.
157
158        Args:
159            x: The horizontal offset in pixels.
160            y: The vertical offset in pixels.
161
162        Returns:
163            A copy of this drawable component but with a positional translation applied.
164        """
165        raise NotImplementedError('Use implementor.')

Get a new version of this same object but with a horizontal and vertical offset.

Arguments:
  • x: The horizontal offset in pixels.
  • y: The vertical offset in pixels.
Returns:

A copy of this drawable component but with a positional translation applied.

def transform( self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
167    def transform(self, transformer: sketchingpy.transform.Transformer) -> 'TransformedDrawable':
168        """Get a new version of this same object but with a transformation applied.
169
170        Args:
171            transformer: Transformation matrix to apply.
172
173        Returns:
174            A copy of this drawable component but with a transformation applied.
175        """
176        raise NotImplementedError('Use implementor.')

Get a new version of this same object but with a transformation applied.

Arguments:
  • transformer: Transformation matrix to apply.
Returns:

A copy of this drawable component but with a transformation applied.

def draw(self, target: WritableImage):
178    def draw(self, target: WritableImage):
179        """Draw this component.
180
181        Args:
182            target: The image on which to draw this component.
183        """
184        raise NotImplementedError('Use implementor.')

Draw this component.

Arguments:
  • target: The image on which to draw this component.
class TransformedWritable(TransformedDrawable):
187class TransformedWritable(TransformedDrawable):
188    """A writable image after transformation."""
189
190    def __init__(self, writable: WritableImage, native_x: float, native_y: float):
191        """Create a new record of a writable which is pre-transformed.
192
193        Args:
194            writable: The writable after transformation.
195            native_x: The horizontal position of where the image should be drawn.
196            native_y: The vertical position of where the image should be drawn.
197        """
198        self._writable = writable
199        self._native_x = native_x
200        self._native_y = native_y
201
202    def get_writable(self) -> WritableImage:
203        """Get the writable image.
204
205        Returns:
206            The image which has been pre-transformed.
207        """
208        return self._writable
209
210    def get_x(self) -> float:
211        """Get the intended x coordinate where this image should be drawn.
212
213        Returns:
214            The horizontal position of where the image should be drawn.
215        """
216        return self._native_x
217
218    def get_y(self) -> float:
219        """Get the intended y coordinate where this image should be drawn.
220
221        Returns:
222            The vertical position of where the image should be drawn.
223        """
224        return self._native_y
225
226    def get_with_offset(self, x: float, y: float) -> TransformedDrawable:
227        return TransformedWritable(self._writable, self._native_x + x, self._native_y + y)
228
229    def transform(self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
230        return get_transformed(
231            transformer,
232            self._writable.get_image(),
233            self._native_x,
234            self._native_y
235        )
236
237    def draw(self, target: WritableImage):
238        subject = self._writable.get_image()
239        native_pos = (int(self._native_x), int(self._native_y))
240        if subject.mode == 'RGB':
241            target.get_image().paste(subject, native_pos)
242        else:
243            target.get_image().paste(subject, native_pos, subject)

A writable image after transformation.

TransformedWritable( writable: WritableImage, native_x: float, native_y: float)
190    def __init__(self, writable: WritableImage, native_x: float, native_y: float):
191        """Create a new record of a writable which is pre-transformed.
192
193        Args:
194            writable: The writable after transformation.
195            native_x: The horizontal position of where the image should be drawn.
196            native_y: The vertical position of where the image should be drawn.
197        """
198        self._writable = writable
199        self._native_x = native_x
200        self._native_y = native_y

Create a new record of a writable which is pre-transformed.

Arguments:
  • writable: The writable after transformation.
  • native_x: The horizontal position of where the image should be drawn.
  • native_y: The vertical position of where the image should be drawn.
def get_writable(self) -> WritableImage:
202    def get_writable(self) -> WritableImage:
203        """Get the writable image.
204
205        Returns:
206            The image which has been pre-transformed.
207        """
208        return self._writable

Get the writable image.

Returns:

The image which has been pre-transformed.

def get_x(self) -> float:
210    def get_x(self) -> float:
211        """Get the intended x coordinate where this image should be drawn.
212
213        Returns:
214            The horizontal position of where the image should be drawn.
215        """
216        return self._native_x

Get the intended x coordinate where this image should be drawn.

Returns:

The horizontal position of where the image should be drawn.

def get_y(self) -> float:
218    def get_y(self) -> float:
219        """Get the intended y coordinate where this image should be drawn.
220
221        Returns:
222            The vertical position of where the image should be drawn.
223        """
224        return self._native_y

Get the intended y coordinate where this image should be drawn.

Returns:

The vertical position of where the image should be drawn.

def get_with_offset( self, x: float, y: float) -> TransformedDrawable:
226    def get_with_offset(self, x: float, y: float) -> TransformedDrawable:
227        return TransformedWritable(self._writable, self._native_x + x, self._native_y + y)

Get a new version of this same object but with a horizontal and vertical offset.

Arguments:
  • x: The horizontal offset in pixels.
  • y: The vertical offset in pixels.
Returns:

A copy of this drawable component but with a positional translation applied.

def transform( self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
229    def transform(self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
230        return get_transformed(
231            transformer,
232            self._writable.get_image(),
233            self._native_x,
234            self._native_y
235        )

Get a new version of this same object but with a transformation applied.

Arguments:
  • transformer: Transformation matrix to apply.
Returns:

A copy of this drawable component but with a transformation applied.

def draw(self, target: WritableImage):
237    def draw(self, target: WritableImage):
238        subject = self._writable.get_image()
239        native_pos = (int(self._native_x), int(self._native_y))
240        if subject.mode == 'RGB':
241            target.get_image().paste(subject, native_pos)
242        else:
243            target.get_image().paste(subject, native_pos, subject)

Draw this component.

Arguments:
  • target: The image on which to draw this component.
class TransformedLine(TransformedDrawable):
246class TransformedLine(TransformedDrawable):
247    """A pre-transformed simple two point line."""
248
249    def __init__(self, x1: float, y1: float, x2: float, y2: float, stroke: COLOR_TUPLE,
250        weight: float):
251        """Create a new record of a pre-transformed line.
252
253        Args:
254            x1: The first x coordinate.
255            y1: The first y coordinate.
256            x2: The second x coordinate.
257            y2: The second y coordinate.
258            stroke: The color with which to draw this line.
259            weight: The stroke weight to use when drawing.
260        """
261        self._x1 = x1
262        self._y1 = y1
263        self._x2 = x2
264        self._y2 = y2
265        self._stroke = stroke
266        self._weight = weight
267
268    def get_with_offset(self, x: float, y: float) -> TransformedDrawable:
269        return TransformedLine(
270            self._x1 + x,
271            self._y1 + y,
272            self._x2 + x,
273            self._y2 + y,
274            self._stroke,
275            self._weight
276        )
277
278    def transform(self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
279        point_1 = transformer.transform(self._x1, self._y1)
280        point_2 = transformer.transform(self._x2, self._y2)
281        weight = self._weight * point_1.get_scale()
282        return TransformedLine(
283            point_1.get_x(),
284            point_1.get_y(),
285            point_2.get_x(),
286            point_2.get_y(),
287            self._stroke,
288            weight
289        )
290
291    def draw(self, target: WritableImage):
292        target.get_drawable().line(
293            (
294                (self._x1, self._y1),
295                (self._x2, self._y2)
296            ),
297            fill=self._stroke,
298            width=self._weight
299        )

A pre-transformed simple two point line.

TransformedLine( x1: float, y1: float, x2: float, y2: float, stroke: Union[Tuple[int, int, int], Tuple[int, int, int, int]], weight: float)
249    def __init__(self, x1: float, y1: float, x2: float, y2: float, stroke: COLOR_TUPLE,
250        weight: float):
251        """Create a new record of a pre-transformed line.
252
253        Args:
254            x1: The first x coordinate.
255            y1: The first y coordinate.
256            x2: The second x coordinate.
257            y2: The second y coordinate.
258            stroke: The color with which to draw this line.
259            weight: The stroke weight to use when drawing.
260        """
261        self._x1 = x1
262        self._y1 = y1
263        self._x2 = x2
264        self._y2 = y2
265        self._stroke = stroke
266        self._weight = weight

Create a new record of a pre-transformed line.

Arguments:
  • x1: The first x coordinate.
  • y1: The first y coordinate.
  • x2: The second x coordinate.
  • y2: The second y coordinate.
  • stroke: The color with which to draw this line.
  • weight: The stroke weight to use when drawing.
def get_with_offset( self, x: float, y: float) -> TransformedDrawable:
268    def get_with_offset(self, x: float, y: float) -> TransformedDrawable:
269        return TransformedLine(
270            self._x1 + x,
271            self._y1 + y,
272            self._x2 + x,
273            self._y2 + y,
274            self._stroke,
275            self._weight
276        )

Get a new version of this same object but with a horizontal and vertical offset.

Arguments:
  • x: The horizontal offset in pixels.
  • y: The vertical offset in pixels.
Returns:

A copy of this drawable component but with a positional translation applied.

def transform( self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
278    def transform(self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
279        point_1 = transformer.transform(self._x1, self._y1)
280        point_2 = transformer.transform(self._x2, self._y2)
281        weight = self._weight * point_1.get_scale()
282        return TransformedLine(
283            point_1.get_x(),
284            point_1.get_y(),
285            point_2.get_x(),
286            point_2.get_y(),
287            self._stroke,
288            weight
289        )

Get a new version of this same object but with a transformation applied.

Arguments:
  • transformer: Transformation matrix to apply.
Returns:

A copy of this drawable component but with a transformation applied.

def draw(self, target: WritableImage):
291    def draw(self, target: WritableImage):
292        target.get_drawable().line(
293            (
294                (self._x1, self._y1),
295                (self._x2, self._y2)
296            ),
297            fill=self._stroke,
298            width=self._weight
299        )

Draw this component.

Arguments:
  • target: The image on which to draw this component.
class TransformedClear(TransformedDrawable):
302class TransformedClear(TransformedDrawable):
303    """A pre-transformed clear operation."""
304
305    def __init__(self, color: COLOR_TUPLE):
306        """Create a record of a clear operation.
307
308        Args:
309            color: The color with which to clear.
310        """
311        self._color = color
312
313    def get_with_offset(self, x: float, y: float) -> TransformedDrawable:
314        return self
315
316    def transform(self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
317        return self
318
319    def draw(self, target: WritableImage):
320        image = target.get_image()
321        size = image.size
322        rect = (0, 0, size[0], size[1])
323        target.get_drawable().rectangle(rect, fill=self._color, width=0)

A pre-transformed clear operation.

TransformedClear(color: Union[Tuple[int, int, int], Tuple[int, int, int, int]])
305    def __init__(self, color: COLOR_TUPLE):
306        """Create a record of a clear operation.
307
308        Args:
309            color: The color with which to clear.
310        """
311        self._color = color

Create a record of a clear operation.

Arguments:
  • color: The color with which to clear.
def get_with_offset( self, x: float, y: float) -> TransformedDrawable:
313    def get_with_offset(self, x: float, y: float) -> TransformedDrawable:
314        return self

Get a new version of this same object but with a horizontal and vertical offset.

Arguments:
  • x: The horizontal offset in pixels.
  • y: The vertical offset in pixels.
Returns:

A copy of this drawable component but with a positional translation applied.

def transform( self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
316    def transform(self, transformer: sketchingpy.transform.Transformer) -> TransformedDrawable:
317        return self

Get a new version of this same object but with a transformation applied.

Arguments:
  • transformer: Transformation matrix to apply.
Returns:

A copy of this drawable component but with a transformation applied.

def draw(self, target: WritableImage):
319    def draw(self, target: WritableImage):
320        image = target.get_image()
321        size = image.size
322        rect = (0, 0, size[0], size[1])
323        target.get_drawable().rectangle(rect, fill=self._color, width=0)

Draw this component.

Arguments:
  • target: The image on which to draw this component.
def build_rect_with_mode( x1: float, y1: float, x2: float, y2: float, native_mode: int) -> Rect:
326def build_rect_with_mode(x1: float, y1: float, x2: float, y2: float,
327    native_mode: int) -> Rect:
328    """Build a rect with a mode of coordinate specification.
329
330    Args:
331        x1: The left or center x depending on mode.
332        y1: The top or center y depending on mode.
333        x2: The right or type of width depending on mode.
334        y2: The bottom or type of height depending on mode.
335        native_mode: The mode with which the coordinates were provided.
336
337    Returns:
338        Rect which interprets the given coordinates.
339    """
340    if native_mode == sketchingpy.const.CENTER:
341        start_x = x1 - math.floor(x2 / 2)
342        start_y = y1 - math.floor(y2 / 2)
343        width = x2
344        height = y2
345    elif native_mode == sketchingpy.const.RADIUS:
346        start_x = x1 - x2
347        start_y = y1 - y2
348        width = x2 * 2
349        height = y2 * 2
350    elif native_mode == sketchingpy.const.CORNER:
351        start_x = x1
352        start_y = y1
353        width = x2
354        height = y2
355    elif native_mode == sketchingpy.const.CORNERS:
356        (x1, y1, x2, y2) = sketchingpy.abstracted.reorder_coords(x1, y1, x2, y2)
357        start_x = x1
358        start_y = y1
359        width = x2 - x1
360        height = y2 - y1
361    else:
362        raise RuntimeError('Unknown mode: ' + str(native_mode))
363
364    return Rect(start_x, start_y, width, height)

Build a rect with a mode of coordinate specification.

Arguments:
  • x1: The left or center x depending on mode.
  • y1: The top or center y depending on mode.
  • x2: The right or type of width depending on mode.
  • y2: The bottom or type of height depending on mode.
  • native_mode: The mode with which the coordinates were provided.
Returns:

Rect which interprets the given coordinates.

class Macro:
367class Macro:
368    """Buffer-like object which keeps track of operations instead of the resulting raster."""
369
370    def __init__(self, width: float, height: float):
371        """Build a new macro record.
372
373        Args:
374            width: The horizontal size in pixels of the inteded area of drawing.
375            height: The vertical size in pixels of the inteded area of drawing.
376        """
377        self._width = width
378        self._height = height
379        self._elements: typing.List[TransformedDrawable] = []
380
381    def append(self, target: TransformedDrawable):
382        """Add a new drawable to this macro.
383
384        Args:
385            target: New element to add to this macro.
386        """
387        self._elements.append(target)
388
389    def get(self) -> typing.List[TransformedDrawable]:
390        """Get the elements in this macro.
391
392        Returns:
393            Operations for this macro.
394        """
395        return self._elements
396
397    def get_width(self) -> float:
398        """Get the width of the intended drawing area for this macro.
399
400        Returns:
401            Horizontal size of drawing area.
402        """
403        return self._width
404
405    def get_height(self) -> float:
406        """Get the vertical of the intended drawing area for this macro.
407
408        Returns:
409            Vertical size of drawing area.
410        """
411        return self._height

Buffer-like object which keeps track of operations instead of the resulting raster.

Macro(width: float, height: float)
370    def __init__(self, width: float, height: float):
371        """Build a new macro record.
372
373        Args:
374            width: The horizontal size in pixels of the inteded area of drawing.
375            height: The vertical size in pixels of the inteded area of drawing.
376        """
377        self._width = width
378        self._height = height
379        self._elements: typing.List[TransformedDrawable] = []

Build a new macro record.

Arguments:
  • width: The horizontal size in pixels of the inteded area of drawing.
  • height: The vertical size in pixels of the inteded area of drawing.
def append(self, target: TransformedDrawable):
381    def append(self, target: TransformedDrawable):
382        """Add a new drawable to this macro.
383
384        Args:
385            target: New element to add to this macro.
386        """
387        self._elements.append(target)

Add a new drawable to this macro.

Arguments:
  • target: New element to add to this macro.
def get(self) -> List[TransformedDrawable]:
389    def get(self) -> typing.List[TransformedDrawable]:
390        """Get the elements in this macro.
391
392        Returns:
393            Operations for this macro.
394        """
395        return self._elements

Get the elements in this macro.

Returns:

Operations for this macro.

def get_width(self) -> float:
397    def get_width(self) -> float:
398        """Get the width of the intended drawing area for this macro.
399
400        Returns:
401            Horizontal size of drawing area.
402        """
403        return self._width

Get the width of the intended drawing area for this macro.

Returns:

Horizontal size of drawing area.

def get_height(self) -> float:
405    def get_height(self) -> float:
406        """Get the vertical of the intended drawing area for this macro.
407
408        Returns:
409            Vertical size of drawing area.
410        """
411        return self._height

Get the vertical of the intended drawing area for this macro.

Returns:

Vertical size of drawing area.

def zero_rect(rect: Rect) -> Rect:
414def zero_rect(rect: Rect) -> Rect:
415    """Make a copy of a given rect but where the x and y coordinates are set to zero.
416
417    Args:
418        rect: The rect to put at 0, 0.
419
420    Returns:
421        Copy of the input rect set at 0, 0.
422    """
423    return Rect(0, 0, rect.get_width(), rect.get_height())

Make a copy of a given rect but where the x and y coordinates are set to zero.

Arguments:
  • rect: The rect to put at 0, 0.
Returns:

Copy of the input rect set at 0, 0.

def get_transformed( transformer: sketchingpy.transform.Transformer, surface: PIL.Image.Image, x: float, y: float) -> TransformedWritable:
426def get_transformed(transformer: sketchingpy.transform.Transformer, surface: PIL.Image.Image,
427    x: float, y: float) -> TransformedWritable:
428    """Convert an image to a pre-transformed writable.
429
430    Args:
431        transformer: The transformation to pre-apply.
432        surface: The image on which to apply the transformation.
433        x: The intended horizontal draw location of the given position within the given
434            transformation.
435        y: The intended vertical draw location of the given position within the given
436            transformation.
437
438    Returns:
439        Writable with the given transformation pre-applied.
440    """
441    start_rect = Rect(x, y, surface.width, surface.height)
442
443    transformed_center = transformer.transform(
444        start_rect.get_center_x(),
445        start_rect.get_center_y()
446    )
447
448    has_scale = transformed_center.get_scale() != 1
449    has_rotation = transformed_center.get_rotation() != 0
450    has_content_transform = has_scale or has_rotation
451    if has_content_transform:
452        angle = transformed_center.get_rotation()
453        angle_transform = math.degrees(angle)
454        scale = transformed_center.get_scale()
455        surface = surface.rotate(angle_transform, expand=True)
456        surface = surface.resize((
457            int(surface.width * scale),
458            int(surface.height * scale)
459        ))
460
461    end_rect = Rect(x, y, surface.width, surface.height)
462    end_rect.set_center_x(transformed_center.get_x())
463    end_rect.set_center_y(transformed_center.get_y())
464
465    return TransformedWritable(
466        WritableImage(surface, PIL.ImageDraw.Draw(surface)),
467        end_rect.get_x(),
468        end_rect.get_y()
469    )

Convert an image to a pre-transformed writable.

Arguments:
  • transformer: The transformation to pre-apply.
  • surface: The image on which to apply the transformation.
  • x: The intended horizontal draw location of the given position within the given transformation.
  • y: The intended vertical draw location of the given position within the given transformation.
Returns:

Writable with the given transformation pre-applied.

def get_retransformed( transformer: sketchingpy.transform.Transformer, target: TransformedWritable) -> TransformedWritable:
472def get_retransformed(transformer: sketchingpy.transform.Transformer,
473    target: TransformedWritable) -> TransformedWritable:
474    """Convert a transformed writable to a further pre-transformed writable.
475
476    Args:
477        transformer: The transformation to pre-apply.
478        target: The transformed writable to re-transform.
479
480    Returns:
481        Writable with the given transformation pre-applied.
482    """
483    return target.transform(transformer)  # type: ignore

Convert a transformed writable to a further pre-transformed writable.

Arguments:
  • transformer: The transformation to pre-apply.
  • target: The transformed writable to re-transform.
Returns:

Writable with the given transformation pre-applied.