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