classes.c21_page
1import drawBot 2 3from lib import layout, color, DEFAULT_FONT 4from classes import KBox 5 6 7class KPage(KBox): 8 bgColor: color.RawRGB = None 9 10 def __init__( 11 self, 12 size: layout.PageSize = "A5", 13 margin: tuple = 5, 14 aside: int = 0, 15 spread: bool = False, 16 render: bool = True, 17 ): 18 """ 19 Extends `classes.c20_box.KBox` to form a page object. 20 21 Args: 22 size: Page size. See `lib.layout.PageSize`. 23 margin: Margin (implicitly in millimeters). 24 aside: Width of pagination column (implicitly in millimeters). 25 spread: Divide at spine for 2-page PDF view. 26 render: Draw immediately. Otherwise, call `render()` manually. 27 """ 28 super().__init__(size) 29 30 if render: 31 self.render() 32 33 self.aside: int = aside 34 """Width of aside/pagination column.""" 35 self.margin: tuple[int] = layout.mirror(margin) if spread else margin 36 """Margin for the page (mirrored for left side).""" 37 self._isSpread: bool = spread 38 """Used to draw a spine for 2-page PDF view.""" 39 40 self._divide() 41 self._draftBoxes() 42 43 @staticmethod 44 def fromMillimeters(dimensions: tuple[int, int], **kwargs) -> "KPage": 45 """ 46 Alternate constructor to create a KPage from dimensions in millimeters. 47 48 Args: 49 dimensions: Tuple of (width, height) in mm. 50 **kwargs: Additional arguments for the main constructor. 51 """ 52 return KPage(tuple(layout.mm(d) for d in dimensions), **kwargs) 53 54 def _draftBoxes(self) -> None: 55 """Set up the frame and body `classes.c20_box.KBox` objects.""" 56 self.frame = KBox(layout.shrink(frame=self.coords, margin=self.margin)) 57 self.body = KBox( 58 layout.shrinkWidth( 59 frame=self.frame.coords, 60 amount=self.aside, 61 origin="right" if self.isRightSide else "left", 62 implicitUnit="mm", 63 ) 64 ) 65 66 @property 67 def isLeftSide(self) -> bool: 68 """True if the page is on the left side.""" 69 return layout.pageIsEven() 70 71 @property 72 def isRightSide(self) -> bool: 73 """True if the page is on the right side.""" 74 return not self.isLeftSide 75 76 @property 77 def size(self) -> tuple[int, int]: 78 """Page size in millimeters.""" 79 return tuple(layout.pt(d) for d in self.dimensions) 80 81 @property 82 def pageNumber(self) -> int: 83 """The current page number.""" 84 return drawBot.pageCount() 85 86 def fillBackground(self, color: color.RawRGB) -> None: 87 """ 88 Fill the background of the page with a color. 89 90 Args: 91 color: RGB color to fill with. 92 93 Example: 94 ``` 95 page.fillBackground((0.5, 0.5, 0.5)) 96 ``` 97 """ 98 self.bgColor = color 99 100 with drawBot.savedState(): 101 drawBot.fill(*color) 102 drawBot.rect(*self.coords) 103 104 self._divide() 105 106 def paginate(self) -> None: 107 """Draw the page number with contextual alignment. Padded to 2 digits.""" 108 folio = str(self.pageNumber).rjust(2, "0") 109 folioBox = KBox.fromText(folio).alignInside( 110 self.frame.coords, ("left", "top") if self.isRightSide else ("right", "top") 111 ) 112 113 if self.bgColor: 114 with drawBot.savedState(): 115 drawBot.fill(*self.bgColor) 116 drawBot.rect( 117 *layout.grow(folioBox.coords, "1mm") 118 ) # Do not mutate folioBox coords directly 119 drawBot.textBox( 120 folio, 121 folioBox.coords, 122 align="left" if self.isRightSide else "right", 123 ) 124 125 def addBodyHeader( 126 self, 127 title: str = None, 128 gap: layout.UnitSource = "5mm", 129 lines: int = 1, 130 mutate: bool = True, 131 ) -> "KBox": 132 """ 133 Add a header box to page body. 134 135 Args: 136 title (optional): Text to write in the header. 137 gap: Space between header and body. 138 lines: Number of lines in the header. Font properties must be set beforehand. 139 mutate: If True, modify the header in place. If False, keep the header unchanged. 140 141 Returns the header box. 142 """ 143 self.header = self.body.addHeader(gap=gap, lines=lines, mutate=mutate) 144 145 if title and self.bgColor: 146 with drawBot.savedState(): 147 drawBot.fill(*self.bgColor) 148 bgBox = ( 149 KBox.fromText(title) 150 .alignInside(self.header.coords, ("left", "top")) 151 .grow(1) 152 ) 153 drawBot.rect(*bgBox.coords) 154 if title: 155 drawBot.textBox(title, self.header.coords) 156 157 return self.header 158 159 def addBodyFooter( 160 self, 161 title: str = None, 162 gap: layout.UnitSource = "5mm", 163 lines: int = 1, 164 mutate: bool = True, 165 ) -> "KBox": 166 """ 167 Add a footer box to page body. 168 169 Args: 170 title (optional): Text to write in the footer. Font props must be set beforehand. 171 gap: Space between footer and body. 172 lines: Number of lines in the footer. Font properties must be set beforehand. 173 mutate: If True, modify the footer in place. If False, keep the footer unchanged. 174 175 Returns the footer box. 176 """ 177 self.footer = self.body.addFooter(gap=gap, lines=lines, mutate=mutate) 178 if title: 179 drawBot.textBox(title, self.footer.coords) 180 181 return self.footer 182 183 def addBleed(self, bleed: int = 7, preview: bool = True) -> None: 184 """ 185 Add crop marks and optional bleed preview. 186 187 Args: 188 bleed: Bleed size. 189 preview: Fill bleed area with black for preview. 190 """ 191 if preview: 192 self.fillBackground((0,)) 193 self.shrink(bleed) 194 self.addCropMarks() 195 if preview: 196 self.fillBackground((1,)) 197 198 self._draftBoxes() 199 200 def xray(self, enabled: bool = True) -> "KPage": 201 """ 202 Xaray all child `classes.c20_box.KBox` instances. 203 204 Args: 205 enabled: Toggle debug mode. 206 207 Returns self for chaining. 208 """ 209 if enabled: 210 coordsList = [self.frame.coords, self.body.coords] 211 if hasattr(self, "header"): 212 coordsList.append(self.header.coords) 213 if hasattr(self, "footer"): 214 coordsList.append(self.footer.coords) 215 layout.xray(coordsList) 216 217 # ? Add page number and side for debugging 218 with drawBot.savedState(): 219 drawBot.font(DEFAULT_FONT, 4) 220 xrayText = f"Page {self.pageNumber}" 221 if self._isSpread: 222 xrayText += f", {'left' if self.isLeftSide else 'right'}" 223 drawBot.text(xrayText, (10, 10)) 224 225 return self 226 227 def _divide(self) -> None: 228 """Draw a spine divider for 2-page PDF view.""" 229 if not self._isSpread or not self.isLeftSide: 230 return 231 232 with drawBot.savedState(): 233 drawBot.fill(None) 234 drawBot.stroke(0, 0.25) 235 drawBot.strokeWidth(3) 236 drawBot.line((self.right, self.bottom), (self.right, self.top)) 237 238 def render(self) -> None: 239 """Render the page to the canvas via `drawBot`.""" 240 drawBot.newPage(*self.dimensions)
8class KPage(KBox): 9 bgColor: color.RawRGB = None 10 11 def __init__( 12 self, 13 size: layout.PageSize = "A5", 14 margin: tuple = 5, 15 aside: int = 0, 16 spread: bool = False, 17 render: bool = True, 18 ): 19 """ 20 Extends `classes.c20_box.KBox` to form a page object. 21 22 Args: 23 size: Page size. See `lib.layout.PageSize`. 24 margin: Margin (implicitly in millimeters). 25 aside: Width of pagination column (implicitly in millimeters). 26 spread: Divide at spine for 2-page PDF view. 27 render: Draw immediately. Otherwise, call `render()` manually. 28 """ 29 super().__init__(size) 30 31 if render: 32 self.render() 33 34 self.aside: int = aside 35 """Width of aside/pagination column.""" 36 self.margin: tuple[int] = layout.mirror(margin) if spread else margin 37 """Margin for the page (mirrored for left side).""" 38 self._isSpread: bool = spread 39 """Used to draw a spine for 2-page PDF view.""" 40 41 self._divide() 42 self._draftBoxes() 43 44 @staticmethod 45 def fromMillimeters(dimensions: tuple[int, int], **kwargs) -> "KPage": 46 """ 47 Alternate constructor to create a KPage from dimensions in millimeters. 48 49 Args: 50 dimensions: Tuple of (width, height) in mm. 51 **kwargs: Additional arguments for the main constructor. 52 """ 53 return KPage(tuple(layout.mm(d) for d in dimensions), **kwargs) 54 55 def _draftBoxes(self) -> None: 56 """Set up the frame and body `classes.c20_box.KBox` objects.""" 57 self.frame = KBox(layout.shrink(frame=self.coords, margin=self.margin)) 58 self.body = KBox( 59 layout.shrinkWidth( 60 frame=self.frame.coords, 61 amount=self.aside, 62 origin="right" if self.isRightSide else "left", 63 implicitUnit="mm", 64 ) 65 ) 66 67 @property 68 def isLeftSide(self) -> bool: 69 """True if the page is on the left side.""" 70 return layout.pageIsEven() 71 72 @property 73 def isRightSide(self) -> bool: 74 """True if the page is on the right side.""" 75 return not self.isLeftSide 76 77 @property 78 def size(self) -> tuple[int, int]: 79 """Page size in millimeters.""" 80 return tuple(layout.pt(d) for d in self.dimensions) 81 82 @property 83 def pageNumber(self) -> int: 84 """The current page number.""" 85 return drawBot.pageCount() 86 87 def fillBackground(self, color: color.RawRGB) -> None: 88 """ 89 Fill the background of the page with a color. 90 91 Args: 92 color: RGB color to fill with. 93 94 Example: 95 ``` 96 page.fillBackground((0.5, 0.5, 0.5)) 97 ``` 98 """ 99 self.bgColor = color 100 101 with drawBot.savedState(): 102 drawBot.fill(*color) 103 drawBot.rect(*self.coords) 104 105 self._divide() 106 107 def paginate(self) -> None: 108 """Draw the page number with contextual alignment. Padded to 2 digits.""" 109 folio = str(self.pageNumber).rjust(2, "0") 110 folioBox = KBox.fromText(folio).alignInside( 111 self.frame.coords, ("left", "top") if self.isRightSide else ("right", "top") 112 ) 113 114 if self.bgColor: 115 with drawBot.savedState(): 116 drawBot.fill(*self.bgColor) 117 drawBot.rect( 118 *layout.grow(folioBox.coords, "1mm") 119 ) # Do not mutate folioBox coords directly 120 drawBot.textBox( 121 folio, 122 folioBox.coords, 123 align="left" if self.isRightSide else "right", 124 ) 125 126 def addBodyHeader( 127 self, 128 title: str = None, 129 gap: layout.UnitSource = "5mm", 130 lines: int = 1, 131 mutate: bool = True, 132 ) -> "KBox": 133 """ 134 Add a header box to page body. 135 136 Args: 137 title (optional): Text to write in the header. 138 gap: Space between header and body. 139 lines: Number of lines in the header. Font properties must be set beforehand. 140 mutate: If True, modify the header in place. If False, keep the header unchanged. 141 142 Returns the header box. 143 """ 144 self.header = self.body.addHeader(gap=gap, lines=lines, mutate=mutate) 145 146 if title and self.bgColor: 147 with drawBot.savedState(): 148 drawBot.fill(*self.bgColor) 149 bgBox = ( 150 KBox.fromText(title) 151 .alignInside(self.header.coords, ("left", "top")) 152 .grow(1) 153 ) 154 drawBot.rect(*bgBox.coords) 155 if title: 156 drawBot.textBox(title, self.header.coords) 157 158 return self.header 159 160 def addBodyFooter( 161 self, 162 title: str = None, 163 gap: layout.UnitSource = "5mm", 164 lines: int = 1, 165 mutate: bool = True, 166 ) -> "KBox": 167 """ 168 Add a footer box to page body. 169 170 Args: 171 title (optional): Text to write in the footer. Font props must be set beforehand. 172 gap: Space between footer and body. 173 lines: Number of lines in the footer. Font properties must be set beforehand. 174 mutate: If True, modify the footer in place. If False, keep the footer unchanged. 175 176 Returns the footer box. 177 """ 178 self.footer = self.body.addFooter(gap=gap, lines=lines, mutate=mutate) 179 if title: 180 drawBot.textBox(title, self.footer.coords) 181 182 return self.footer 183 184 def addBleed(self, bleed: int = 7, preview: bool = True) -> None: 185 """ 186 Add crop marks and optional bleed preview. 187 188 Args: 189 bleed: Bleed size. 190 preview: Fill bleed area with black for preview. 191 """ 192 if preview: 193 self.fillBackground((0,)) 194 self.shrink(bleed) 195 self.addCropMarks() 196 if preview: 197 self.fillBackground((1,)) 198 199 self._draftBoxes() 200 201 def xray(self, enabled: bool = True) -> "KPage": 202 """ 203 Xaray all child `classes.c20_box.KBox` instances. 204 205 Args: 206 enabled: Toggle debug mode. 207 208 Returns self for chaining. 209 """ 210 if enabled: 211 coordsList = [self.frame.coords, self.body.coords] 212 if hasattr(self, "header"): 213 coordsList.append(self.header.coords) 214 if hasattr(self, "footer"): 215 coordsList.append(self.footer.coords) 216 layout.xray(coordsList) 217 218 # ? Add page number and side for debugging 219 with drawBot.savedState(): 220 drawBot.font(DEFAULT_FONT, 4) 221 xrayText = f"Page {self.pageNumber}" 222 if self._isSpread: 223 xrayText += f", {'left' if self.isLeftSide else 'right'}" 224 drawBot.text(xrayText, (10, 10)) 225 226 return self 227 228 def _divide(self) -> None: 229 """Draw a spine divider for 2-page PDF view.""" 230 if not self._isSpread or not self.isLeftSide: 231 return 232 233 with drawBot.savedState(): 234 drawBot.fill(None) 235 drawBot.stroke(0, 0.25) 236 drawBot.strokeWidth(3) 237 drawBot.line((self.right, self.bottom), (self.right, self.top)) 238 239 def render(self) -> None: 240 """Render the page to the canvas via `drawBot`.""" 241 drawBot.newPage(*self.dimensions)
A layout primitive with coordinates, dimensions and spatial utilities.
KPage( size: Union[Literal['A3', 'A3Landscape', 'A4', 'A4Landscape', 'A4Small', 'A4SmallLandscape', 'A5', 'A5Landscape', 'B4', 'B4Landscape', 'B5', 'B5Landscape'], tuple[int, int], NoneType] = 'A5', margin: tuple = 5, aside: int = 0, spread: bool = False, render: bool = True)
11 def __init__( 12 self, 13 size: layout.PageSize = "A5", 14 margin: tuple = 5, 15 aside: int = 0, 16 spread: bool = False, 17 render: bool = True, 18 ): 19 """ 20 Extends `classes.c20_box.KBox` to form a page object. 21 22 Args: 23 size: Page size. See `lib.layout.PageSize`. 24 margin: Margin (implicitly in millimeters). 25 aside: Width of pagination column (implicitly in millimeters). 26 spread: Divide at spine for 2-page PDF view. 27 render: Draw immediately. Otherwise, call `render()` manually. 28 """ 29 super().__init__(size) 30 31 if render: 32 self.render() 33 34 self.aside: int = aside 35 """Width of aside/pagination column.""" 36 self.margin: tuple[int] = layout.mirror(margin) if spread else margin 37 """Margin for the page (mirrored for left side).""" 38 self._isSpread: bool = spread 39 """Used to draw a spine for 2-page PDF view.""" 40 41 self._divide() 42 self._draftBoxes()
Extends classes.c20_box.KBox to form a page object.
Arguments:
- size: Page size. See
lib.layout.PageSize. - margin: Margin (implicitly in millimeters).
- aside: Width of pagination column (implicitly in millimeters).
- spread: Divide at spine for 2-page PDF view.
- render: Draw immediately. Otherwise, call
render()manually.
44 @staticmethod 45 def fromMillimeters(dimensions: tuple[int, int], **kwargs) -> "KPage": 46 """ 47 Alternate constructor to create a KPage from dimensions in millimeters. 48 49 Args: 50 dimensions: Tuple of (width, height) in mm. 51 **kwargs: Additional arguments for the main constructor. 52 """ 53 return KPage(tuple(layout.mm(d) for d in dimensions), **kwargs)
Alternate constructor to create a KPage from dimensions in millimeters.
Arguments:
- dimensions: Tuple of (width, height) in mm.
- **kwargs: Additional arguments for the main constructor.
isLeftSide: bool
67 @property 68 def isLeftSide(self) -> bool: 69 """True if the page is on the left side.""" 70 return layout.pageIsEven()
True if the page is on the left side.
isRightSide: bool
72 @property 73 def isRightSide(self) -> bool: 74 """True if the page is on the right side.""" 75 return not self.isLeftSide
True if the page is on the right side.
size: tuple[int, int]
77 @property 78 def size(self) -> tuple[int, int]: 79 """Page size in millimeters.""" 80 return tuple(layout.pt(d) for d in self.dimensions)
Page size in millimeters.
pageNumber: int
82 @property 83 def pageNumber(self) -> int: 84 """The current page number.""" 85 return drawBot.pageCount()
The current page number.
def
fillBackground(self, color: tuple[float, float, float]) -> None:
87 def fillBackground(self, color: color.RawRGB) -> None: 88 """ 89 Fill the background of the page with a color. 90 91 Args: 92 color: RGB color to fill with. 93 94 Example: 95 ``` 96 page.fillBackground((0.5, 0.5, 0.5)) 97 ``` 98 """ 99 self.bgColor = color 100 101 with drawBot.savedState(): 102 drawBot.fill(*color) 103 drawBot.rect(*self.coords) 104 105 self._divide()
Fill the background of the page with a color.
Arguments:
- color: RGB color to fill with.
Example:
page.fillBackground((0.5, 0.5, 0.5))
def
paginate(self) -> None:
107 def paginate(self) -> None: 108 """Draw the page number with contextual alignment. Padded to 2 digits.""" 109 folio = str(self.pageNumber).rjust(2, "0") 110 folioBox = KBox.fromText(folio).alignInside( 111 self.frame.coords, ("left", "top") if self.isRightSide else ("right", "top") 112 ) 113 114 if self.bgColor: 115 with drawBot.savedState(): 116 drawBot.fill(*self.bgColor) 117 drawBot.rect( 118 *layout.grow(folioBox.coords, "1mm") 119 ) # Do not mutate folioBox coords directly 120 drawBot.textBox( 121 folio, 122 folioBox.coords, 123 align="left" if self.isRightSide else "right", 124 )
Draw the page number with contextual alignment. Padded to 2 digits.
def
addBodyHeader( self, title: str = None, gap: int | float | str | None = '5mm', lines: int = 1, mutate: bool = True) -> classes.c20_box.KBox:
126 def addBodyHeader( 127 self, 128 title: str = None, 129 gap: layout.UnitSource = "5mm", 130 lines: int = 1, 131 mutate: bool = True, 132 ) -> "KBox": 133 """ 134 Add a header box to page body. 135 136 Args: 137 title (optional): Text to write in the header. 138 gap: Space between header and body. 139 lines: Number of lines in the header. Font properties must be set beforehand. 140 mutate: If True, modify the header in place. If False, keep the header unchanged. 141 142 Returns the header box. 143 """ 144 self.header = self.body.addHeader(gap=gap, lines=lines, mutate=mutate) 145 146 if title and self.bgColor: 147 with drawBot.savedState(): 148 drawBot.fill(*self.bgColor) 149 bgBox = ( 150 KBox.fromText(title) 151 .alignInside(self.header.coords, ("left", "top")) 152 .grow(1) 153 ) 154 drawBot.rect(*bgBox.coords) 155 if title: 156 drawBot.textBox(title, self.header.coords) 157 158 return self.header
Add a header box to page body.
Arguments:
- title (optional): Text to write in the header.
- gap: Space between header and body.
- lines: Number of lines in the header. Font properties must be set beforehand.
- mutate: If True, modify the header in place. If False, keep the header unchanged.
Returns the header box.
def
addBleed(self, bleed: int = 7, preview: bool = True) -> None:
184 def addBleed(self, bleed: int = 7, preview: bool = True) -> None: 185 """ 186 Add crop marks and optional bleed preview. 187 188 Args: 189 bleed: Bleed size. 190 preview: Fill bleed area with black for preview. 191 """ 192 if preview: 193 self.fillBackground((0,)) 194 self.shrink(bleed) 195 self.addCropMarks() 196 if preview: 197 self.fillBackground((1,)) 198 199 self._draftBoxes()
Add crop marks and optional bleed preview.
Arguments:
- bleed: Bleed size.
- preview: Fill bleed area with black for preview.
201 def xray(self, enabled: bool = True) -> "KPage": 202 """ 203 Xaray all child `classes.c20_box.KBox` instances. 204 205 Args: 206 enabled: Toggle debug mode. 207 208 Returns self for chaining. 209 """ 210 if enabled: 211 coordsList = [self.frame.coords, self.body.coords] 212 if hasattr(self, "header"): 213 coordsList.append(self.header.coords) 214 if hasattr(self, "footer"): 215 coordsList.append(self.footer.coords) 216 layout.xray(coordsList) 217 218 # ? Add page number and side for debugging 219 with drawBot.savedState(): 220 drawBot.font(DEFAULT_FONT, 4) 221 xrayText = f"Page {self.pageNumber}" 222 if self._isSpread: 223 xrayText += f", {'left' if self.isLeftSide else 'right'}" 224 drawBot.text(xrayText, (10, 10)) 225 226 return self
Xaray all child classes.c20_box.KBox instances.
Arguments:
- enabled: Toggle debug mode.
Returns self for chaining.
def
render(self) -> None:
239 def render(self) -> None: 240 """Render the page to the canvas via `drawBot`.""" 241 drawBot.newPage(*self.dimensions)
Render the page to the canvas via drawBot.
Inherited Members
- classes.c20_box.KBox
- text
- fromDimension
- fromDimensions
- fromText
- position
- dimensions
- coords
- left
- right
- top
- bottom
- middle
- middleX
- middleY
- draw
- move
- grow
- shrink
- offsetFrame
- duplicate
- shrinkWidth
- shrinkHeight
- splitRelative
- splitAbsolute
- partition
- addHeader
- makeGrid
- probeGridItem
- alignInside
- justifyInside
- snapHorizontally
- snapVertically
- addCropMarks