classes.c20_box
1from typing import Literal, Union 2from loguru import logger 3import colorama 4import drawBot 5 6from lib import layout, helpers, color, fonts 7 8 9class KBox: 10 "A layout primitive with coordinates, dimensions and spatial utilities." 11 12 def __init__(self, coords: tuple, text: str | None = None) -> None: 13 """ 14 Initialize a KBox with given coordinates. 15 16 See `lib.layout.parsePageSize` for possible coordinate types. 17 18 Args: 19 coords: A tuple of (x, y, width, height) coordinates. 20 21 Example: 22 ```python 23 # Create a new box with coordinates 24 box = KBox((10, 10, 100, 50)) # x, y, width, height 25 26 # Move the box 27 box.move(x=20, y=10) 28 29 # Grow the box by adding margin 30 box.grow((5, 5, 5, 5)) # left, bottom, right, top margins 31 ``` 32 """ 33 self.x, self.y, self.width, self.height = layout.toCoords(coords) 34 self.text = text 35 36 def __str__(self) -> str: 37 """Return a string representation of the KBox instance.""" 38 x = f"{colorama.Fore.LIGHTYELLOW_EX}X {self.x}" 39 y = f"{colorama.Fore.LIGHTBLUE_EX}Y {self.y}" 40 w = f"{colorama.Fore.LIGHTMAGENTA_EX}W {self.width}" 41 h = f"{colorama.Fore.LIGHTCYAN_EX}H {self.height}" 42 t = f"{colorama.Fore.LIGHTWHITE_EX}T {self.text}" if self.text else "" 43 return f"{colorama.Fore.BLACK}{colorama.Back.LIGHTGREEN_EX}KBox{colorama.Style.RESET_ALL} {x} {y} {w} {h} {t}{colorama.Style.RESET_ALL}" 44 45 @staticmethod 46 def fromDimension(unit: layout.UnitSource) -> "KBox": 47 """ 48 Create a square KBox with the same width and height. 49 50 Implicit unit is `pt` but can be changed by passing a string like `10mm`. 51 """ 52 value = layout.parseUnit(unit, implicitUnit="pt") 53 return KBox((0, 0, value, value)) 54 55 @staticmethod 56 def fromDimensions(width: layout.UnitSource, height: layout.UnitSource) -> "KBox": 57 """ 58 Create a KBox from width and height, with x and y set to 0. 59 60 Implicit unit is `pt` but can be changed by passing a string like `10mm`. 61 """ 62 width, height = [ 63 layout.parseUnit(d, implicitUnit="pt") for d in (width, height) 64 ] 65 return KBox((0, 0, width, height)) 66 67 @staticmethod 68 def fromText(str: str, textWidth: float = None) -> "KBox": 69 """ 70 Create a KBox that fits the given text. Font settings must be already set. 71 72 Args: 73 str: The text to fit. 74 textWidth: Optional maximum width for the text box. If provided, the text will wrap to fit within this width. 75 76 Returns: 77 A new KBox instance that fits the text. 78 """ 79 return KBox((0, 0, *drawBot.textSize(str, width=textWidth)), text=str) 80 81 @property 82 def position(self) -> tuple[int, int]: 83 """ 84 Returns the x and y coordinates of the bottom-left corner. 85 """ 86 return self.x, self.y 87 88 @position.setter 89 def position(self, value: tuple[int, int]) -> None: 90 """ 91 Set the position (x, y) of the box. 92 """ 93 try: 94 self.x, self.y = value 95 except Exception as e: 96 logger.warning("Unable to set KBox position: {}. Error: {}", value, e) 97 98 @property 99 def dimensions(self) -> tuple[int, int]: 100 """ 101 Returns the dimensions (width, height) of the box in points. 102 """ 103 return self.width, self.height 104 105 @dimensions.setter 106 def dimensions(self, value: tuple[int, int]) -> None: 107 """ 108 Set the dimensions (width, height) of the box in points. 109 """ 110 try: 111 self.width, self.height = value 112 except Exception as e: 113 logger.warning("Unable to set KBox dimensions: {}. Error: {}", value, e) 114 115 @property 116 def coords(self) -> tuple[int, int, int, int]: 117 """ 118 Returns the complete coordinates (x, y, width, height) of the box. 119 """ 120 return self.x, self.y, self.width, self.height 121 122 @coords.setter 123 def coords(self, value: tuple[int, int, int, int]) -> None: 124 """ 125 Set the complete coordinates (x, y, width, height) of the box. 126 """ 127 try: 128 self.x, self.y, self.width, self.height = value 129 except Exception as e: 130 logger.warning("Unable to set KBox coords: {}. Error: {}", value, e) 131 132 @property 133 def left(self) -> int: 134 """ 135 Returns the x-coordinate of the left edge of the box. 136 """ 137 return self.x 138 139 @property 140 def right(self) -> int: 141 """ 142 Returns the x-coordinate of the right edge of the box. 143 """ 144 return self.x + self.width 145 146 @property 147 def top(self) -> int: 148 """ 149 Returns the y-coordinate of the top edge of the box. 150 """ 151 return self.y + self.height 152 153 @property 154 def bottom(self) -> int: 155 """ 156 Returns the y-coordinate of the bottom edge of the box. 157 """ 158 return self.y 159 160 @property 161 def middle(self) -> tuple[int, int]: 162 """ 163 Returns the (x, y) coordinates of the middle point of the box. 164 """ 165 return (self.middleX, self.middleY) 166 167 @property 168 def middleX(self) -> int: 169 """ 170 Returns the x-coordinate of the middle of the box. 171 """ 172 return self.x + self.width / 2 173 174 @property 175 def middleY(self) -> int: 176 """ 177 Returns the y-coordinate of the middle of the box. 178 """ 179 return self.y + self.height / 2 180 181 def xray(self, draw: bool = True) -> "KBox": 182 """ 183 Visualize the box using `lib.layout.xray` function. 184 185 Args: 186 draw: Whether to draw the box. Defaults to True. 187 """ 188 if draw: 189 layout.xray(self.coords) 190 return self 191 192 def draw(self, align: fonts.TextAlign = "left"): 193 """ 194 Draw the box's text content using `drawBot.textBox`. Font settings must be already set. 195 196 Args: 197 align: Text alignment within the box. Defaults to "left". Can be "left", "center", or "right". 198 199 Returns: 200 The result of `drawBot.textBox` which is overset text (if any). 201 """ 202 if not self.text: 203 raise ValueError( 204 "No text to draw in this KBox. Set the 'text' attribute first." 205 ) 206 return drawBot.textBox(self.text, self.coords, align=align) 207 208 def move( 209 self, 210 x: layout.ImplicitUnit = 0, 211 y: layout.ImplicitUnit = 0, 212 implicitUnit: layout.ImplicitUnit = "mm", 213 ) -> "KBox": 214 """ 215 Move the box by the specified amounts in x and y directions. 216 217 Args: 218 x: Horizontal offset. Positive values move right, negative values move left. 219 y: Vertical offset. Positive values move up, negative values move down. 220 """ 221 self.coords = layout.move(self.coords, x=x, y=y, implicitUnit=implicitUnit) 222 return self 223 224 def grow( 225 self, 226 margin: tuple[layout.UnitSource], 227 implicitUnit: layout.ImplicitUnit = "mm", 228 ) -> "KBox": 229 """ 230 Grow the box by adding margins via CSS-style tuple or single value. 231 232 Args: 233 margin: A tuple of (left, bottom, right, top) margins to add. 234 """ 235 self.coords = layout.grow(self.coords, margin=margin, implicitUnit=implicitUnit) 236 return self 237 238 def shrink( 239 self, 240 margin: tuple[layout.UnitSource], 241 implicitUnit: layout.ImplicitUnit = "mm", 242 ) -> "KBox": 243 """ 244 Shrink the box by adding padding via CSS-style tuple or single value. 245 246 Args: 247 margin: A tuple of (left, bottom, right, top) margins to subtract. 248 """ 249 self.coords = layout.shrink( 250 self.coords, 251 margin=margin, 252 implicitUnit=implicitUnit, 253 ) 254 return self 255 256 def offsetFrame( 257 self, 258 moveX: layout.UnitSource = 0, 259 moveY: layout.UnitSource = 0, 260 grow: layout.UnitSource = None, 261 shrink: layout.UnitSource = None, 262 implicitUnit: layout.ImplicitUnit = "mm", 263 ) -> layout.Coordinates: 264 """ 265 Return a derived frame without mutating this box. 266 267 `grow` and `shrink` are optional, but mutually exclusive. 268 269 Args: 270 moveX: Horizontal offset of the derived frame. 271 moveY: Vertical offset of the derived frame. 272 grow: Margin to grow outward. 273 shrink: Margin to inset inward. 274 implicitUnit: Implicit unit for all numeric values. 275 276 Returns: 277 Offset frame as (x, y, w, h). 278 """ 279 if grow is not None and shrink is not None: 280 raise ValueError("offsetFrame() allows only one of 'grow' or 'shrink'.") 281 282 frame = layout.move(self.coords, x=moveX, y=moveY, implicitUnit=implicitUnit) 283 284 if grow is not None: 285 return layout.grow(frame, margin=grow, implicitUnit=implicitUnit) 286 287 if shrink is not None: 288 return layout.shrink(frame, margin=shrink, implicitUnit=implicitUnit) 289 290 return frame 291 292 def duplicate( 293 self, 294 moveX: layout.UnitSource = 0, 295 moveY: layout.UnitSource = 0, 296 grow: layout.UnitSource = None, 297 shrink: layout.UnitSource = None, 298 implicitUnit: layout.ImplicitUnit = "mm", 299 ) -> "KBox": 300 """ 301 Create a duplicate of this box with optional offset and size adjustments. 302 303 Args: 304 moveX: Horizontal offset for the duplicate box. 305 moveY: Vertical offset for the duplicate box. 306 grow: Margin to grow the duplicate box outward. 307 shrink: Margin to shrink the duplicate box inward. 308 implicitUnit: Implicit unit for all numeric values. 309 310 Returns: 311 A new KBox instance with the applied transformations. 312 """ 313 newCoords = self.offsetFrame( 314 moveX=moveX, 315 moveY=moveY, 316 grow=grow, 317 shrink=shrink, 318 implicitUnit=implicitUnit, 319 ) 320 return KBox(newCoords) 321 322 def shrinkWidth( 323 self, 324 amount: layout.UnitSource, 325 origin: Literal["left", "right"] = "left", 326 implicitUnit: layout.ImplicitUnit = "decimal", 327 ) -> "KBox": 328 """ 329 Shrink the width of the box by a specified amount. 330 331 Args: 332 amount: The amount to shrink the width by. 333 origin: The side from which to shrink. Defaults to "left". 334 """ 335 self.coords = layout.shrinkWidth( 336 self.coords, 337 amount=amount, 338 origin=origin, 339 implicitUnit=implicitUnit, 340 ) 341 return self 342 343 def shrinkHeight( 344 self, 345 amount: layout.UnitSource, 346 origin: Literal["top", "bottom"] = "top", 347 implicitUnit: layout.ImplicitUnit = "decimal", 348 ) -> "KBox": 349 """ 350 Shrink the height of the box by a specified amount. 351 352 Args: 353 amount: The amount to shrink the height by. 354 origin: The side from which to shrink. Defaults to "top". 355 """ 356 self.coords = layout.shrinkHeight( 357 self.coords, 358 amount=amount, 359 origin=origin, 360 implicitUnit=implicitUnit, 361 ) 362 return self 363 364 def splitRelative( 365 self, 366 formula: str = "1/1", 367 gap: layout.UnitSource = 5, 368 gapImplicitUnit: layout.ImplicitUnit = "mm", 369 create: layout.LayoutFlow = "columns", 370 ) -> list["KBox"]: 371 """Split into child boxes by specified ratio. See `layout.splitRelative`.""" 372 return map( 373 KBox, 374 layout.splitRelative(self.coords, formula, gap, gapImplicitUnit, create), 375 ) 376 377 def splitAbsolute( 378 self, 379 create: layout.LayoutFlow, 380 origin: layout.LayoutOrigin, 381 size: layout.UnitSource, 382 gap: layout.UnitSource, 383 implicitUnit: layout.ImplicitUnit = "mm", 384 ) -> list["KBox"]: 385 """Split into child boxes by absolute size. See `layout.splitAbsolute`.""" 386 return map( 387 KBox, 388 layout.splitAbsolute(self.coords, create, origin, size, gap, implicitUnit), 389 ) 390 391 def partition( 392 self, 393 create: layout.LayoutFlow, 394 origin: layout.LayoutOrigin, 395 size: layout.UnitSource, 396 gap: layout.UnitSource, 397 implicitUnit: layout.ImplicitUnit = "pt", 398 mutate: bool = True, 399 ) -> "KBox": 400 """Partition the box by carving out a new box of specified size from a specified edge. 401 402 Args: 403 create: Whether to split into "rows" or "columns". 404 origin: Logical edge from which to partition: "start" (top/left) or "end" (bottom/right). 405 size: The size of the new box to carve out. 406 gap: The gap between the new box and the remaining box. 407 implicitUnit: The implicit unit for size and gap values. 408 mutate: If True, modify this box in place. If False, keep this box unchanged. 409 410 Returns: 411 The new KBox that was carved out. 412 """ 413 # ! Handle percentage-based size by converting to absolute value based on box dimensions 414 if layout.isInPercent(size): 415 size = layout.parseUnit( 416 size, base=self.height if create == "rows" else self.width 417 ) 418 419 new_rect, remainder_rect = layout.splitAbsolute( 420 self.coords, create, origin, size, gap, implicitUnit 421 ) 422 if mutate: 423 self.coords = remainder_rect 424 return KBox(new_rect) 425 426 def addHeader( 427 self, gap: layout.UnitSource = "5mm", lines: int = 1, mutate: bool = True 428 ) -> "KBox": 429 """ 430 Add a header box and optionally modify this box in place (shorthand for partition). 431 432 Args: 433 gap: Space between header and body. 434 lines: Number of lines in the header. Font properties must be set beforehand. 435 mutate: If True, modify this box in place. If False, keep this box unchanged. 436 437 Returns: 438 The header KBox. 439 """ 440 return self.partition( 441 "rows", "start", layout.inferBlockHeight(lines), gap=gap, mutate=mutate 442 ) 443 444 def addFooter( 445 self, gap: layout.UnitSource = "5mm", lines: int = 1, mutate: bool = True 446 ) -> "KBox": 447 """ 448 Add a footer box and optionally modify this box in place (shorthand for partition). 449 450 Args: 451 gap: Space between footer and body. 452 lines: Number of lines in the footer. Font properties must be set beforehand. 453 mutate: If True, modify this box in place. If False, keep this box unchanged. 454 455 Returns: 456 The footer KBox. 457 """ 458 return self.partition( 459 "rows", "end", layout.inferBlockHeight(lines), gap=gap, mutate=mutate 460 ) 461 462 def makeGrid( 463 self, 464 rows: int = 1, 465 cols: int = 1, 466 gutter: layout.UnitSource = "5mm", 467 debug: bool = False, 468 ) -> list["KBox"]: 469 """Split into a grid of specified `KBox` rows and columns with gutter. See `layout.makeGrid`.""" 470 return list(map(KBox, layout.makeGrid(self.coords, rows, cols, gutter, debug))) 471 472 def probeGridItem( 473 self, rows: int = 1, cols: int = 1, gutter: layout.UnitSource = "5mm" 474 ) -> "KBox": 475 """Calculate the dimensions of a grid item without positioning. Useful for layout calculations.""" 476 return KBox(layout.inferGridItemDimensions(self.dimensions, rows, cols, gutter)) 477 478 def alignInside( 479 self, 480 container: Union["KBox", tuple], 481 position: tuple[layout.AlignX, layout.AlignY] = ("center", "center"), 482 clip: bool = True, 483 nudgeY: float = 1.0, 484 ) -> "KBox": 485 """ 486 Align this box inside a container box. 487 488 Args: 489 container: The container `KBox` or `tuple` of coordinates. 490 position: The alignment position (x, y). Defaults to ("center", "center"). 491 clip: If True, shrink this box to fit inside container bounds. 492 If False, keep dimensions and allow overflow. 493 nudgeY: Optical vertical nudge strength. `0` disables nudge, 494 `1` applies default compensation, and larger values increase it. 495 """ 496 self.coords = layout.align( 497 container.coords if isinstance(container, KBox) else container, 498 self.coords, 499 position=position, 500 clip=clip, 501 nudgeY=nudgeY, 502 ) 503 return self 504 505 def justifyInside( 506 self, 507 container: Union["KBox", tuple], 508 count: int, 509 create: layout.LayoutFlow, 510 crossAlign: layout.AlignX | layout.AlignY | None = None, 511 ) -> list["KBox"]: 512 """ 513 Distribute multiple boxes evenly inside a container box. 514 515 Args: 516 container: The container `KBox` or `tuple` of coordinates. 517 count: The number of boxes to distribute. 518 create: Whether to distribute in "rows" or "columns". 519 crossAlign: Optional alignment for the non-justified axis ('left', 'center', 'right', 'stretch' for columns; 'top', 'center', 'bottom', 'stretch' for rows). 'stretch' expands each child to fill the full cross-axis extent of the parent. 520 Returns: 521 A list of new `KBox` instances that are distributed inside the container. 522 """ 523 return list( 524 map( 525 KBox, 526 layout.justify( 527 container.coords if isinstance(container, KBox) else container, 528 self.coords, 529 count, 530 create, 531 crossAlign, 532 ), 533 ) 534 ) 535 536 def snapHorizontally(self, x: int, snapTo: layout.AlignX = "center") -> "KBox": 537 if snapTo == "left": 538 self.x = x 539 elif snapTo == "center": 540 self.x = x - self.width / 2 541 elif snapTo == "right": 542 self.x = x - self.width 543 else: 544 logger.warning("Invalid horizontal snap position: {}", snapTo) 545 return self 546 547 def snapVertically(self, y: int, snapTo: layout.AlignY = "center") -> "KBox": 548 if snapTo == "bottom": 549 self.y = y - self.height 550 elif snapTo == "center": 551 self.y = y - self.height / 2 552 elif snapTo == "top": 553 self.y = y 554 else: 555 logger.warning("Invalid vertical snap position: {}", snapTo) 556 return self 557 558 def addCropMarks( 559 self, 560 edges: list[layout.BoxEdge] = ["top", "right", "bottom", "left"], 561 length: float = 1.5, 562 offset: float = 0.5, 563 ) -> "KBox": 564 """ 565 Add crop marks to the box. 566 567 Args: 568 edges: The edges to add crop marks to. Defaults to all four edges. 569 length: The length of crop marks in mm. Defaults to 1.5. 570 offset: The offset of crop marks in mm. Defaults to 0.5. 571 """ 572 length, offset = layout.mm(length), layout.mm(offset) 573 574 def _setLineAttrs(): 575 drawBot.fill(None) 576 drawBot.cmykStroke(*color.CMYK(k=100).asCMYKA) 577 drawBot.strokeWidth(0.5) 578 579 def _drawVertical(x: int): 580 drawBot.line((x, self.bottom - offset - length), (x, self.bottom - offset)) 581 drawBot.line((x, self.top + offset), (x, self.top + offset + length)) 582 583 def _drawHorizontal(y: int): 584 drawBot.line((self.left - offset - length, y), (self.left - offset, y)) 585 drawBot.line((self.right + offset, y), (self.right + offset + length, y)) 586 587 with drawBot.savedState(): 588 _setLineAttrs() 589 590 for edge in helpers.coerceList(edges): 591 func = _drawHorizontal if edge in ["top", "bottom"] else _drawVertical 592 coordinate: int = self.__getattribute__(edge) # self.top => 123 593 func(coordinate) 594 595 return self
10class KBox: 11 "A layout primitive with coordinates, dimensions and spatial utilities." 12 13 def __init__(self, coords: tuple, text: str | None = None) -> None: 14 """ 15 Initialize a KBox with given coordinates. 16 17 See `lib.layout.parsePageSize` for possible coordinate types. 18 19 Args: 20 coords: A tuple of (x, y, width, height) coordinates. 21 22 Example: 23 ```python 24 # Create a new box with coordinates 25 box = KBox((10, 10, 100, 50)) # x, y, width, height 26 27 # Move the box 28 box.move(x=20, y=10) 29 30 # Grow the box by adding margin 31 box.grow((5, 5, 5, 5)) # left, bottom, right, top margins 32 ``` 33 """ 34 self.x, self.y, self.width, self.height = layout.toCoords(coords) 35 self.text = text 36 37 def __str__(self) -> str: 38 """Return a string representation of the KBox instance.""" 39 x = f"{colorama.Fore.LIGHTYELLOW_EX}X {self.x}" 40 y = f"{colorama.Fore.LIGHTBLUE_EX}Y {self.y}" 41 w = f"{colorama.Fore.LIGHTMAGENTA_EX}W {self.width}" 42 h = f"{colorama.Fore.LIGHTCYAN_EX}H {self.height}" 43 t = f"{colorama.Fore.LIGHTWHITE_EX}T {self.text}" if self.text else "" 44 return f"{colorama.Fore.BLACK}{colorama.Back.LIGHTGREEN_EX}KBox{colorama.Style.RESET_ALL} {x} {y} {w} {h} {t}{colorama.Style.RESET_ALL}" 45 46 @staticmethod 47 def fromDimension(unit: layout.UnitSource) -> "KBox": 48 """ 49 Create a square KBox with the same width and height. 50 51 Implicit unit is `pt` but can be changed by passing a string like `10mm`. 52 """ 53 value = layout.parseUnit(unit, implicitUnit="pt") 54 return KBox((0, 0, value, value)) 55 56 @staticmethod 57 def fromDimensions(width: layout.UnitSource, height: layout.UnitSource) -> "KBox": 58 """ 59 Create a KBox from width and height, with x and y set to 0. 60 61 Implicit unit is `pt` but can be changed by passing a string like `10mm`. 62 """ 63 width, height = [ 64 layout.parseUnit(d, implicitUnit="pt") for d in (width, height) 65 ] 66 return KBox((0, 0, width, height)) 67 68 @staticmethod 69 def fromText(str: str, textWidth: float = None) -> "KBox": 70 """ 71 Create a KBox that fits the given text. Font settings must be already set. 72 73 Args: 74 str: The text to fit. 75 textWidth: Optional maximum width for the text box. If provided, the text will wrap to fit within this width. 76 77 Returns: 78 A new KBox instance that fits the text. 79 """ 80 return KBox((0, 0, *drawBot.textSize(str, width=textWidth)), text=str) 81 82 @property 83 def position(self) -> tuple[int, int]: 84 """ 85 Returns the x and y coordinates of the bottom-left corner. 86 """ 87 return self.x, self.y 88 89 @position.setter 90 def position(self, value: tuple[int, int]) -> None: 91 """ 92 Set the position (x, y) of the box. 93 """ 94 try: 95 self.x, self.y = value 96 except Exception as e: 97 logger.warning("Unable to set KBox position: {}. Error: {}", value, e) 98 99 @property 100 def dimensions(self) -> tuple[int, int]: 101 """ 102 Returns the dimensions (width, height) of the box in points. 103 """ 104 return self.width, self.height 105 106 @dimensions.setter 107 def dimensions(self, value: tuple[int, int]) -> None: 108 """ 109 Set the dimensions (width, height) of the box in points. 110 """ 111 try: 112 self.width, self.height = value 113 except Exception as e: 114 logger.warning("Unable to set KBox dimensions: {}. Error: {}", value, e) 115 116 @property 117 def coords(self) -> tuple[int, int, int, int]: 118 """ 119 Returns the complete coordinates (x, y, width, height) of the box. 120 """ 121 return self.x, self.y, self.width, self.height 122 123 @coords.setter 124 def coords(self, value: tuple[int, int, int, int]) -> None: 125 """ 126 Set the complete coordinates (x, y, width, height) of the box. 127 """ 128 try: 129 self.x, self.y, self.width, self.height = value 130 except Exception as e: 131 logger.warning("Unable to set KBox coords: {}. Error: {}", value, e) 132 133 @property 134 def left(self) -> int: 135 """ 136 Returns the x-coordinate of the left edge of the box. 137 """ 138 return self.x 139 140 @property 141 def right(self) -> int: 142 """ 143 Returns the x-coordinate of the right edge of the box. 144 """ 145 return self.x + self.width 146 147 @property 148 def top(self) -> int: 149 """ 150 Returns the y-coordinate of the top edge of the box. 151 """ 152 return self.y + self.height 153 154 @property 155 def bottom(self) -> int: 156 """ 157 Returns the y-coordinate of the bottom edge of the box. 158 """ 159 return self.y 160 161 @property 162 def middle(self) -> tuple[int, int]: 163 """ 164 Returns the (x, y) coordinates of the middle point of the box. 165 """ 166 return (self.middleX, self.middleY) 167 168 @property 169 def middleX(self) -> int: 170 """ 171 Returns the x-coordinate of the middle of the box. 172 """ 173 return self.x + self.width / 2 174 175 @property 176 def middleY(self) -> int: 177 """ 178 Returns the y-coordinate of the middle of the box. 179 """ 180 return self.y + self.height / 2 181 182 def xray(self, draw: bool = True) -> "KBox": 183 """ 184 Visualize the box using `lib.layout.xray` function. 185 186 Args: 187 draw: Whether to draw the box. Defaults to True. 188 """ 189 if draw: 190 layout.xray(self.coords) 191 return self 192 193 def draw(self, align: fonts.TextAlign = "left"): 194 """ 195 Draw the box's text content using `drawBot.textBox`. Font settings must be already set. 196 197 Args: 198 align: Text alignment within the box. Defaults to "left". Can be "left", "center", or "right". 199 200 Returns: 201 The result of `drawBot.textBox` which is overset text (if any). 202 """ 203 if not self.text: 204 raise ValueError( 205 "No text to draw in this KBox. Set the 'text' attribute first." 206 ) 207 return drawBot.textBox(self.text, self.coords, align=align) 208 209 def move( 210 self, 211 x: layout.ImplicitUnit = 0, 212 y: layout.ImplicitUnit = 0, 213 implicitUnit: layout.ImplicitUnit = "mm", 214 ) -> "KBox": 215 """ 216 Move the box by the specified amounts in x and y directions. 217 218 Args: 219 x: Horizontal offset. Positive values move right, negative values move left. 220 y: Vertical offset. Positive values move up, negative values move down. 221 """ 222 self.coords = layout.move(self.coords, x=x, y=y, implicitUnit=implicitUnit) 223 return self 224 225 def grow( 226 self, 227 margin: tuple[layout.UnitSource], 228 implicitUnit: layout.ImplicitUnit = "mm", 229 ) -> "KBox": 230 """ 231 Grow the box by adding margins via CSS-style tuple or single value. 232 233 Args: 234 margin: A tuple of (left, bottom, right, top) margins to add. 235 """ 236 self.coords = layout.grow(self.coords, margin=margin, implicitUnit=implicitUnit) 237 return self 238 239 def shrink( 240 self, 241 margin: tuple[layout.UnitSource], 242 implicitUnit: layout.ImplicitUnit = "mm", 243 ) -> "KBox": 244 """ 245 Shrink the box by adding padding via CSS-style tuple or single value. 246 247 Args: 248 margin: A tuple of (left, bottom, right, top) margins to subtract. 249 """ 250 self.coords = layout.shrink( 251 self.coords, 252 margin=margin, 253 implicitUnit=implicitUnit, 254 ) 255 return self 256 257 def offsetFrame( 258 self, 259 moveX: layout.UnitSource = 0, 260 moveY: layout.UnitSource = 0, 261 grow: layout.UnitSource = None, 262 shrink: layout.UnitSource = None, 263 implicitUnit: layout.ImplicitUnit = "mm", 264 ) -> layout.Coordinates: 265 """ 266 Return a derived frame without mutating this box. 267 268 `grow` and `shrink` are optional, but mutually exclusive. 269 270 Args: 271 moveX: Horizontal offset of the derived frame. 272 moveY: Vertical offset of the derived frame. 273 grow: Margin to grow outward. 274 shrink: Margin to inset inward. 275 implicitUnit: Implicit unit for all numeric values. 276 277 Returns: 278 Offset frame as (x, y, w, h). 279 """ 280 if grow is not None and shrink is not None: 281 raise ValueError("offsetFrame() allows only one of 'grow' or 'shrink'.") 282 283 frame = layout.move(self.coords, x=moveX, y=moveY, implicitUnit=implicitUnit) 284 285 if grow is not None: 286 return layout.grow(frame, margin=grow, implicitUnit=implicitUnit) 287 288 if shrink is not None: 289 return layout.shrink(frame, margin=shrink, implicitUnit=implicitUnit) 290 291 return frame 292 293 def duplicate( 294 self, 295 moveX: layout.UnitSource = 0, 296 moveY: layout.UnitSource = 0, 297 grow: layout.UnitSource = None, 298 shrink: layout.UnitSource = None, 299 implicitUnit: layout.ImplicitUnit = "mm", 300 ) -> "KBox": 301 """ 302 Create a duplicate of this box with optional offset and size adjustments. 303 304 Args: 305 moveX: Horizontal offset for the duplicate box. 306 moveY: Vertical offset for the duplicate box. 307 grow: Margin to grow the duplicate box outward. 308 shrink: Margin to shrink the duplicate box inward. 309 implicitUnit: Implicit unit for all numeric values. 310 311 Returns: 312 A new KBox instance with the applied transformations. 313 """ 314 newCoords = self.offsetFrame( 315 moveX=moveX, 316 moveY=moveY, 317 grow=grow, 318 shrink=shrink, 319 implicitUnit=implicitUnit, 320 ) 321 return KBox(newCoords) 322 323 def shrinkWidth( 324 self, 325 amount: layout.UnitSource, 326 origin: Literal["left", "right"] = "left", 327 implicitUnit: layout.ImplicitUnit = "decimal", 328 ) -> "KBox": 329 """ 330 Shrink the width of the box by a specified amount. 331 332 Args: 333 amount: The amount to shrink the width by. 334 origin: The side from which to shrink. Defaults to "left". 335 """ 336 self.coords = layout.shrinkWidth( 337 self.coords, 338 amount=amount, 339 origin=origin, 340 implicitUnit=implicitUnit, 341 ) 342 return self 343 344 def shrinkHeight( 345 self, 346 amount: layout.UnitSource, 347 origin: Literal["top", "bottom"] = "top", 348 implicitUnit: layout.ImplicitUnit = "decimal", 349 ) -> "KBox": 350 """ 351 Shrink the height of the box by a specified amount. 352 353 Args: 354 amount: The amount to shrink the height by. 355 origin: The side from which to shrink. Defaults to "top". 356 """ 357 self.coords = layout.shrinkHeight( 358 self.coords, 359 amount=amount, 360 origin=origin, 361 implicitUnit=implicitUnit, 362 ) 363 return self 364 365 def splitRelative( 366 self, 367 formula: str = "1/1", 368 gap: layout.UnitSource = 5, 369 gapImplicitUnit: layout.ImplicitUnit = "mm", 370 create: layout.LayoutFlow = "columns", 371 ) -> list["KBox"]: 372 """Split into child boxes by specified ratio. See `layout.splitRelative`.""" 373 return map( 374 KBox, 375 layout.splitRelative(self.coords, formula, gap, gapImplicitUnit, create), 376 ) 377 378 def splitAbsolute( 379 self, 380 create: layout.LayoutFlow, 381 origin: layout.LayoutOrigin, 382 size: layout.UnitSource, 383 gap: layout.UnitSource, 384 implicitUnit: layout.ImplicitUnit = "mm", 385 ) -> list["KBox"]: 386 """Split into child boxes by absolute size. See `layout.splitAbsolute`.""" 387 return map( 388 KBox, 389 layout.splitAbsolute(self.coords, create, origin, size, gap, implicitUnit), 390 ) 391 392 def partition( 393 self, 394 create: layout.LayoutFlow, 395 origin: layout.LayoutOrigin, 396 size: layout.UnitSource, 397 gap: layout.UnitSource, 398 implicitUnit: layout.ImplicitUnit = "pt", 399 mutate: bool = True, 400 ) -> "KBox": 401 """Partition the box by carving out a new box of specified size from a specified edge. 402 403 Args: 404 create: Whether to split into "rows" or "columns". 405 origin: Logical edge from which to partition: "start" (top/left) or "end" (bottom/right). 406 size: The size of the new box to carve out. 407 gap: The gap between the new box and the remaining box. 408 implicitUnit: The implicit unit for size and gap values. 409 mutate: If True, modify this box in place. If False, keep this box unchanged. 410 411 Returns: 412 The new KBox that was carved out. 413 """ 414 # ! Handle percentage-based size by converting to absolute value based on box dimensions 415 if layout.isInPercent(size): 416 size = layout.parseUnit( 417 size, base=self.height if create == "rows" else self.width 418 ) 419 420 new_rect, remainder_rect = layout.splitAbsolute( 421 self.coords, create, origin, size, gap, implicitUnit 422 ) 423 if mutate: 424 self.coords = remainder_rect 425 return KBox(new_rect) 426 427 def addHeader( 428 self, gap: layout.UnitSource = "5mm", lines: int = 1, mutate: bool = True 429 ) -> "KBox": 430 """ 431 Add a header box and optionally modify this box in place (shorthand for partition). 432 433 Args: 434 gap: Space between header and body. 435 lines: Number of lines in the header. Font properties must be set beforehand. 436 mutate: If True, modify this box in place. If False, keep this box unchanged. 437 438 Returns: 439 The header KBox. 440 """ 441 return self.partition( 442 "rows", "start", layout.inferBlockHeight(lines), gap=gap, mutate=mutate 443 ) 444 445 def addFooter( 446 self, gap: layout.UnitSource = "5mm", lines: int = 1, mutate: bool = True 447 ) -> "KBox": 448 """ 449 Add a footer box and optionally modify this box in place (shorthand for partition). 450 451 Args: 452 gap: Space between footer and body. 453 lines: Number of lines in the footer. Font properties must be set beforehand. 454 mutate: If True, modify this box in place. If False, keep this box unchanged. 455 456 Returns: 457 The footer KBox. 458 """ 459 return self.partition( 460 "rows", "end", layout.inferBlockHeight(lines), gap=gap, mutate=mutate 461 ) 462 463 def makeGrid( 464 self, 465 rows: int = 1, 466 cols: int = 1, 467 gutter: layout.UnitSource = "5mm", 468 debug: bool = False, 469 ) -> list["KBox"]: 470 """Split into a grid of specified `KBox` rows and columns with gutter. See `layout.makeGrid`.""" 471 return list(map(KBox, layout.makeGrid(self.coords, rows, cols, gutter, debug))) 472 473 def probeGridItem( 474 self, rows: int = 1, cols: int = 1, gutter: layout.UnitSource = "5mm" 475 ) -> "KBox": 476 """Calculate the dimensions of a grid item without positioning. Useful for layout calculations.""" 477 return KBox(layout.inferGridItemDimensions(self.dimensions, rows, cols, gutter)) 478 479 def alignInside( 480 self, 481 container: Union["KBox", tuple], 482 position: tuple[layout.AlignX, layout.AlignY] = ("center", "center"), 483 clip: bool = True, 484 nudgeY: float = 1.0, 485 ) -> "KBox": 486 """ 487 Align this box inside a container box. 488 489 Args: 490 container: The container `KBox` or `tuple` of coordinates. 491 position: The alignment position (x, y). Defaults to ("center", "center"). 492 clip: If True, shrink this box to fit inside container bounds. 493 If False, keep dimensions and allow overflow. 494 nudgeY: Optical vertical nudge strength. `0` disables nudge, 495 `1` applies default compensation, and larger values increase it. 496 """ 497 self.coords = layout.align( 498 container.coords if isinstance(container, KBox) else container, 499 self.coords, 500 position=position, 501 clip=clip, 502 nudgeY=nudgeY, 503 ) 504 return self 505 506 def justifyInside( 507 self, 508 container: Union["KBox", tuple], 509 count: int, 510 create: layout.LayoutFlow, 511 crossAlign: layout.AlignX | layout.AlignY | None = None, 512 ) -> list["KBox"]: 513 """ 514 Distribute multiple boxes evenly inside a container box. 515 516 Args: 517 container: The container `KBox` or `tuple` of coordinates. 518 count: The number of boxes to distribute. 519 create: Whether to distribute in "rows" or "columns". 520 crossAlign: Optional alignment for the non-justified axis ('left', 'center', 'right', 'stretch' for columns; 'top', 'center', 'bottom', 'stretch' for rows). 'stretch' expands each child to fill the full cross-axis extent of the parent. 521 Returns: 522 A list of new `KBox` instances that are distributed inside the container. 523 """ 524 return list( 525 map( 526 KBox, 527 layout.justify( 528 container.coords if isinstance(container, KBox) else container, 529 self.coords, 530 count, 531 create, 532 crossAlign, 533 ), 534 ) 535 ) 536 537 def snapHorizontally(self, x: int, snapTo: layout.AlignX = "center") -> "KBox": 538 if snapTo == "left": 539 self.x = x 540 elif snapTo == "center": 541 self.x = x - self.width / 2 542 elif snapTo == "right": 543 self.x = x - self.width 544 else: 545 logger.warning("Invalid horizontal snap position: {}", snapTo) 546 return self 547 548 def snapVertically(self, y: int, snapTo: layout.AlignY = "center") -> "KBox": 549 if snapTo == "bottom": 550 self.y = y - self.height 551 elif snapTo == "center": 552 self.y = y - self.height / 2 553 elif snapTo == "top": 554 self.y = y 555 else: 556 logger.warning("Invalid vertical snap position: {}", snapTo) 557 return self 558 559 def addCropMarks( 560 self, 561 edges: list[layout.BoxEdge] = ["top", "right", "bottom", "left"], 562 length: float = 1.5, 563 offset: float = 0.5, 564 ) -> "KBox": 565 """ 566 Add crop marks to the box. 567 568 Args: 569 edges: The edges to add crop marks to. Defaults to all four edges. 570 length: The length of crop marks in mm. Defaults to 1.5. 571 offset: The offset of crop marks in mm. Defaults to 0.5. 572 """ 573 length, offset = layout.mm(length), layout.mm(offset) 574 575 def _setLineAttrs(): 576 drawBot.fill(None) 577 drawBot.cmykStroke(*color.CMYK(k=100).asCMYKA) 578 drawBot.strokeWidth(0.5) 579 580 def _drawVertical(x: int): 581 drawBot.line((x, self.bottom - offset - length), (x, self.bottom - offset)) 582 drawBot.line((x, self.top + offset), (x, self.top + offset + length)) 583 584 def _drawHorizontal(y: int): 585 drawBot.line((self.left - offset - length, y), (self.left - offset, y)) 586 drawBot.line((self.right + offset, y), (self.right + offset + length, y)) 587 588 with drawBot.savedState(): 589 _setLineAttrs() 590 591 for edge in helpers.coerceList(edges): 592 func = _drawHorizontal if edge in ["top", "bottom"] else _drawVertical 593 coordinate: int = self.__getattribute__(edge) # self.top => 123 594 func(coordinate) 595 596 return self
A layout primitive with coordinates, dimensions and spatial utilities.
13 def __init__(self, coords: tuple, text: str | None = None) -> None: 14 """ 15 Initialize a KBox with given coordinates. 16 17 See `lib.layout.parsePageSize` for possible coordinate types. 18 19 Args: 20 coords: A tuple of (x, y, width, height) coordinates. 21 22 Example: 23 ```python 24 # Create a new box with coordinates 25 box = KBox((10, 10, 100, 50)) # x, y, width, height 26 27 # Move the box 28 box.move(x=20, y=10) 29 30 # Grow the box by adding margin 31 box.grow((5, 5, 5, 5)) # left, bottom, right, top margins 32 ``` 33 """ 34 self.x, self.y, self.width, self.height = layout.toCoords(coords) 35 self.text = text
Initialize a KBox with given coordinates.
See lib.layout.parsePageSize for possible coordinate types.
Arguments:
- coords: A tuple of (x, y, width, height) coordinates.
Example:
# Create a new box with coordinates
box = KBox((10, 10, 100, 50)) # x, y, width, height
# Move the box
box.move(x=20, y=10)
# Grow the box by adding margin
box.grow((5, 5, 5, 5)) # left, bottom, right, top margins
46 @staticmethod 47 def fromDimension(unit: layout.UnitSource) -> "KBox": 48 """ 49 Create a square KBox with the same width and height. 50 51 Implicit unit is `pt` but can be changed by passing a string like `10mm`. 52 """ 53 value = layout.parseUnit(unit, implicitUnit="pt") 54 return KBox((0, 0, value, value))
Create a square KBox with the same width and height.
Implicit unit is pt but can be changed by passing a string like 10mm.
56 @staticmethod 57 def fromDimensions(width: layout.UnitSource, height: layout.UnitSource) -> "KBox": 58 """ 59 Create a KBox from width and height, with x and y set to 0. 60 61 Implicit unit is `pt` but can be changed by passing a string like `10mm`. 62 """ 63 width, height = [ 64 layout.parseUnit(d, implicitUnit="pt") for d in (width, height) 65 ] 66 return KBox((0, 0, width, height))
Create a KBox from width and height, with x and y set to 0.
Implicit unit is pt but can be changed by passing a string like 10mm.
68 @staticmethod 69 def fromText(str: str, textWidth: float = None) -> "KBox": 70 """ 71 Create a KBox that fits the given text. Font settings must be already set. 72 73 Args: 74 str: The text to fit. 75 textWidth: Optional maximum width for the text box. If provided, the text will wrap to fit within this width. 76 77 Returns: 78 A new KBox instance that fits the text. 79 """ 80 return KBox((0, 0, *drawBot.textSize(str, width=textWidth)), text=str)
Create a KBox that fits the given text. Font settings must be already set.
Arguments:
- str: The text to fit.
- textWidth: Optional maximum width for the text box. If provided, the text will wrap to fit within this width.
Returns:
A new KBox instance that fits the text.
82 @property 83 def position(self) -> tuple[int, int]: 84 """ 85 Returns the x and y coordinates of the bottom-left corner. 86 """ 87 return self.x, self.y
Returns the x and y coordinates of the bottom-left corner.
99 @property 100 def dimensions(self) -> tuple[int, int]: 101 """ 102 Returns the dimensions (width, height) of the box in points. 103 """ 104 return self.width, self.height
Returns the dimensions (width, height) of the box in points.
116 @property 117 def coords(self) -> tuple[int, int, int, int]: 118 """ 119 Returns the complete coordinates (x, y, width, height) of the box. 120 """ 121 return self.x, self.y, self.width, self.height
Returns the complete coordinates (x, y, width, height) of the box.
133 @property 134 def left(self) -> int: 135 """ 136 Returns the x-coordinate of the left edge of the box. 137 """ 138 return self.x
Returns the x-coordinate of the left edge of the box.
140 @property 141 def right(self) -> int: 142 """ 143 Returns the x-coordinate of the right edge of the box. 144 """ 145 return self.x + self.width
Returns the x-coordinate of the right edge of the box.
147 @property 148 def top(self) -> int: 149 """ 150 Returns the y-coordinate of the top edge of the box. 151 """ 152 return self.y + self.height
Returns the y-coordinate of the top edge of the box.
154 @property 155 def bottom(self) -> int: 156 """ 157 Returns the y-coordinate of the bottom edge of the box. 158 """ 159 return self.y
Returns the y-coordinate of the bottom edge of the box.
161 @property 162 def middle(self) -> tuple[int, int]: 163 """ 164 Returns the (x, y) coordinates of the middle point of the box. 165 """ 166 return (self.middleX, self.middleY)
Returns the (x, y) coordinates of the middle point of the box.
168 @property 169 def middleX(self) -> int: 170 """ 171 Returns the x-coordinate of the middle of the box. 172 """ 173 return self.x + self.width / 2
Returns the x-coordinate of the middle of the box.
175 @property 176 def middleY(self) -> int: 177 """ 178 Returns the y-coordinate of the middle of the box. 179 """ 180 return self.y + self.height / 2
Returns the y-coordinate of the middle of the box.
182 def xray(self, draw: bool = True) -> "KBox": 183 """ 184 Visualize the box using `lib.layout.xray` function. 185 186 Args: 187 draw: Whether to draw the box. Defaults to True. 188 """ 189 if draw: 190 layout.xray(self.coords) 191 return self
Visualize the box using lib.layout.xray function.
Arguments:
- draw: Whether to draw the box. Defaults to True.
193 def draw(self, align: fonts.TextAlign = "left"): 194 """ 195 Draw the box's text content using `drawBot.textBox`. Font settings must be already set. 196 197 Args: 198 align: Text alignment within the box. Defaults to "left". Can be "left", "center", or "right". 199 200 Returns: 201 The result of `drawBot.textBox` which is overset text (if any). 202 """ 203 if not self.text: 204 raise ValueError( 205 "No text to draw in this KBox. Set the 'text' attribute first." 206 ) 207 return drawBot.textBox(self.text, self.coords, align=align)
Draw the box's text content using drawBot.textBox. Font settings must be already set.
Arguments:
- align: Text alignment within the box. Defaults to "left". Can be "left", "center", or "right".
Returns:
The result of
drawBot.textBoxwhich is overset text (if any).
209 def move( 210 self, 211 x: layout.ImplicitUnit = 0, 212 y: layout.ImplicitUnit = 0, 213 implicitUnit: layout.ImplicitUnit = "mm", 214 ) -> "KBox": 215 """ 216 Move the box by the specified amounts in x and y directions. 217 218 Args: 219 x: Horizontal offset. Positive values move right, negative values move left. 220 y: Vertical offset. Positive values move up, negative values move down. 221 """ 222 self.coords = layout.move(self.coords, x=x, y=y, implicitUnit=implicitUnit) 223 return self
Move the box by the specified amounts in x and y directions.
Arguments:
- x: Horizontal offset. Positive values move right, negative values move left.
- y: Vertical offset. Positive values move up, negative values move down.
225 def grow( 226 self, 227 margin: tuple[layout.UnitSource], 228 implicitUnit: layout.ImplicitUnit = "mm", 229 ) -> "KBox": 230 """ 231 Grow the box by adding margins via CSS-style tuple or single value. 232 233 Args: 234 margin: A tuple of (left, bottom, right, top) margins to add. 235 """ 236 self.coords = layout.grow(self.coords, margin=margin, implicitUnit=implicitUnit) 237 return self
Grow the box by adding margins via CSS-style tuple or single value.
Arguments:
- margin: A tuple of (left, bottom, right, top) margins to add.
239 def shrink( 240 self, 241 margin: tuple[layout.UnitSource], 242 implicitUnit: layout.ImplicitUnit = "mm", 243 ) -> "KBox": 244 """ 245 Shrink the box by adding padding via CSS-style tuple or single value. 246 247 Args: 248 margin: A tuple of (left, bottom, right, top) margins to subtract. 249 """ 250 self.coords = layout.shrink( 251 self.coords, 252 margin=margin, 253 implicitUnit=implicitUnit, 254 ) 255 return self
Shrink the box by adding padding via CSS-style tuple or single value.
Arguments:
- margin: A tuple of (left, bottom, right, top) margins to subtract.
257 def offsetFrame( 258 self, 259 moveX: layout.UnitSource = 0, 260 moveY: layout.UnitSource = 0, 261 grow: layout.UnitSource = None, 262 shrink: layout.UnitSource = None, 263 implicitUnit: layout.ImplicitUnit = "mm", 264 ) -> layout.Coordinates: 265 """ 266 Return a derived frame without mutating this box. 267 268 `grow` and `shrink` are optional, but mutually exclusive. 269 270 Args: 271 moveX: Horizontal offset of the derived frame. 272 moveY: Vertical offset of the derived frame. 273 grow: Margin to grow outward. 274 shrink: Margin to inset inward. 275 implicitUnit: Implicit unit for all numeric values. 276 277 Returns: 278 Offset frame as (x, y, w, h). 279 """ 280 if grow is not None and shrink is not None: 281 raise ValueError("offsetFrame() allows only one of 'grow' or 'shrink'.") 282 283 frame = layout.move(self.coords, x=moveX, y=moveY, implicitUnit=implicitUnit) 284 285 if grow is not None: 286 return layout.grow(frame, margin=grow, implicitUnit=implicitUnit) 287 288 if shrink is not None: 289 return layout.shrink(frame, margin=shrink, implicitUnit=implicitUnit) 290 291 return frame
Return a derived frame without mutating this box.
grow and shrink are optional, but mutually exclusive.
Arguments:
- moveX: Horizontal offset of the derived frame.
- moveY: Vertical offset of the derived frame.
- grow: Margin to grow outward.
- shrink: Margin to inset inward.
- implicitUnit: Implicit unit for all numeric values.
Returns:
Offset frame as (x, y, w, h).
293 def duplicate( 294 self, 295 moveX: layout.UnitSource = 0, 296 moveY: layout.UnitSource = 0, 297 grow: layout.UnitSource = None, 298 shrink: layout.UnitSource = None, 299 implicitUnit: layout.ImplicitUnit = "mm", 300 ) -> "KBox": 301 """ 302 Create a duplicate of this box with optional offset and size adjustments. 303 304 Args: 305 moveX: Horizontal offset for the duplicate box. 306 moveY: Vertical offset for the duplicate box. 307 grow: Margin to grow the duplicate box outward. 308 shrink: Margin to shrink the duplicate box inward. 309 implicitUnit: Implicit unit for all numeric values. 310 311 Returns: 312 A new KBox instance with the applied transformations. 313 """ 314 newCoords = self.offsetFrame( 315 moveX=moveX, 316 moveY=moveY, 317 grow=grow, 318 shrink=shrink, 319 implicitUnit=implicitUnit, 320 ) 321 return KBox(newCoords)
Create a duplicate of this box with optional offset and size adjustments.
Arguments:
- moveX: Horizontal offset for the duplicate box.
- moveY: Vertical offset for the duplicate box.
- grow: Margin to grow the duplicate box outward.
- shrink: Margin to shrink the duplicate box inward.
- implicitUnit: Implicit unit for all numeric values.
Returns:
A new KBox instance with the applied transformations.
323 def shrinkWidth( 324 self, 325 amount: layout.UnitSource, 326 origin: Literal["left", "right"] = "left", 327 implicitUnit: layout.ImplicitUnit = "decimal", 328 ) -> "KBox": 329 """ 330 Shrink the width of the box by a specified amount. 331 332 Args: 333 amount: The amount to shrink the width by. 334 origin: The side from which to shrink. Defaults to "left". 335 """ 336 self.coords = layout.shrinkWidth( 337 self.coords, 338 amount=amount, 339 origin=origin, 340 implicitUnit=implicitUnit, 341 ) 342 return self
Shrink the width of the box by a specified amount.
Arguments:
- amount: The amount to shrink the width by.
- origin: The side from which to shrink. Defaults to "left".
344 def shrinkHeight( 345 self, 346 amount: layout.UnitSource, 347 origin: Literal["top", "bottom"] = "top", 348 implicitUnit: layout.ImplicitUnit = "decimal", 349 ) -> "KBox": 350 """ 351 Shrink the height of the box by a specified amount. 352 353 Args: 354 amount: The amount to shrink the height by. 355 origin: The side from which to shrink. Defaults to "top". 356 """ 357 self.coords = layout.shrinkHeight( 358 self.coords, 359 amount=amount, 360 origin=origin, 361 implicitUnit=implicitUnit, 362 ) 363 return self
Shrink the height of the box by a specified amount.
Arguments:
- amount: The amount to shrink the height by.
- origin: The side from which to shrink. Defaults to "top".
365 def splitRelative( 366 self, 367 formula: str = "1/1", 368 gap: layout.UnitSource = 5, 369 gapImplicitUnit: layout.ImplicitUnit = "mm", 370 create: layout.LayoutFlow = "columns", 371 ) -> list["KBox"]: 372 """Split into child boxes by specified ratio. See `layout.splitRelative`.""" 373 return map( 374 KBox, 375 layout.splitRelative(self.coords, formula, gap, gapImplicitUnit, create), 376 )
Split into child boxes by specified ratio. See layout.splitRelative.
378 def splitAbsolute( 379 self, 380 create: layout.LayoutFlow, 381 origin: layout.LayoutOrigin, 382 size: layout.UnitSource, 383 gap: layout.UnitSource, 384 implicitUnit: layout.ImplicitUnit = "mm", 385 ) -> list["KBox"]: 386 """Split into child boxes by absolute size. See `layout.splitAbsolute`.""" 387 return map( 388 KBox, 389 layout.splitAbsolute(self.coords, create, origin, size, gap, implicitUnit), 390 )
Split into child boxes by absolute size. See layout.splitAbsolute.
392 def partition( 393 self, 394 create: layout.LayoutFlow, 395 origin: layout.LayoutOrigin, 396 size: layout.UnitSource, 397 gap: layout.UnitSource, 398 implicitUnit: layout.ImplicitUnit = "pt", 399 mutate: bool = True, 400 ) -> "KBox": 401 """Partition the box by carving out a new box of specified size from a specified edge. 402 403 Args: 404 create: Whether to split into "rows" or "columns". 405 origin: Logical edge from which to partition: "start" (top/left) or "end" (bottom/right). 406 size: The size of the new box to carve out. 407 gap: The gap between the new box and the remaining box. 408 implicitUnit: The implicit unit for size and gap values. 409 mutate: If True, modify this box in place. If False, keep this box unchanged. 410 411 Returns: 412 The new KBox that was carved out. 413 """ 414 # ! Handle percentage-based size by converting to absolute value based on box dimensions 415 if layout.isInPercent(size): 416 size = layout.parseUnit( 417 size, base=self.height if create == "rows" else self.width 418 ) 419 420 new_rect, remainder_rect = layout.splitAbsolute( 421 self.coords, create, origin, size, gap, implicitUnit 422 ) 423 if mutate: 424 self.coords = remainder_rect 425 return KBox(new_rect)
Partition the box by carving out a new box of specified size from a specified edge.
Arguments:
- create: Whether to split into "rows" or "columns".
- origin: Logical edge from which to partition: "start" (top/left) or "end" (bottom/right).
- size: The size of the new box to carve out.
- gap: The gap between the new box and the remaining box.
- implicitUnit: The implicit unit for size and gap values.
- mutate: If True, modify this box in place. If False, keep this box unchanged.
Returns:
The new KBox that was carved out.
427 def addHeader( 428 self, gap: layout.UnitSource = "5mm", lines: int = 1, mutate: bool = True 429 ) -> "KBox": 430 """ 431 Add a header box and optionally modify this box in place (shorthand for partition). 432 433 Args: 434 gap: Space between header and body. 435 lines: Number of lines in the header. Font properties must be set beforehand. 436 mutate: If True, modify this box in place. If False, keep this box unchanged. 437 438 Returns: 439 The header KBox. 440 """ 441 return self.partition( 442 "rows", "start", layout.inferBlockHeight(lines), gap=gap, mutate=mutate 443 )
Add a header box and optionally modify this box in place (shorthand for partition).
Arguments:
- gap: Space between header and body.
- lines: Number of lines in the header. Font properties must be set beforehand.
- mutate: If True, modify this box in place. If False, keep this box unchanged.
Returns:
The header KBox.
463 def makeGrid( 464 self, 465 rows: int = 1, 466 cols: int = 1, 467 gutter: layout.UnitSource = "5mm", 468 debug: bool = False, 469 ) -> list["KBox"]: 470 """Split into a grid of specified `KBox` rows and columns with gutter. See `layout.makeGrid`.""" 471 return list(map(KBox, layout.makeGrid(self.coords, rows, cols, gutter, debug)))
Split into a grid of specified KBox rows and columns with gutter. See layout.makeGrid.
473 def probeGridItem( 474 self, rows: int = 1, cols: int = 1, gutter: layout.UnitSource = "5mm" 475 ) -> "KBox": 476 """Calculate the dimensions of a grid item without positioning. Useful for layout calculations.""" 477 return KBox(layout.inferGridItemDimensions(self.dimensions, rows, cols, gutter))
Calculate the dimensions of a grid item without positioning. Useful for layout calculations.
479 def alignInside( 480 self, 481 container: Union["KBox", tuple], 482 position: tuple[layout.AlignX, layout.AlignY] = ("center", "center"), 483 clip: bool = True, 484 nudgeY: float = 1.0, 485 ) -> "KBox": 486 """ 487 Align this box inside a container box. 488 489 Args: 490 container: The container `KBox` or `tuple` of coordinates. 491 position: The alignment position (x, y). Defaults to ("center", "center"). 492 clip: If True, shrink this box to fit inside container bounds. 493 If False, keep dimensions and allow overflow. 494 nudgeY: Optical vertical nudge strength. `0` disables nudge, 495 `1` applies default compensation, and larger values increase it. 496 """ 497 self.coords = layout.align( 498 container.coords if isinstance(container, KBox) else container, 499 self.coords, 500 position=position, 501 clip=clip, 502 nudgeY=nudgeY, 503 ) 504 return self
Align this box inside a container box.
Arguments:
- container: The container
KBoxortupleof coordinates. - position: The alignment position (x, y). Defaults to ("center", "center").
- clip: If True, shrink this box to fit inside container bounds. If False, keep dimensions and allow overflow.
- nudgeY: Optical vertical nudge strength.
0disables nudge,1applies default compensation, and larger values increase it.
506 def justifyInside( 507 self, 508 container: Union["KBox", tuple], 509 count: int, 510 create: layout.LayoutFlow, 511 crossAlign: layout.AlignX | layout.AlignY | None = None, 512 ) -> list["KBox"]: 513 """ 514 Distribute multiple boxes evenly inside a container box. 515 516 Args: 517 container: The container `KBox` or `tuple` of coordinates. 518 count: The number of boxes to distribute. 519 create: Whether to distribute in "rows" or "columns". 520 crossAlign: Optional alignment for the non-justified axis ('left', 'center', 'right', 'stretch' for columns; 'top', 'center', 'bottom', 'stretch' for rows). 'stretch' expands each child to fill the full cross-axis extent of the parent. 521 Returns: 522 A list of new `KBox` instances that are distributed inside the container. 523 """ 524 return list( 525 map( 526 KBox, 527 layout.justify( 528 container.coords if isinstance(container, KBox) else container, 529 self.coords, 530 count, 531 create, 532 crossAlign, 533 ), 534 ) 535 )
Distribute multiple boxes evenly inside a container box.
Arguments:
- container: The container
KBoxortupleof coordinates. - count: The number of boxes to distribute.
- create: Whether to distribute in "rows" or "columns".
- crossAlign: Optional alignment for the non-justified axis ('left', 'center', 'right', 'stretch' for columns; 'top', 'center', 'bottom', 'stretch' for rows). 'stretch' expands each child to fill the full cross-axis extent of the parent.
Returns:
A list of new
KBoxinstances that are distributed inside the container.
537 def snapHorizontally(self, x: int, snapTo: layout.AlignX = "center") -> "KBox": 538 if snapTo == "left": 539 self.x = x 540 elif snapTo == "center": 541 self.x = x - self.width / 2 542 elif snapTo == "right": 543 self.x = x - self.width 544 else: 545 logger.warning("Invalid horizontal snap position: {}", snapTo) 546 return self
548 def snapVertically(self, y: int, snapTo: layout.AlignY = "center") -> "KBox": 549 if snapTo == "bottom": 550 self.y = y - self.height 551 elif snapTo == "center": 552 self.y = y - self.height / 2 553 elif snapTo == "top": 554 self.y = y 555 else: 556 logger.warning("Invalid vertical snap position: {}", snapTo) 557 return self
559 def addCropMarks( 560 self, 561 edges: list[layout.BoxEdge] = ["top", "right", "bottom", "left"], 562 length: float = 1.5, 563 offset: float = 0.5, 564 ) -> "KBox": 565 """ 566 Add crop marks to the box. 567 568 Args: 569 edges: The edges to add crop marks to. Defaults to all four edges. 570 length: The length of crop marks in mm. Defaults to 1.5. 571 offset: The offset of crop marks in mm. Defaults to 0.5. 572 """ 573 length, offset = layout.mm(length), layout.mm(offset) 574 575 def _setLineAttrs(): 576 drawBot.fill(None) 577 drawBot.cmykStroke(*color.CMYK(k=100).asCMYKA) 578 drawBot.strokeWidth(0.5) 579 580 def _drawVertical(x: int): 581 drawBot.line((x, self.bottom - offset - length), (x, self.bottom - offset)) 582 drawBot.line((x, self.top + offset), (x, self.top + offset + length)) 583 584 def _drawHorizontal(y: int): 585 drawBot.line((self.left - offset - length, y), (self.left - offset, y)) 586 drawBot.line((self.right + offset, y), (self.right + offset + length, y)) 587 588 with drawBot.savedState(): 589 _setLineAttrs() 590 591 for edge in helpers.coerceList(edges): 592 func = _drawHorizontal if edge in ["top", "bottom"] else _drawVertical 593 coordinate: int = self.__getattribute__(edge) # self.top => 123 594 func(coordinate) 595 596 return self
Add crop marks to the box.
Arguments:
- edges: The edges to add crop marks to. Defaults to all four edges.
- length: The length of crop marks in mm. Defaults to 1.5.
- offset: The offset of crop marks in mm. Defaults to 0.5.