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)
class KPage(classes.c20_box.KBox):
  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.
bgColor: tuple[float, float, float] = None
aside: int

Width of aside/pagination column.

margin: tuple[int]

Margin for the page (mirrored for left side).

@staticmethod
def fromMillimeters(dimensions: tuple[int, int], **kwargs) -> KPage:
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 addBodyFooter( self, title: str = None, gap: int | float | str | None = '5mm', lines: int = 1, mutate: bool = True) -> classes.c20_box.KBox:
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

Add a footer box to page body.

Arguments:
  • title (optional): Text to write in the footer. Font props must be set beforehand.
  • gap: Space between footer and body.
  • lines: Number of lines in the footer. Font properties must be set beforehand.
  • mutate: If True, modify the footer in place. If False, keep the footer unchanged.

Returns the footer 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.
def xray(self, enabled: bool = True) -> KPage:
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.