classes.c50_image_pipeline
1from __future__ import annotations 2 3from contextlib import contextmanager 4from dataclasses import dataclass 5from typing import Iterator 6import drawBot 7from lib import graphics 8 9 10@dataclass 11class KImageContext: 12 """Rendering context for ``KImagePipeline.canvas()``. 13 14 Attributes: 15 width: Upsampled canvas width in points. 16 height: Upsampled canvas height in points. 17 fidelity: Upsample factor. 18 """ 19 20 width: float 21 height: float 22 fidelity: float 23 24 @property 25 def dimensions(self) -> tuple[float, float]: 26 return (self.width, self.height) 27 28 @property 29 def coords(self) -> tuple[float, float, float, float]: 30 return (0, 0, self.width, self.height) 31 32 def up(self, value: float | tuple[float, float]) -> float | tuple[float, float]: 33 """Scale point values from logical space to upsampled space. 34 35 Args: 36 value: Scalar or position tuple in logical coordinates. 37 38 Returns: 39 The value multiplied by the context fidelity. 40 """ 41 if isinstance(value, tuple): 42 return tuple(v * self.fidelity for v in value) 43 return value * self.fidelity 44 45 def tracking(self, letterSpacing: float, fontSize: float) -> float: 46 """Convert relative letter spacing units to DrawBot tracking points. 47 48 Args: 49 letterSpacing: Relative spacing in 1/1000 em units. 50 fontSize: Logical font size in points. 51 52 Returns: 53 Fidelity-aware tracking value in points. 54 """ 55 return self.up(letterSpacing * (fontSize / 1000)) 56 57 @contextmanager 58 def layer( 59 self, 60 backgroundColor: tuple | None = None, 61 ) -> Iterator["KLayerContext"]: 62 """Create a same-size temporary layer for selective processing. 63 64 Args: 65 backgroundColor: Optional fill color for the layer. 66 67 Yields: 68 A ``KLayerContext`` sized to the current canvas context. 69 """ 70 layer = drawBot.ImageObject() 71 with layer: 72 drawBot.size(self.width, self.height) 73 if backgroundColor is not None: 74 drawBot.fill(*backgroundColor) 75 drawBot.rect(*self.coords) 76 77 # Yield after the ImageObject context closes so the outer canvas 78 # remains active and generator filters can be applied to the layer. 79 yield KLayerContext( 80 width=self.width, 81 height=self.height, 82 fidelity=self.fidelity, 83 image=layer, 84 ) 85 86 def drawLayer( 87 self, 88 layer: drawBot.ImageObject | "KLayerContext", 89 position: tuple[float, float] = (0, 0), 90 blendMode: str | None = None, 91 compensateOffset: bool = True, 92 ): 93 """Draw a temporary layer onto the active pipeline canvas. 94 95 Args: 96 layer: Layer image to composite. 97 position: Destination position in upsampled coordinates. 98 blendMode: Optional DrawBot blend mode for compositing. 99 compensateOffset: Apply nested layer offset when compositing. 100 """ 101 layerImage = layer.image if isinstance(layer, KLayerContext) else layer 102 drawPosition = position 103 104 if compensateOffset and isinstance(layer, KLayerContext): 105 drawPosition = tuple(p + o for p, o in zip(position, layer.offset)) 106 107 with drawBot.savedState(): 108 if blendMode: 109 drawBot.blendMode(blendMode) 110 drawBot.image(layerImage, drawPosition) 111 112 113@dataclass 114class KLayerContext(KImageContext): 115 """Temporary compositing layer with context helpers and image effects.""" 116 117 image: drawBot.ImageObject 118 offset: tuple[float, float] = (0, 0) 119 120 def __getattr__(self, name: str): 121 """Proxy unknown attributes to the underlying ImageObject.""" 122 return getattr(self.image, name) 123 124 @property 125 def offsetLogical(self) -> tuple[float, float]: 126 """Layer offset converted back to logical coordinates.""" 127 return tuple(o / self.fidelity for o in self.offset) 128 129 def _setOffsetFromImage(self): 130 """Refresh layer offset from the current image state.""" 131 newOffset = tuple(self.image.offset()) 132 133 # Preserve existing non-zero expansion when a later effect reports zero. 134 if newOffset == (0, 0) and self.offset != (0, 0): 135 return 136 137 self.offset = newOffset 138 139 def precompose(self, backgroundColor=(1,)): 140 """Precompose the layer on a solid background. 141 142 Args: 143 backgroundColor: Fill color tuple. 144 145 Returns: 146 Self for fluent chaining. 147 """ 148 self.image = graphics.precomposeImage(self.image, backgroundColor) 149 self._setOffsetFromImage() 150 return self 151 152 def boxBlur(self, radius: float, scaleWithFidelity: bool = True): 153 """Apply DrawBot box blur to the layer. 154 155 Args: 156 radius: Blur radius in logical points. 157 scaleWithFidelity: Multiply radius by fidelity when ``True``. 158 159 Returns: 160 Self for fluent chaining. 161 """ 162 if scaleWithFidelity: 163 radius = self.up(radius) 164 self.image.boxBlur(radius) 165 self._setOffsetFromImage() 166 return self 167 168 def dotScreen( 169 self, 170 width: float, 171 angle: float = 45, 172 sharpness: float = 0.99, 173 scaleWithFidelity: bool = True, 174 ): 175 """Apply DrawBot dot-screen effect to the layer. 176 177 Args: 178 width: Dot width in logical points. 179 angle: Screen angle. 180 sharpness: Dot sharpness. 181 scaleWithFidelity: Multiply width by fidelity when ``True``. 182 183 Returns: 184 Self for fluent chaining. 185 """ 186 if scaleWithFidelity: 187 width = self.up(width) 188 self.image.dotScreen(width=width, angle=angle, sharpness=sharpness) 189 self._setOffsetFromImage() 190 return self 191 192 193class KImagePipeline: 194 ULTRA_HIGH_RES = 10 # For logo rendering 195 HIGH_RES = 5 # 300 / 72 ~= 4.17 196 MEDIUM_RES = 2 197 SCREEN_RES = 1 198 199 size: tuple[float, float] | None 200 renderSize: tuple[float, float] | None 201 fidelity: float 202 image: drawBot.ImageObject | None 203 offset: tuple[float, float] 204 205 def __init__( 206 self, 207 size: tuple[float, float] | None = None, 208 fidelity: float = HIGH_RES, 209 ): 210 self.size = size 211 self.fidelity = fidelity 212 self.renderSize = None 213 self.image = None 214 self.offset = (0, 0) 215 216 @property 217 def center(self) -> tuple[float, float]: 218 """Return the center point of the rendered image in logical coordinates.""" 219 if self.size is None: 220 raise ValueError("Image size not set. Use setSize() or canvas(size=...).") 221 return tuple(dim / 2 for dim in self.size) 222 223 def setSize(self, size: tuple[float, float]): 224 """Set logical image size in canvas coordinates. 225 226 Args: 227 size: Width and height in logical (target canvas) points. 228 229 Returns: 230 Self for fluent chaining. 231 """ 232 self.size = size 233 return self 234 235 def setFidelity(self, fidelity: float): 236 """Set upsample factor used during rendering. 237 238 Args: 239 fidelity: Resolution multiplier (1 = screen, 300/72 = print-like). 240 241 Returns: 242 Self for fluent chaining. 243 """ 244 self.fidelity = fidelity 245 return self 246 247 def _requireImage(self) -> drawBot.ImageObject: 248 if self.image is None: 249 raise ValueError( 250 "Image not set. Please run the pipeline canvas before drawing." 251 ) 252 return self.image 253 254 def _setOffsetFromImage(self): 255 image = self._requireImage() 256 newOffset = tuple(o / self.fidelity for o in image.offset()) 257 258 # Some effects (for example dotScreen) may report zero offset even when a 259 # previous effect expanded bounds; keep the known non-zero offset. 260 if newOffset == (0, 0) and self.offset != (0, 0): 261 return 262 263 self.offset = newOffset 264 265 @contextmanager 266 def canvas( 267 self, 268 size: tuple[float, float] | None = None, 269 backgroundColor: tuple | None = None, 270 ) -> Iterator[KImageContext]: 271 """Open an upsampled DrawBot image canvas. 272 273 Args: 274 size: Optional logical size override for this render pass. 275 backgroundColor: Optional background fill color. 276 277 Yields: 278 ``KImageContext`` with upsampled dimensions and scaling helper. 279 """ 280 if size is not None: 281 self.size = size 282 if self.size is None: 283 raise ValueError("Image size not set. Use setSize() or canvas(size=...).") 284 285 self.renderSize = tuple(dim * self.fidelity for dim in self.size) 286 self.image = drawBot.ImageObject() 287 288 with self.image: 289 drawBot.size(*self.renderSize) 290 if backgroundColor is not None: 291 drawBot.fill(*backgroundColor) 292 drawBot.rect(0, 0, *self.renderSize) 293 294 context = KImageContext( 295 width=self.renderSize[0], 296 height=self.renderSize[1], 297 fidelity=self.fidelity, 298 ) 299 yield context 300 301 self._setOffsetFromImage() 302 303 def render( 304 self, 305 paintFn, 306 size: tuple[float, float] | None = None, 307 backgroundColor: tuple | None = None, 308 ): 309 """Render image with a callback in one declarative call. 310 311 Args: 312 paintFn: Callback receiving ``KImageContext``. 313 size: Optional logical size override. 314 backgroundColor: Optional background fill color. 315 316 Returns: 317 Self for fluent chaining. 318 """ 319 with self.canvas(size=size, backgroundColor=backgroundColor) as context: 320 paintFn(context) 321 return self 322 323 def precompose(self, backgroundColor=(1,)): 324 """Precompose image on a solid background. 325 326 Args: 327 backgroundColor: Fill color tuple. 328 329 Returns: 330 Self for fluent chaining. 331 """ 332 self.image = graphics.precomposeImage(self._requireImage(), backgroundColor) 333 self._setOffsetFromImage() 334 return self 335 336 def boxBlur(self, radius: float, scaleWithFidelity: bool = True): 337 """Apply DrawBot box blur. 338 339 Args: 340 radius: Blur radius in logical points. 341 scaleWithFidelity: Multiply radius by fidelity when ``True``. 342 343 Returns: 344 Self for fluent chaining. 345 """ 346 image = self._requireImage() 347 if scaleWithFidelity: 348 radius *= self.fidelity 349 image.boxBlur(radius) 350 self._setOffsetFromImage() 351 return self 352 353 def dotScreen( 354 self, 355 width: float, 356 angle: float = 45, 357 sharpness: float = 0.99, 358 scaleWithFidelity: bool = True, 359 ): 360 """Apply DrawBot dot-screen effect. 361 362 Args: 363 width: Dot width in logical points. 364 angle: Screen angle. 365 sharpness: Dot sharpness. 366 scaleWithFidelity: Multiply width by fidelity when ``True``. 367 368 Returns: 369 Self for fluent chaining. 370 """ 371 image = self._requireImage() 372 if scaleWithFidelity: 373 width *= self.fidelity 374 375 image.dotScreen( 376 center=self.center, width=width, angle=angle, sharpness=sharpness 377 ) 378 self._setOffsetFromImage() 379 return self 380 381 def lineScreen( 382 self, 383 width: float, 384 angle: float = 0, 385 sharpness: float = 0.99, 386 scaleWithFidelity: bool = True, 387 ): 388 """Apply DrawBot line-screen effect. 389 390 Args: 391 width: Line width in logical points. 392 angle: Screen angle. 393 sharpness: Line sharpness. 394 scaleWithFidelity: Multiply width by fidelity when ``True``. 395 396 Returns: 397 Self for fluent chaining. 398 """ 399 image = self._requireImage() 400 if scaleWithFidelity: 401 width *= self.fidelity 402 403 image.lineScreen( 404 center=self.center, width=width, angle=angle, sharpness=sharpness 405 ) 406 self._setOffsetFromImage() 407 return self 408 409 def draw( 410 self, 411 position: tuple[float, float], 412 compensateOffset: bool = True, 413 ): 414 image = self._requireImage() 415 416 drawPosition = position 417 if compensateOffset: 418 drawPosition = tuple(p + o for p, o in zip(position, self.offset)) 419 420 with drawBot.savedState(): 421 if self.fidelity != self.SCREEN_RES: 422 drawBot.scale(1 / self.fidelity, center=drawPosition) 423 drawBot.image(image, drawPosition)
11@dataclass 12class KImageContext: 13 """Rendering context for ``KImagePipeline.canvas()``. 14 15 Attributes: 16 width: Upsampled canvas width in points. 17 height: Upsampled canvas height in points. 18 fidelity: Upsample factor. 19 """ 20 21 width: float 22 height: float 23 fidelity: float 24 25 @property 26 def dimensions(self) -> tuple[float, float]: 27 return (self.width, self.height) 28 29 @property 30 def coords(self) -> tuple[float, float, float, float]: 31 return (0, 0, self.width, self.height) 32 33 def up(self, value: float | tuple[float, float]) -> float | tuple[float, float]: 34 """Scale point values from logical space to upsampled space. 35 36 Args: 37 value: Scalar or position tuple in logical coordinates. 38 39 Returns: 40 The value multiplied by the context fidelity. 41 """ 42 if isinstance(value, tuple): 43 return tuple(v * self.fidelity for v in value) 44 return value * self.fidelity 45 46 def tracking(self, letterSpacing: float, fontSize: float) -> float: 47 """Convert relative letter spacing units to DrawBot tracking points. 48 49 Args: 50 letterSpacing: Relative spacing in 1/1000 em units. 51 fontSize: Logical font size in points. 52 53 Returns: 54 Fidelity-aware tracking value in points. 55 """ 56 return self.up(letterSpacing * (fontSize / 1000)) 57 58 @contextmanager 59 def layer( 60 self, 61 backgroundColor: tuple | None = None, 62 ) -> Iterator["KLayerContext"]: 63 """Create a same-size temporary layer for selective processing. 64 65 Args: 66 backgroundColor: Optional fill color for the layer. 67 68 Yields: 69 A ``KLayerContext`` sized to the current canvas context. 70 """ 71 layer = drawBot.ImageObject() 72 with layer: 73 drawBot.size(self.width, self.height) 74 if backgroundColor is not None: 75 drawBot.fill(*backgroundColor) 76 drawBot.rect(*self.coords) 77 78 # Yield after the ImageObject context closes so the outer canvas 79 # remains active and generator filters can be applied to the layer. 80 yield KLayerContext( 81 width=self.width, 82 height=self.height, 83 fidelity=self.fidelity, 84 image=layer, 85 ) 86 87 def drawLayer( 88 self, 89 layer: drawBot.ImageObject | "KLayerContext", 90 position: tuple[float, float] = (0, 0), 91 blendMode: str | None = None, 92 compensateOffset: bool = True, 93 ): 94 """Draw a temporary layer onto the active pipeline canvas. 95 96 Args: 97 layer: Layer image to composite. 98 position: Destination position in upsampled coordinates. 99 blendMode: Optional DrawBot blend mode for compositing. 100 compensateOffset: Apply nested layer offset when compositing. 101 """ 102 layerImage = layer.image if isinstance(layer, KLayerContext) else layer 103 drawPosition = position 104 105 if compensateOffset and isinstance(layer, KLayerContext): 106 drawPosition = tuple(p + o for p, o in zip(position, layer.offset)) 107 108 with drawBot.savedState(): 109 if blendMode: 110 drawBot.blendMode(blendMode) 111 drawBot.image(layerImage, drawPosition)
Rendering context for KImagePipeline.canvas().
Attributes:
- width: Upsampled canvas width in points.
- height: Upsampled canvas height in points.
- fidelity: Upsample factor.
33 def up(self, value: float | tuple[float, float]) -> float | tuple[float, float]: 34 """Scale point values from logical space to upsampled space. 35 36 Args: 37 value: Scalar or position tuple in logical coordinates. 38 39 Returns: 40 The value multiplied by the context fidelity. 41 """ 42 if isinstance(value, tuple): 43 return tuple(v * self.fidelity for v in value) 44 return value * self.fidelity
Scale point values from logical space to upsampled space.
Arguments:
- value: Scalar or position tuple in logical coordinates.
Returns:
The value multiplied by the context fidelity.
46 def tracking(self, letterSpacing: float, fontSize: float) -> float: 47 """Convert relative letter spacing units to DrawBot tracking points. 48 49 Args: 50 letterSpacing: Relative spacing in 1/1000 em units. 51 fontSize: Logical font size in points. 52 53 Returns: 54 Fidelity-aware tracking value in points. 55 """ 56 return self.up(letterSpacing * (fontSize / 1000))
Convert relative letter spacing units to DrawBot tracking points.
Arguments:
- letterSpacing: Relative spacing in 1/1000 em units.
- fontSize: Logical font size in points.
Returns:
Fidelity-aware tracking value in points.
58 @contextmanager 59 def layer( 60 self, 61 backgroundColor: tuple | None = None, 62 ) -> Iterator["KLayerContext"]: 63 """Create a same-size temporary layer for selective processing. 64 65 Args: 66 backgroundColor: Optional fill color for the layer. 67 68 Yields: 69 A ``KLayerContext`` sized to the current canvas context. 70 """ 71 layer = drawBot.ImageObject() 72 with layer: 73 drawBot.size(self.width, self.height) 74 if backgroundColor is not None: 75 drawBot.fill(*backgroundColor) 76 drawBot.rect(*self.coords) 77 78 # Yield after the ImageObject context closes so the outer canvas 79 # remains active and generator filters can be applied to the layer. 80 yield KLayerContext( 81 width=self.width, 82 height=self.height, 83 fidelity=self.fidelity, 84 image=layer, 85 )
Create a same-size temporary layer for selective processing.
Arguments:
- backgroundColor: Optional fill color for the layer.
Yields:
A
KLayerContextsized to the current canvas context.
87 def drawLayer( 88 self, 89 layer: drawBot.ImageObject | "KLayerContext", 90 position: tuple[float, float] = (0, 0), 91 blendMode: str | None = None, 92 compensateOffset: bool = True, 93 ): 94 """Draw a temporary layer onto the active pipeline canvas. 95 96 Args: 97 layer: Layer image to composite. 98 position: Destination position in upsampled coordinates. 99 blendMode: Optional DrawBot blend mode for compositing. 100 compensateOffset: Apply nested layer offset when compositing. 101 """ 102 layerImage = layer.image if isinstance(layer, KLayerContext) else layer 103 drawPosition = position 104 105 if compensateOffset and isinstance(layer, KLayerContext): 106 drawPosition = tuple(p + o for p, o in zip(position, layer.offset)) 107 108 with drawBot.savedState(): 109 if blendMode: 110 drawBot.blendMode(blendMode) 111 drawBot.image(layerImage, drawPosition)
Draw a temporary layer onto the active pipeline canvas.
Arguments:
- layer: Layer image to composite.
- position: Destination position in upsampled coordinates.
- blendMode: Optional DrawBot blend mode for compositing.
- compensateOffset: Apply nested layer offset when compositing.
114@dataclass 115class KLayerContext(KImageContext): 116 """Temporary compositing layer with context helpers and image effects.""" 117 118 image: drawBot.ImageObject 119 offset: tuple[float, float] = (0, 0) 120 121 def __getattr__(self, name: str): 122 """Proxy unknown attributes to the underlying ImageObject.""" 123 return getattr(self.image, name) 124 125 @property 126 def offsetLogical(self) -> tuple[float, float]: 127 """Layer offset converted back to logical coordinates.""" 128 return tuple(o / self.fidelity for o in self.offset) 129 130 def _setOffsetFromImage(self): 131 """Refresh layer offset from the current image state.""" 132 newOffset = tuple(self.image.offset()) 133 134 # Preserve existing non-zero expansion when a later effect reports zero. 135 if newOffset == (0, 0) and self.offset != (0, 0): 136 return 137 138 self.offset = newOffset 139 140 def precompose(self, backgroundColor=(1,)): 141 """Precompose the layer on a solid background. 142 143 Args: 144 backgroundColor: Fill color tuple. 145 146 Returns: 147 Self for fluent chaining. 148 """ 149 self.image = graphics.precomposeImage(self.image, backgroundColor) 150 self._setOffsetFromImage() 151 return self 152 153 def boxBlur(self, radius: float, scaleWithFidelity: bool = True): 154 """Apply DrawBot box blur to the layer. 155 156 Args: 157 radius: Blur radius in logical points. 158 scaleWithFidelity: Multiply radius by fidelity when ``True``. 159 160 Returns: 161 Self for fluent chaining. 162 """ 163 if scaleWithFidelity: 164 radius = self.up(radius) 165 self.image.boxBlur(radius) 166 self._setOffsetFromImage() 167 return self 168 169 def dotScreen( 170 self, 171 width: float, 172 angle: float = 45, 173 sharpness: float = 0.99, 174 scaleWithFidelity: bool = True, 175 ): 176 """Apply DrawBot dot-screen effect to the layer. 177 178 Args: 179 width: Dot width in logical points. 180 angle: Screen angle. 181 sharpness: Dot sharpness. 182 scaleWithFidelity: Multiply width by fidelity when ``True``. 183 184 Returns: 185 Self for fluent chaining. 186 """ 187 if scaleWithFidelity: 188 width = self.up(width) 189 self.image.dotScreen(width=width, angle=angle, sharpness=sharpness) 190 self._setOffsetFromImage() 191 return self
Temporary compositing layer with context helpers and image effects.
125 @property 126 def offsetLogical(self) -> tuple[float, float]: 127 """Layer offset converted back to logical coordinates.""" 128 return tuple(o / self.fidelity for o in self.offset)
Layer offset converted back to logical coordinates.
140 def precompose(self, backgroundColor=(1,)): 141 """Precompose the layer on a solid background. 142 143 Args: 144 backgroundColor: Fill color tuple. 145 146 Returns: 147 Self for fluent chaining. 148 """ 149 self.image = graphics.precomposeImage(self.image, backgroundColor) 150 self._setOffsetFromImage() 151 return self
Precompose the layer on a solid background.
Arguments:
- backgroundColor: Fill color tuple.
Returns:
Self for fluent chaining.
153 def boxBlur(self, radius: float, scaleWithFidelity: bool = True): 154 """Apply DrawBot box blur to the layer. 155 156 Args: 157 radius: Blur radius in logical points. 158 scaleWithFidelity: Multiply radius by fidelity when ``True``. 159 160 Returns: 161 Self for fluent chaining. 162 """ 163 if scaleWithFidelity: 164 radius = self.up(radius) 165 self.image.boxBlur(radius) 166 self._setOffsetFromImage() 167 return self
Apply DrawBot box blur to the layer.
Arguments:
- radius: Blur radius in logical points.
- scaleWithFidelity: Multiply radius by fidelity when
True.
Returns:
Self for fluent chaining.
169 def dotScreen( 170 self, 171 width: float, 172 angle: float = 45, 173 sharpness: float = 0.99, 174 scaleWithFidelity: bool = True, 175 ): 176 """Apply DrawBot dot-screen effect to the layer. 177 178 Args: 179 width: Dot width in logical points. 180 angle: Screen angle. 181 sharpness: Dot sharpness. 182 scaleWithFidelity: Multiply width by fidelity when ``True``. 183 184 Returns: 185 Self for fluent chaining. 186 """ 187 if scaleWithFidelity: 188 width = self.up(width) 189 self.image.dotScreen(width=width, angle=angle, sharpness=sharpness) 190 self._setOffsetFromImage() 191 return self
Apply DrawBot dot-screen effect to the layer.
Arguments:
- width: Dot width in logical points.
- angle: Screen angle.
- sharpness: Dot sharpness.
- scaleWithFidelity: Multiply width by fidelity when
True.
Returns:
Self for fluent chaining.
Inherited Members
194class KImagePipeline: 195 ULTRA_HIGH_RES = 10 # For logo rendering 196 HIGH_RES = 5 # 300 / 72 ~= 4.17 197 MEDIUM_RES = 2 198 SCREEN_RES = 1 199 200 size: tuple[float, float] | None 201 renderSize: tuple[float, float] | None 202 fidelity: float 203 image: drawBot.ImageObject | None 204 offset: tuple[float, float] 205 206 def __init__( 207 self, 208 size: tuple[float, float] | None = None, 209 fidelity: float = HIGH_RES, 210 ): 211 self.size = size 212 self.fidelity = fidelity 213 self.renderSize = None 214 self.image = None 215 self.offset = (0, 0) 216 217 @property 218 def center(self) -> tuple[float, float]: 219 """Return the center point of the rendered image in logical coordinates.""" 220 if self.size is None: 221 raise ValueError("Image size not set. Use setSize() or canvas(size=...).") 222 return tuple(dim / 2 for dim in self.size) 223 224 def setSize(self, size: tuple[float, float]): 225 """Set logical image size in canvas coordinates. 226 227 Args: 228 size: Width and height in logical (target canvas) points. 229 230 Returns: 231 Self for fluent chaining. 232 """ 233 self.size = size 234 return self 235 236 def setFidelity(self, fidelity: float): 237 """Set upsample factor used during rendering. 238 239 Args: 240 fidelity: Resolution multiplier (1 = screen, 300/72 = print-like). 241 242 Returns: 243 Self for fluent chaining. 244 """ 245 self.fidelity = fidelity 246 return self 247 248 def _requireImage(self) -> drawBot.ImageObject: 249 if self.image is None: 250 raise ValueError( 251 "Image not set. Please run the pipeline canvas before drawing." 252 ) 253 return self.image 254 255 def _setOffsetFromImage(self): 256 image = self._requireImage() 257 newOffset = tuple(o / self.fidelity for o in image.offset()) 258 259 # Some effects (for example dotScreen) may report zero offset even when a 260 # previous effect expanded bounds; keep the known non-zero offset. 261 if newOffset == (0, 0) and self.offset != (0, 0): 262 return 263 264 self.offset = newOffset 265 266 @contextmanager 267 def canvas( 268 self, 269 size: tuple[float, float] | None = None, 270 backgroundColor: tuple | None = None, 271 ) -> Iterator[KImageContext]: 272 """Open an upsampled DrawBot image canvas. 273 274 Args: 275 size: Optional logical size override for this render pass. 276 backgroundColor: Optional background fill color. 277 278 Yields: 279 ``KImageContext`` with upsampled dimensions and scaling helper. 280 """ 281 if size is not None: 282 self.size = size 283 if self.size is None: 284 raise ValueError("Image size not set. Use setSize() or canvas(size=...).") 285 286 self.renderSize = tuple(dim * self.fidelity for dim in self.size) 287 self.image = drawBot.ImageObject() 288 289 with self.image: 290 drawBot.size(*self.renderSize) 291 if backgroundColor is not None: 292 drawBot.fill(*backgroundColor) 293 drawBot.rect(0, 0, *self.renderSize) 294 295 context = KImageContext( 296 width=self.renderSize[0], 297 height=self.renderSize[1], 298 fidelity=self.fidelity, 299 ) 300 yield context 301 302 self._setOffsetFromImage() 303 304 def render( 305 self, 306 paintFn, 307 size: tuple[float, float] | None = None, 308 backgroundColor: tuple | None = None, 309 ): 310 """Render image with a callback in one declarative call. 311 312 Args: 313 paintFn: Callback receiving ``KImageContext``. 314 size: Optional logical size override. 315 backgroundColor: Optional background fill color. 316 317 Returns: 318 Self for fluent chaining. 319 """ 320 with self.canvas(size=size, backgroundColor=backgroundColor) as context: 321 paintFn(context) 322 return self 323 324 def precompose(self, backgroundColor=(1,)): 325 """Precompose image on a solid background. 326 327 Args: 328 backgroundColor: Fill color tuple. 329 330 Returns: 331 Self for fluent chaining. 332 """ 333 self.image = graphics.precomposeImage(self._requireImage(), backgroundColor) 334 self._setOffsetFromImage() 335 return self 336 337 def boxBlur(self, radius: float, scaleWithFidelity: bool = True): 338 """Apply DrawBot box blur. 339 340 Args: 341 radius: Blur radius in logical points. 342 scaleWithFidelity: Multiply radius by fidelity when ``True``. 343 344 Returns: 345 Self for fluent chaining. 346 """ 347 image = self._requireImage() 348 if scaleWithFidelity: 349 radius *= self.fidelity 350 image.boxBlur(radius) 351 self._setOffsetFromImage() 352 return self 353 354 def dotScreen( 355 self, 356 width: float, 357 angle: float = 45, 358 sharpness: float = 0.99, 359 scaleWithFidelity: bool = True, 360 ): 361 """Apply DrawBot dot-screen effect. 362 363 Args: 364 width: Dot width in logical points. 365 angle: Screen angle. 366 sharpness: Dot sharpness. 367 scaleWithFidelity: Multiply width by fidelity when ``True``. 368 369 Returns: 370 Self for fluent chaining. 371 """ 372 image = self._requireImage() 373 if scaleWithFidelity: 374 width *= self.fidelity 375 376 image.dotScreen( 377 center=self.center, width=width, angle=angle, sharpness=sharpness 378 ) 379 self._setOffsetFromImage() 380 return self 381 382 def lineScreen( 383 self, 384 width: float, 385 angle: float = 0, 386 sharpness: float = 0.99, 387 scaleWithFidelity: bool = True, 388 ): 389 """Apply DrawBot line-screen effect. 390 391 Args: 392 width: Line width in logical points. 393 angle: Screen angle. 394 sharpness: Line sharpness. 395 scaleWithFidelity: Multiply width by fidelity when ``True``. 396 397 Returns: 398 Self for fluent chaining. 399 """ 400 image = self._requireImage() 401 if scaleWithFidelity: 402 width *= self.fidelity 403 404 image.lineScreen( 405 center=self.center, width=width, angle=angle, sharpness=sharpness 406 ) 407 self._setOffsetFromImage() 408 return self 409 410 def draw( 411 self, 412 position: tuple[float, float], 413 compensateOffset: bool = True, 414 ): 415 image = self._requireImage() 416 417 drawPosition = position 418 if compensateOffset: 419 drawPosition = tuple(p + o for p, o in zip(position, self.offset)) 420 421 with drawBot.savedState(): 422 if self.fidelity != self.SCREEN_RES: 423 drawBot.scale(1 / self.fidelity, center=drawPosition) 424 drawBot.image(image, drawPosition)
217 @property 218 def center(self) -> tuple[float, float]: 219 """Return the center point of the rendered image in logical coordinates.""" 220 if self.size is None: 221 raise ValueError("Image size not set. Use setSize() or canvas(size=...).") 222 return tuple(dim / 2 for dim in self.size)
Return the center point of the rendered image in logical coordinates.
224 def setSize(self, size: tuple[float, float]): 225 """Set logical image size in canvas coordinates. 226 227 Args: 228 size: Width and height in logical (target canvas) points. 229 230 Returns: 231 Self for fluent chaining. 232 """ 233 self.size = size 234 return self
Set logical image size in canvas coordinates.
Arguments:
- size: Width and height in logical (target canvas) points.
Returns:
Self for fluent chaining.
236 def setFidelity(self, fidelity: float): 237 """Set upsample factor used during rendering. 238 239 Args: 240 fidelity: Resolution multiplier (1 = screen, 300/72 = print-like). 241 242 Returns: 243 Self for fluent chaining. 244 """ 245 self.fidelity = fidelity 246 return self
Set upsample factor used during rendering.
Arguments:
- fidelity: Resolution multiplier (1 = screen, 300/72 = print-like).
Returns:
Self for fluent chaining.
266 @contextmanager 267 def canvas( 268 self, 269 size: tuple[float, float] | None = None, 270 backgroundColor: tuple | None = None, 271 ) -> Iterator[KImageContext]: 272 """Open an upsampled DrawBot image canvas. 273 274 Args: 275 size: Optional logical size override for this render pass. 276 backgroundColor: Optional background fill color. 277 278 Yields: 279 ``KImageContext`` with upsampled dimensions and scaling helper. 280 """ 281 if size is not None: 282 self.size = size 283 if self.size is None: 284 raise ValueError("Image size not set. Use setSize() or canvas(size=...).") 285 286 self.renderSize = tuple(dim * self.fidelity for dim in self.size) 287 self.image = drawBot.ImageObject() 288 289 with self.image: 290 drawBot.size(*self.renderSize) 291 if backgroundColor is not None: 292 drawBot.fill(*backgroundColor) 293 drawBot.rect(0, 0, *self.renderSize) 294 295 context = KImageContext( 296 width=self.renderSize[0], 297 height=self.renderSize[1], 298 fidelity=self.fidelity, 299 ) 300 yield context 301 302 self._setOffsetFromImage()
Open an upsampled DrawBot image canvas.
Arguments:
- size: Optional logical size override for this render pass.
- backgroundColor: Optional background fill color.
Yields:
KImageContextwith upsampled dimensions and scaling helper.
304 def render( 305 self, 306 paintFn, 307 size: tuple[float, float] | None = None, 308 backgroundColor: tuple | None = None, 309 ): 310 """Render image with a callback in one declarative call. 311 312 Args: 313 paintFn: Callback receiving ``KImageContext``. 314 size: Optional logical size override. 315 backgroundColor: Optional background fill color. 316 317 Returns: 318 Self for fluent chaining. 319 """ 320 with self.canvas(size=size, backgroundColor=backgroundColor) as context: 321 paintFn(context) 322 return self
Render image with a callback in one declarative call.
Arguments:
- paintFn: Callback receiving
KImageContext. - size: Optional logical size override.
- backgroundColor: Optional background fill color.
Returns:
Self for fluent chaining.
324 def precompose(self, backgroundColor=(1,)): 325 """Precompose image on a solid background. 326 327 Args: 328 backgroundColor: Fill color tuple. 329 330 Returns: 331 Self for fluent chaining. 332 """ 333 self.image = graphics.precomposeImage(self._requireImage(), backgroundColor) 334 self._setOffsetFromImage() 335 return self
Precompose image on a solid background.
Arguments:
- backgroundColor: Fill color tuple.
Returns:
Self for fluent chaining.
337 def boxBlur(self, radius: float, scaleWithFidelity: bool = True): 338 """Apply DrawBot box blur. 339 340 Args: 341 radius: Blur radius in logical points. 342 scaleWithFidelity: Multiply radius by fidelity when ``True``. 343 344 Returns: 345 Self for fluent chaining. 346 """ 347 image = self._requireImage() 348 if scaleWithFidelity: 349 radius *= self.fidelity 350 image.boxBlur(radius) 351 self._setOffsetFromImage() 352 return self
Apply DrawBot box blur.
Arguments:
- radius: Blur radius in logical points.
- scaleWithFidelity: Multiply radius by fidelity when
True.
Returns:
Self for fluent chaining.
354 def dotScreen( 355 self, 356 width: float, 357 angle: float = 45, 358 sharpness: float = 0.99, 359 scaleWithFidelity: bool = True, 360 ): 361 """Apply DrawBot dot-screen effect. 362 363 Args: 364 width: Dot width in logical points. 365 angle: Screen angle. 366 sharpness: Dot sharpness. 367 scaleWithFidelity: Multiply width by fidelity when ``True``. 368 369 Returns: 370 Self for fluent chaining. 371 """ 372 image = self._requireImage() 373 if scaleWithFidelity: 374 width *= self.fidelity 375 376 image.dotScreen( 377 center=self.center, width=width, angle=angle, sharpness=sharpness 378 ) 379 self._setOffsetFromImage() 380 return self
Apply DrawBot dot-screen effect.
Arguments:
- width: Dot width in logical points.
- angle: Screen angle.
- sharpness: Dot sharpness.
- scaleWithFidelity: Multiply width by fidelity when
True.
Returns:
Self for fluent chaining.
382 def lineScreen( 383 self, 384 width: float, 385 angle: float = 0, 386 sharpness: float = 0.99, 387 scaleWithFidelity: bool = True, 388 ): 389 """Apply DrawBot line-screen effect. 390 391 Args: 392 width: Line width in logical points. 393 angle: Screen angle. 394 sharpness: Line sharpness. 395 scaleWithFidelity: Multiply width by fidelity when ``True``. 396 397 Returns: 398 Self for fluent chaining. 399 """ 400 image = self._requireImage() 401 if scaleWithFidelity: 402 width *= self.fidelity 403 404 image.lineScreen( 405 center=self.center, width=width, angle=angle, sharpness=sharpness 406 ) 407 self._setOffsetFromImage() 408 return self
Apply DrawBot line-screen effect.
Arguments:
- width: Line width in logical points.
- angle: Screen angle.
- sharpness: Line sharpness.
- scaleWithFidelity: Multiply width by fidelity when
True.
Returns:
Self for fluent chaining.
410 def draw( 411 self, 412 position: tuple[float, float], 413 compensateOffset: bool = True, 414 ): 415 image = self._requireImage() 416 417 drawPosition = position 418 if compensateOffset: 419 drawPosition = tuple(p + o for p, o in zip(position, self.offset)) 420 421 with drawBot.savedState(): 422 if self.fidelity != self.SCREEN_RES: 423 drawBot.scale(1 / self.fidelity, center=drawPosition) 424 drawBot.image(image, drawPosition)