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)
@dataclass
class KImageContext:
 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.
KImageContext(width: float, height: float, fidelity: float)
width: float
height: float
fidelity: float
dimensions: tuple[float, float]
25    @property
26    def dimensions(self) -> tuple[float, float]:
27        return (self.width, self.height)
coords: tuple[float, float, float, float]
29    @property
30    def coords(self) -> tuple[float, float, float, float]:
31        return (0, 0, self.width, self.height)
def up(self, value: float | tuple[float, float]) -> float | tuple[float, float]:
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.

def tracking(self, letterSpacing: float, fontSize: float) -> float:
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.

@contextmanager
def layer( self, backgroundColor: tuple | None = None) -> Iterator[KLayerContext]:
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 KLayerContext sized to the current canvas context.

def drawLayer( self, layer: "drawBot.ImageObject | 'KLayerContext'", position: tuple[float, float] = (0, 0), blendMode: str | None = None, compensateOffset: bool = True):
 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.
@dataclass
class KLayerContext(KImageContext):
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.

KLayerContext( width: float, height: float, fidelity: float, image: drawBot.context.tools.imageObject.ImageObject, offset: tuple[float, float] = (0, 0))
image: drawBot.context.tools.imageObject.ImageObject
offset: tuple[float, float] = (0, 0)
offsetLogical: tuple[float, float]
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.

def precompose(self, backgroundColor=(1,)):
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.

def boxBlur(self, radius: float, scaleWithFidelity: bool = True):
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.

def dotScreen( self, width: float, angle: float = 45, sharpness: float = 0.99, scaleWithFidelity: bool = True):
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.

class KImagePipeline:
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)
KImagePipeline(size: tuple[float, float] | None = None, fidelity: float = 5)
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)
ULTRA_HIGH_RES = 10
HIGH_RES = 5
MEDIUM_RES = 2
SCREEN_RES = 1
size: tuple[float, float] | None
renderSize: tuple[float, float] | None
fidelity: float
image: drawBot.context.tools.imageObject.ImageObject | None
offset: tuple[float, float]
center: tuple[float, float]
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.

def setSize(self, size: tuple[float, float]):
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.

def setFidelity(self, fidelity: float):
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.

@contextmanager
def canvas( self, size: tuple[float, float] | None = None, backgroundColor: tuple | None = None) -> Iterator[KImageContext]:
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:

KImageContext with upsampled dimensions and scaling helper.

def render( self, paintFn, size: tuple[float, float] | None = None, backgroundColor: tuple | None = None):
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.

def precompose(self, backgroundColor=(1,)):
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.

def boxBlur(self, radius: float, scaleWithFidelity: bool = True):
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.

def dotScreen( self, width: float, angle: float = 45, sharpness: float = 0.99, scaleWithFidelity: bool = True):
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.

def lineScreen( self, width: float, angle: float = 0, sharpness: float = 0.99, scaleWithFidelity: bool = True):
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.

def draw(self, position: tuple[float, float], compensateOffset: bool = True):
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)