write_page_labels.py

The Python file of the “Custom page labels” example. Pages numbered by their labels rather than their position.

Python 158 lines

What this example is for

The number you type to go to a page ought to be the number printed on that page. In any document with a preface it is not: the reader asks for page 12, reading software counts pages from the front and shows them page 4. On a legal file or a technical manual, that is a reference nobody can follow.

What this example shows

  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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""Writes a document whose pages are numbered by their labels, not by their position.

The Python twin of the `write_page_labels` example in Rust: the same document,
through the binding rather than through the library directly.

Whether the labels work is not a question the bytes can answer: it is answered by the
page box of reading software, which is the one place they show. So every page here says
what reading software ought to be showing for it — open the file and the two either
agree or they do not.

What each page says is written in the language `HQF_PDF_LANG` names. The labels
themselves are not: `i`, `1` and `A-1` are what reading software shows in its page box,
they come from the numbering style and the prefix the file writes, and a page that
translated them would no longer say what the file asks for.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# How far in from the left edge of the sheet every line is set, in points.
LEFT = 72.0

# How many sheets the document runs to, which each page says of itself.
SHEETS = 7

# The size each line of a page is set at, in points, in the order they are drawn.
SIZES = [12.0, 9.0, 9.0, 14.0, 20.0]

# Where the baseline of each line sits, in points up from the foot of the sheet, in the
# order they are drawn.
BASELINES = [772.0, 752.0, 738.0, 700.0, 640.0]


@dataclass(frozen=True)
class Words:
    """Every word a page is written in, in one language.

    What reading software shows in its page box is not among them: `i`, `1` and `A-1`
    come from the numbering style and the prefix the file writes, and the page carries
    them so that the two can be read against each other.
    """

    # What the document is called, in what the file says of itself.
    title: str
    # The three lines at the head of every page, which say what page labels are and
    # where they show.
    head: tuple[str, str, str]
    # What stands before a sheet's number.
    sheet: str
    # What stands between a sheet's number and the count of them.
    of: str
    # What stands before the label reading software ought to be showing.
    shows_as: str


# The pages in English.
ENGLISH = Words(
    title="A document numbered by its labels",
    head=(
        "Page labels number the pages for the reader.",
        "Roman numerals for the front matter, arabic for the body, "
        "a prefix for the annex.",
        "The label shows in reading software's page box, not the sheet number below.",
    ),
    sheet="Sheet",
    of="of",
    shows_as="Reading software shows this page as:",
)

# The pages in French.
FRENCH = Words(
    title="Un document numéroté par ses étiquettes",
    head=(
        "Les étiquettes de page numérotent les pages pour le lecteur.",
        "Chiffres romains pour les pages liminaires, arabes pour le corps, "
        "un préfixe pour l'annexe.",
        "L'étiquette s'affiche dans la case du logiciel de lecture, pas le numéro de "
        "feuille ci-dessous.",
    ),
    sheet="Feuille",
    of="sur",
    shows_as="Le logiciel de lecture l'affiche ainsi :",
)

# 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 runs the document is made of: the page each starts on counting from zero, the
# label it is numbered by, and what reading software ought to show for each of its
# pages.
RUNS = [
    (0, hqf_pdf.PageLabel(hqf_pdf.LabelStyle.LowerRoman), ["i", "ii"]),
    (2, hqf_pdf.PageLabel(hqf_pdf.LabelStyle.Decimal), ["1", "2", "3"]),
    (5, hqf_pdf.PageLabel(hqf_pdf.LabelStyle.Decimal, prefix="A-"), ["A-1", "A-2"]),
]


def lines(words: Words, sheet: int, shown: str) -> list[str]:
    """What one page says: what page labels are, which sheet it is, and its label."""
    return [
        *words.head,
        f"{words.sheet} {sheet} {words.of} {SHEETS}",
        f"{words.shows_as} {shown}",
    ]


def page(
    handle: hqf_pdf.FontHandle, words: Words, sheet: int, shown: str
) -> hqf_pdf.Page:
    """A page that says which sheet it is and what reading software ought to call it."""
    content = hqf_pdf.Content()
    for line, size, top in zip(lines(words, sheet, shown), SIZES, BASELINES):
        content.draw_text(handle, size, LEFT, top, line)

    result = hqf_pdf.Page.a4()
    result.set_content(content)
    return result


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("page-labels.pdf", language)).stem)

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    document.set_info("Title", words.title)

    handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))

    sheet = 1
    for start, label, shown_as in RUNS:
        for shown in shown_as:
            document.add_page(page(handle, words, sheet, shown))
            sheet += 1
        document.label_pages(start, label)

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


if __name__ == "__main__":
    main()