Text poured into columns

One long passage poured into three columns, each fitting the lines its height allows and handing the rest to the next.

Python write_columns.py 128 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 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
 76
 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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""Pours one long passage into three columns, flowing from the foot of each to
the head of the next.

The Python twin of the `write_columns` example in Rust. The passage is broken
once to the column width, then set column by column: each column fits as many of
the lines as its height allows, draws that slice with `Lines.sub`, and the next
column takes up where it stopped. A thin rule outlines each column so the text
can be seen reaching its foot and carrying on.

The passage is held in `Words`, once per language, and `HQF_PDF_LANG` picks which one
is poured. The measure, the gap and the height are not language: they are the same
three columns whoever reads the page.

Usage: python examples/write_columns.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_columns.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf


@dataclass(frozen=True)
class Words:
    """Every word the page draws, in one language."""

    # The passage poured through the columns, long enough to reach the third.
    passage: str


# The page in English.
ENGLISH = Words(
    passage=(
        "A flow too long for one column is set column by column. Each column takes "
        "as many of the broken lines as its height allows, draws them, and hands the "
        "rest to the column beside it, which takes up exactly where the last one "
        "stopped. The words are broken once, to the width one column has, so every "
        "column is set to the same measure. A line that would fall below a column's "
        "foot opens the next column instead, and the reading runs down one column "
        "and on into the top of the next with no line lost and none set twice. This "
        "is what sets a newspaper's text, a report's two columns, or the narrow "
        "measure a wide page reads best in."
    ),
)

# The page in French.
FRENCH = Words(
    passage=(
        "Un texte trop long pour une seule colonne se compose colonne par colonne. "
        "Chaque colonne prend autant de lignes coupées que sa hauteur le permet, les "
        "dessine, et passe le reste à sa voisine, qui reprend là où la "
        "précédente s'est arrêtée. Les mots ne sont coupés qu'une fois, à la largeur "
        "d'une colonne, et toutes sont composées à la même mesure. Une ligne qui "
        "tomberait sous le pied d'une colonne ouvre la suivante, et la lecture descend "
        "une colonne puis remonte en haut de la prochaine sans qu'aucune ligne soit "
        "perdue ni composée deux fois. C'est ainsi que se composent le "
        "texte d'un journal, les deux colonnes d'un rapport, ou la colonne étroite "
        "qui rend une page large lisible."
    ),
)

# Every language the example is written in. A language is added by writing its own set
# of words and naming it here.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH}

# The left edge of the first column.
LEFT = 70.0
# The top edge every column shares.
TOP = 720.0
# The width of one column.
COLUMN = 145.0
# The gap between one column and the next.
GAP = 25.0
# How far down from TOP a column reaches.
HEIGHT = 130.0
# How many columns the passage is poured through.
COLUMNS = 3


def main() -> None:
    language = _language.from_environment()
    words = _language.words_of(WORDS, language)

    # A named file is written as named; the default one carries the language, so the two
    # languages do not overwrite each other in `tmp/`.
    out = _out.output_path(Path(_language.file_name("columns.pdf", language)).stem)

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    flow = hqf_pdf.TextFlow(handle, 10.0, leading=14.0, align=hqf_pdf.Align.Justify)
    lines = flow.break_lines(words.passage, COLUMN)

    content = hqf_pdf.Content()
    start = 0
    x = LEFT
    for _ in range(COLUMNS):
        taken = flow.fit(lines, HEIGHT, start)
        flow.draw(content, lines.sub(start, start + taken), x, TOP, COLUMN)

        content.save_state()
        content.set_stroke(hqf_pdf.Rgb.gray(0.8))
        content.set_line_width(0.5)
        content.rect(x, TOP - HEIGHT, COLUMN, HEIGHT)
        content.stroke()
        content.restore_state()

        start += taken
        x += COLUMN + GAP

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    document.add_page(page)

    written = document.write(out)
    print(f"wrote {out}: {written} bytes")


if __name__ == "__main__":
    main()