Custom page labels

Pages numbered by their labels rather than their position.

Python write_page_labels.py 157 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
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
"""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 a reader, which is the one place they show. So every page here says
what a reader 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 a reader 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 a reader 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 a reader 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 a reader's page box, not the sheet number below.",
    ),
    sheet="Sheet",
    of="of",
    shows_as="A reader 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 lecteur, pas le numéro de "
        "feuille ci-dessous.",
    ),
    sheet="Feuille",
    of="sur",
    shows_as="Un lecteur affiche cette page 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 a reader 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 a reader 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()