Skip to content

xpuz.pdf ¤

Module implementing PDF export functionality.

PDF ¤

PDF(
    cwrapper: CrosswordWrapper,
    starting_word_positions: List[Tuple[int]],
    starting_word_matrix: List[List[int]],
    definitions_a: List[Dict[int, Tuple[str]]],
    definitions_d: List[Dict[int, Tuple[str]]],
)

Provides the functionality to create a PDF with the an empty crossword grid and its definitions, as well as the completed crossword grid.

Disclaimer

Cannot draw crosswords that contains complex glyphs, such as Mandarin and Japanese, due to limitations with cairo.

Parameters:

Name Type Description Default
cwrapper CrosswordWrapper

The crossword wrapper.

required
starting_word_positions List[Tuple[int]] required
starting_word_matrix List[List[int]] required
definitions_a List[Dict[int, Tuple[str]]] required
definitions_d List[Dict[int, Tuple[str]]] required
Source code in src/xpuz/pdf.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(
    self,
    cwrapper: CrosswordWrapper,
    starting_word_positions: List[Tuple[int]],
    starting_word_matrix: List[List[int]],
    definitions_a: List[Dict[int, Tuple[str]]],
    definitions_d: List[Dict[int, Tuple[str]]],
) -> None:
    """Initialise the crossword data and the crossword wrapper object.

    Args:
        cwrapper: The crossword wrapper.
        starting_word_positions: 
            [read this function](utils.md#xpuz.utils._interpret_cword_data)
        starting_word_matrix: 
            [read this function](utils.md#xpuz.utils._interpret_cword_data)
        definitions_a: 
            [read this function](utils.md#xpuz.utils._interpret_cword_data)
        definitions_d: 
            [read this function](utils.md#xpuz.utils._interpret_cword_data)
    """

    self.cwrapper = cwrapper
    self.crossword = self.cwrapper.crossword
    self.grid: List[List[str]] = self.crossword.grid
    self.dimensions: int = self.crossword.dimensions
    self.drawn: bool = False
    self.display_name = self.cwrapper.display_name

    self.starting_word_positions = starting_word_positions
    self.starting_word_matrix = starting_word_matrix
    self.definitions_a, self.definitions_d = definitions_a, definitions_d
    self.definitions_a_backlog, self.definitions_d_backlog = [], []
    self.backlog_inserted: bool = False
    self.display_name_answer = self.display_name + f" - {_('Answers')}"

    # Calculating relative side length of each cell, then using that value
    # to calculate the remaining measurements and font sizes
    self.cell_dim: float = (PDF_HEIGHT - 2 * PDF_MARGIN) / self.dimensions
    self.grid_dim: float = self.dimensions * self.cell_dim
    self.num_label_fontsize = self.cell_dim * 0.3
    self.cell_fontsize = self.cell_dim * 0.8

_draw_all ¤

_draw_all() -> None

Driver function to draw the PDF.

Source code in src/xpuz/pdf.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def _draw_all(self) -> None:
    """Driver function to draw the PDF."""
    self._draw_grid()
    self._draw_display_name(self.display_name)

    self._s.show_page()
    self._c = Context(self._s)
    self._draw_definitions()

    self._s.show_page()
    self._c = Context(self._s)
    self._draw_grid(with_answers=True)
    self._draw_display_name(self.display_name_answer)

    self._s.finish()

_draw_cell_letter ¤

_draw_cell_letter(row: int, col: int) -> None

Draw the letter at self.grid[row][col].

Parameters:

Name Type Description Default
row int

The row, used as a reference to determine the drawing location.

required
col int

The column, used as a reference to determine the drawing location.

required
Source code in src/xpuz/pdf.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def _draw_cell_letter(self, row: int, col: int) -> None:
    """Draw the letter at `self.grid[row][col]`.

    Args:
        row: The row, used as a reference to determine the drawing location.
        col: The column, used as a reference to determine the drawing location.
    """
    self._c.set_source_rgb(0, 0, 0)
    self._c.set_font_size(self.cell_fontsize)
    self._set_font_face(weight=FONT_WEIGHT_NORMAL)

    letter = self.crossword.grid[row][col]
    text_extents = self._c.text_extents(letter)

    x_position = (
        col * self.cell_dim + (self.cell_dim - text_extents.width) / 2.2
    )
    y_position = row * self.cell_dim + self.cell_fontsize * 1.2

    self._c.move_to(x_position, y_position)
    self._c.show_text(letter)

_draw_definitions ¤

_draw_definitions() -> None

Driver function to draw both columns of a crossword's definitions.

Source code in src/xpuz/pdf.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
def _draw_definitions(self) -> None:
    """Driver function to draw both columns of a crossword's definitions."""
    self._draw_definitions_col(
        self.definitions_a,
        _("Across"),
        PDF_MARGIN,
        PDF_MARGIN * 1.5,
        self.definitions_a_backlog,
    )
    self._draw_definitions_col(
        self.definitions_d,
        _("Down"),
        PDF_WIDTH / 2 + 20,
        PDF_MARGIN * 1.5,
        self.definitions_d_backlog,
    )

    # Some definitions could not fit, so recurse ``self._draw_definitions``
    if not self.backlog_inserted and (
        self.definitions_a_backlog or self.definitions_d_backlog
    ):
        self.backlog_inserted = True
        self._s.show_page()
        self._c = Context(self._s)
        # Update definitions arrays to their respective backlogs
        self.definitions_a = self.definitions_a_backlog
        self.definitions_d = self.definitions_d_backlog
        self._draw_definitions()

_draw_definitions_col ¤

_draw_definitions_col(
    definitions: List[str],
    dir_title: str,
    start_x: float,
    start_y: float,
    backlog: Union[List[None], List[str]],
) -> None

Draw either the across or down definitions column.

Disclaimer

Does not provide wrapping for clues.

Parameters:

Name Type Description Default
definitions List[str]

The definitions for either the across or down column.

required
dir_title str

The title for the column.

required
start_x float

The x position to begin drawing the column.

required
start_y float

The y position to begin drawing the column.

required
backlog Union[List[None], List[str]]

The definitions that could not fit on the existing page. It is empty in the first call of this method.

required
Source code in src/xpuz/pdf.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
def _draw_definitions_col(
    self,
    definitions: List[str],
    dir_title: str,
    start_x: float,
    start_y: float,
    backlog: Union[List[None], List[str]],
) -> None:
    """Draw either the across or down definitions column.

    DISCLAIMER: Does not provide wrapping for clues.

    Args:
        definitions: The definitions for either the across or down column.
        dir_title: The title for the column.
        start_x: The x position to begin drawing the column.
        start_y: The y position to begin drawing the column.
        backlog: The definitions that could not fit on the existing page.
                 It is empty in the first call of this method.
    """
    definitions_x: float = (
        start_x + ((PDF_WIDTH - 2 * PDF_MARGIN) / 2 - 40) / 2
    )

    # Draw "Across" or "Down"
    self._c.set_source_rgb(0, 0, 0)
    self._c.set_font_size(FONTSIZE_DIR_TITLE)
    self._set_font_face()
    self._c.move_to(
        definitions_x - self._c.text_extents(dir_title).width / 2, start_y
    )
    self._c.show_text(dir_title)

    y = start_y + FONTSIZE_DIR_TITLE + 60
    self._c.set_font_size(FONTSIZE_DEF)
    self._set_font_face(weight=FONT_WEIGHT_NORMAL)

    for i, definition in enumerate(definitions):
        # These definitions would go off the page, so skip them and append
        # them to ``backlog``
        if i + 1 > PAGE_DEF_MAX and not self.backlog_inserted:
            backlog.append(definition)
            continue

        for number, (word, clue) in definition.items():
            text = f"{number}. {clue}"
            self._c.move_to(
                definitions_x - self._c.text_extents(text).width / 2, y
            )
            self._c.show_text(text)
            y += FONTSIZE_DEF * 2

_draw_display_name ¤

_draw_display_name(name: str) -> None

Draw name at the top of the current page.

Parameters:

Name Type Description Default
name str

The display name.

required
Source code in src/xpuz/pdf.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def _draw_display_name(self, name: str) -> None:
    """Draw ``name`` at the top of the current page.

    Args:
        name: The display name.
    """
    self._c.set_source_rgb(0, 0, 0)

    self._c.set_font_size(60.0)
    self._set_font_face()
    self._c.move_to(
        (self.grid_dim - self._c.text_extents(self.display_name).width)
        / 2,
        -50,
    )
    self._c.show_text(name)

_draw_grid ¤

_draw_grid(with_answers: bool = False) -> None

Draw the crossword grid in the center of the page.

Parameters:

Name Type Description Default
with_answers bool

Whether to draw this current part of the crossword PDF with answers in the grid or not.

False
Source code in src/xpuz/pdf.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def _draw_grid(self, with_answers: bool = False) -> None:
    """Draw the crossword grid in the center of the page.

    Args:
        with_answers: Whether to draw this current part of the crossword PDF
                      with answers in the grid or not.
    """
    offset_x: float = (PDF_WIDTH - 2 * PDF_MARGIN - self.grid_dim) / 2
    offset_y: float = (PDF_HEIGHT - 2 * PDF_MARGIN - self.grid_dim) / 2
    self._c.translate(PDF_MARGIN, PDF_MARGIN)
    # Begin drawing after allowing space for the margin in the prior line
    self._c.translate(offset_x, offset_y)

    # This structure is very similar to the Jinja2 in ``app/templates/index.html``
    for row in range(self.dimensions):
        for col in range(self.dimensions):
            if self.grid[row][col] == EMPTY:  # Void cell
                self._c.set_source_rgb(0, 0, 0)
                self._c.rectangle(
                    col * self.cell_dim,
                    row * self.cell_dim,
                    self.cell_dim,
                    self.cell_dim,
                )
                self._c.fill()

            else:  # This cell has text
                self._c.set_source_rgb(1, 1, 1)
                self._c.rectangle(
                    col * self.cell_dim,
                    row * self.cell_dim,
                    self.cell_dim,
                    self.cell_dim,
                )
                self._c.fill()

                if with_answers:  # Also draw the letter in the cell
                    self._draw_cell_letter(row, col)

            # This is the start of a word, draw a number label
            if (row, col) in self.starting_word_positions:
                self._draw_number_label(row, col)

    self._draw_grid_lines()  # Separate the cells

_draw_grid_lines ¤

_draw_grid_lines() -> None

Draw lines in between all of the cells.

Source code in src/xpuz/pdf.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def _draw_grid_lines(self) -> None:
    """Draw lines in between all of the cells."""
    self._c.set_source_rgb(0, 0, 0)

    for row in range(
        self.dimensions + 1
    ):  # Account for far right and bottom edges by adding 1
        self._c.move_to(0, row * self.cell_dim)
        self._c.line_to(
            self.dimensions * self.cell_dim, row * self.cell_dim
        )
        self._c.stroke()

    for col in range(self.dimensions + 1):
        self._c.move_to(col * self.cell_dim, 0)
        self._c.line_to(
            col * self.cell_dim, self.dimensions * self.cell_dim
        )
        self._c.stroke()

_draw_number_label ¤

_draw_number_label(row: int, col: int) -> None

Draw a number label in the top left hand corner of the cell at row and col.

Parameters:

Name Type Description Default
row int

The row, used as a reference to determine the drawing location.

required
col int

The column, used as a reference to determine the drawing location.

required
Source code in src/xpuz/pdf.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def _draw_number_label(self, row: int, col: int) -> None:
    """Draw a number label in the top left hand corner of the cell at ``row``
    and ``col``.

    Args:
        row: The row, used as a reference to determine the drawing location.
        col: The column, used as a reference to determine the drawing location.
    """
    self._c.set_source_rgb(0, 0, 0)

    self._c.set_font_size(self.num_label_fontsize)
    self._set_font_face(weight=FONT_WEIGHT_NORMAL)
    self._c.move_to(
        col * self.cell_dim + self.num_label_fontsize / 4,
        row * self.cell_dim + self.num_label_fontsize,
    )
    self._c.show_text(str(self.starting_word_matrix[row][col]))

_on_finish ¤

_on_finish() -> None

Provide information to the user after drawing is finished.

Source code in src/xpuz/pdf.py
103
104
105
106
107
108
109
110
111
112
def _on_finish(self) -> None:
    """Provide information to the user after drawing is finished."""
    if not self.drawn:
        return GUIHelper.show_messagebox(pdf_write_err=True)
    else:
        fails = self.crossword.fails
        if fails > 0:
            return GUIHelper.show_messagebox(fails, pdf_write_success=True)
        else:
            return GUIHelper.show_messagebox(pdf_write_success=True)

_set_font_face ¤

_set_font_face(
    family: str = "Arial",
    slant: str = FONT_SLANT_NORMAL,
    weight: str = FONT_WEIGHT_BOLD,
) -> None

Set the font that is used by pycairo.

Parameters:

Name Type Description Default
family str

The font family.

'Arial'
slant str

The font slant.

FONT_SLANT_NORMAL
weight str

The font weight.

FONT_WEIGHT_BOLD
Source code in src/xpuz/pdf.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def _set_font_face(
    self,
    family: str = "Arial",
    slant: str = FONT_SLANT_NORMAL,
    weight: str = FONT_WEIGHT_BOLD,
) -> None:
    """Set the font that is used by `pycairo`.

    Args:
        family: The font family.
        slant: The font slant.
        weight: The font weight.
    """
    self._c.select_font_face(family, slant, weight)

write ¤

write() -> None

Begin the PDF writing process.

Source code in src/xpuz/pdf.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def write(self) -> None:
    """Begin the PDF writing process."""
    filepath = _get_saveas_filename(
        _("Select a destination to download your PDF to"),
        self.display_name,
        ".pdf",
        [("PDF files", "*.pdf")],
    )
    if not filepath:
        return
    if not filepath.endswith(".pdf"):
        filepath += ".pdf"

    try:
        self._s: PDFSurface = PDFSurface(filepath, PDF_WIDTH, PDF_HEIGHT)
        self._c: Context = Context(self._s)
        self._c.set_line_width(1)

        self._draw_all()
        self.drawn = True

    except Exception:
        pass

    self._on_finish()