write_soft_hyphen.py

Die Python-Datei des Beispiels „Weiche Trennstriche und geschützte Leerzeichen“. Eine Blocksatzspalte, zweimal gesetzt: die langen Wörter an ihren weichen Trennstrichen mit sichtbarem Strich umbrochen oder ganz gelassen, und eine Zahl, die ein geschütztes Leerzeichen zusammenhält.

Python 188 Zeilen

Wofür dieses Beispiel da ist

Eine schmale Blocksatzspalte wird locker, sobald ihre Wörter lang sind: Die Leerräume dehnen sich, weiße Gassen laufen die Seite hinab, und das Auge fällt hinein, statt zu lesen. Was die Seite heute richtig setzt und nichts kostet, ist, im Text selbst anzugeben, wo ein bestimmtes Wort getrennt werden darf (eine automatische Silbentrennung mit einem Wörterbuch für jede Sprache ist für eine künftige Version geplant).

Was dieses Beispiel zeigt

  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
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
"""Sets one justified paragraph in a narrow column twice: above without its soft
hyphens, below with them, so the breaks and the hyphens they draw can be seen
against the loose lines above.

The Python twin of the `write_soft_hyphen` example in Rust. The text carries soft
hyphens (U+00AD) inside its long words and a no-break space (U+00A0) inside the
figure "1 200 EUR". The upper setting is the same text with the soft hyphens taken
out, so only the breaking differs between the two; the no-break space holds the
figure together in both.

The paragraph and the two headings are held in `Words`, once per language, and
`HQF_PDF_LANG` picks which set is drawn. Every language has to carry its own soft
hyphens, in words its own spelling allows to break: a paragraph translated without
them would show nothing.

Usage: python examples/write_soft_hyphen.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_soft_hyphen.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."""

    # What stands over the setting whose soft hyphens have been taken out.
    without: str
    # What stands over the setting that keeps them.
    with_: str
    # The paragraph, with soft hyphens marking where its long words may break and a
    # no-break space inside the figure so it never falls across two lines. Each language
    # marks the breaks its own spelling allows.
    hyphenated: str


# The page in English.
ENGLISH = Words(
    without="Without soft hyphens: justify opens the lines to fill the column.",
    with_="With soft hyphens: the long words break, each break drawing a hyphen.",
    hyphenated=(
        "Our inter­national sub­scription, billed at "
        "1 200 EUR, covers unlimited docu­ments, prior­ity "
        "support and quarterly state­ments."
    ),
)

# The page in French.
FRENCH = Words(
    without=(
        ("Sans césures conditionnelles : la justification écarte les mots pour remplir"
         " la colonne.")
    ),
    with_=(
        ("Avec césures conditionnelles : les mots longs se coupent, chaque coupure "
         "ajoutant un trait d’union.")
    ),
    hyphenated=(
        ("Notre abonne­ment inter­national, factu­ré 1 200 EUR, comprend un nombre "
         "illi­mité de docu­ments, une assis­tance prio­ritaire et des rele­vés "
         "trimes­triels.")
    ),
)

# The page in Dutch.
DUTCH = Words(
    without="Zonder zachte afbreekstreepjes: uitvullen trekt de regels open om de kolom te vullen.",
    with_="Met zachte afbreekstreepjes: de lange woorden breken, en elke breuk tekent een streepje.",
    hyphenated=(
        "Ons inter­nationale abonne­ment, gefac­tureerd tegen EUR 1.200, omvat "
        "onbe­perkte docu­menten, voorrangs­ondersteuning en driemaan­delijkse "
        "overzichten."
    ),
)

# The page in Portuguese.
PORTUGUESE = Words(
    without="Sem hífenes condicionais: justificar abre as linhas para encher a coluna.",
    with_="Com hífenes condicionais: as palavras longas partem-se, e cada corte desenha um hífen.",
    hyphenated=(
        "A nossa assi­natura inter­nacional, faturada a 1 200 EUR, cobre docu­mentos "
        "ilimi­tados, assis­tência priori­tária e extratos trimes­trais."
    ),
)

# The page in Italian.
ITALIAN = Words(
    without="Senza trattini facoltativi: la giustificazione allarga le righe per riempire la colonna.",
    with_="Con trattini facoltativi: le parole lunghe si spezzano, e ogni taglio disegna un trattino.",
    hyphenated=(
        "Il nostro abbona­mento inter­nazionale, fatturato a 1.200 EUR, copre "
        "docu­menti illimi­tati, assi­stenza priori­taria ed estratti trime­strali."
    ),
)

# The page in Spanish.
SPANISH = Words(
    without="Sin guiones condicionales: justificar abre las líneas para llenar la columna.",
    with_="Con guiones condicionales: las palabras largas se cortan, y cada corte dibuja un guion.",
    hyphenated=(
        "Nuestra sus­crip­ción inter­nacional, factu­rada a 1 200 EUR, cubre "
        "docu­mentos ilimi­tados, asis­tencia prio­ritaria y extractos trimes­trales."
    ),
)

# The page in German.
GERMAN = Words(
    without="Ohne weiche Trennstriche: Der Blocksatz öffnet die Zeilen, um die Spalte zu füllen.",
    with_="Mit weichen Trennstrichen: Die langen Wörter brechen, jeder Bruch zeichnet einen Bindestrich.",
    hyphenated=(
        "Unser inter­nationales Abon­nement, abge­rechnet mit 1 200 EUR, umfasst "
        "unbe­grenzte Doku­mente, bevor­zugten Support und viertel­jährliche "
        "Konto­auszüge."
    ),
)


# 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, _language.GERMAN: GERMAN, _language.SPANISH: SPANISH, _language.ITALIAN: ITALIAN, _language.PORTUGUESE: PORTUGUESE, _language.DUTCH: DUTCH}

# The column both settings are broken to, narrow enough that the long words reach its
# edge.
COLUMN = 150.0

# The left edge of both settings, stacked one above the other.
X = 80.0


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

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

    content = hqf_pdf.Content()

    # The upper setting is the same text with its soft hyphens removed, so the two
    # panels differ only in whether the words may break.
    plain = words.hyphenated.replace("­", "")

    for top, label, paragraph in (
        (760.0, words.without, plain),
        (600.0, words.with_, words.hyphenated),
    ):
        heading = hqf_pdf.TextFlow(handle, 8.0)
        heading_lines = heading.break_lines(label, 400.0)
        heading.draw(content, heading_lines, X, top, 400.0)

        text_top = top - 20.0
        flow = hqf_pdf.TextFlow(handle, 11.0, leading=15.0, align=hqf_pdf.Align.Justify)
        lines = flow.break_lines(paragraph, COLUMN)
        flow.draw(content, lines, X, text_top, COLUMN)

        # The column's edge, so the lines can be read against it.
        height = flow.height(lines)
        content.save_state()
        content.set_stroke(hqf_pdf.Rgb.gray(0.8))
        content.set_line_width(0.5)
        content.rect(X, text_top - height, COLUMN, height)
        content.stroke()
        content.restore_state()

    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()