Des polices rangées dans le fichier

Du texte écrit avec une police d'écriture rangée à l'intérieur du fichier, réduite aux seules lettres employées.

Python write_text.py 147 lignes
  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
"""Sets text in an embedded font, and writes the result to disk.

The Python twin of the `write_text` example in Rust: the same PDF, the same
words, through the binding rather than through the library directly.

`pdftotext` must be able to pull the words back out of the result. If it cannot,
the map back to characters is wrong and the text is unsearchable and uncopyable
even though the page looks right.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set is
drawn. The number, the issuer and the three amounts are the same in every language.

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

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The invoice's number.
NUMBER = "1964413"

# Who issued it.
ISSUER = "HQF Development, Cabriès"

# What each of the three billed lines comes to, in the order they are set.
AMOUNTS = ("4 250.00 EUR", "850.00 EUR", "5 100.00 EUR")

# How many characters a billed line runs to: its label, a space, the leader dots, a
# space, then its amount. The dots take whatever is left, so the amounts end on the same
# character however long the label before them is.
LINE_CHARS = 57


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

    What is not language stays out of it: the invoice's number, who issued it and the
    three amounts read the same wherever the page is read.
    """

    # The document's title, which a reader shows over the page.
    title: str
    # What stands before the invoice's number.
    number_label: str
    # The line that gives the day it was issued and the term it falls due in.
    dates: str
    # What each billed line is called, in the order AMOUNTS prices them.
    items: tuple[str, str, str]

    def lines(self) -> list[str]:
        """Return the lines the sample page sets, which pdftotext must find again.

        They are not all spelled out of the ASCII range for the look of it: an accent and
        a dash are two glyphs a wrong ToUnicode map loses first, and a page that only
        ever said "Total" would never catch it.
        """
        lines = [f"{self.number_label} {NUMBER}{ISSUER}", self.dates]
        for item, amount in zip(self.items, AMOUNTS):
            dots = max(0, LINE_CHARS - len(item) - len(amount) - 2)
            lines.append(f"{item} {'.' * dots} {amount}")
        return lines


# The page in English.
ENGLISH = Words(
    title="hqf-pdf text sample",
    number_label="Invoice No.",
    dates="Issued 14 July 2026 — due within 30 days",
    items=("Rendering engine development", "VAT at 20 %", "Total due"),
)

# The page in French.
FRENCH = Words(
    title="hqf-pdf, échantillon de texte",
    number_label="Facture n°",
    dates="Émise le 14 juillet 2026 — payable sous 30 jours",
    items=("Développement du moteur de rendu", "TVA 20 %", "Total à payer"),
)


# 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}


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

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

    font = hqf_pdf.Font.from_path(_out.font_path())
    print(
        f"font: {font.postscript_name} "
        f"({font.glyph_count} glyphs, {font.units_per_em} units per em)"
    )

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())  # no notice, so the text stands alone
    document.set_info("Title", words.title)
    handle = document.add_font(font)

    content = hqf_pdf.Content()
    content.begin_text()
    content.set_font(handle, 14.0)
    content.text_position(60.0, 760.0)
    for line in lines:
        content.show_glyphs(handle.glyphs(line))
        # Move down one line. The step is relative to the start of the previous one.
        content.text_position(0.0, -28.0)
    content.end_text()

    # A rule under the total, positioned from the measured width of the text, to prove
    # the metrics agree with what was drawn.
    width = handle.measure(lines[-1], 14.0)
    content.set_line_width(0.8)
    baseline = 760.0 - 28.0 * 5.0
    content.rect(60.0, baseline, width, 0.0)
    content.stroke()

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

    data = document.to_bytes()
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_bytes(data)

    print(f"wrote {out}: {len(data)} bytes")
    print(f"the last line measures {width:.1f} pt at 14 pt")


if __name__ == "__main__":
    main()