An invoice laid on a letterhead

An invoice laid on a letterhead somebody else made.

Python write_overlay.py 171 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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
"""Lays an invoice on a letterhead somebody else made.

The Python twin of the `write_overlay` example in Rust. The letterhead is a PDF:
it is read, brought in as a form, drawn under the invoice, and nothing about it
is rewritten.

The invoice's labels, and the one label the stand-in letterhead carries, are held in
`Words`, once per language, and `HQF_PDF_LANG` picks which set is drawn. The leader
dots are counted rather than typed, so a longer label in another language still ends
its amount on the same character.

Usage: python examples/write_overlay.py [out.pdf] [letterhead.pdf]
       HQF_PDF_LANG=fr python examples/write_overlay.py
"""

from __future__ import annotations

import sys
from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# 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 = 56

# The invoice's number, and what each billed line comes to. Neither is language.
NUMBER = "1964413"
AMOUNTS = ("4 250.00 EUR", "850.00 EUR", "5 100.00 EUR")

# The letterhead's own name, address and registration number. None of them is language:
# they belong to whoever the letterhead is for.
COMPANY = "ACME Ltd"
COMPANY_ADDRESS = "12 Fleet Street — London EC4Y 1AA"
COMPANY_NUMBER = "123 456 789"

# The size each of the five invoice lines is set at, in the order they are laid down.
SIZES = (16.0, 11.0, 11.0, 11.0, 12.0)


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

    What is not language stays out of it: the invoice's number, the three amounts, and
    the letterhead's name, address and number.
    """

    # What stands on the letterhead before its registration number.
    company_number_label: str
    # What stands before the invoice's number.
    number_label: str
    # The line that gives the day the invoice was issued.
    issued: str
    # What each billed line is for, in the order they are set.
    items: tuple[str, str, str]

    def lines(self) -> list[str]:
        """The five lines the invoice sets, in order.

        Each billed line is padded out with leader dots until it runs to `LINE_CHARS`.
        """
        lines = [f"{self.number_label} {NUMBER}", self.issued]
        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

    def letterhead_line(self) -> str:
        """The second line of the stand-in letterhead: an address, then the number the
        company registers under, under whatever this language calls it."""
        return f"{COMPANY_ADDRESS}{self.company_number_label} {COMPANY_NUMBER}"


# The invoice in English.
ENGLISH = Words(
    company_number_label="Company No.",
    number_label="Invoice No.",
    issued="Issued 14 July 2026",
    items=("Rendering engine development", "VAT at 20 %", "Total due"),
)

# The invoice in French.
FRENCH = Words(
    company_number_label="N° d'entreprise",
    number_label="Facture n°",
    issued="Émise le 14 juillet 2026",
    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 a_letterhead(words: Words) -> bytes:
    """A letterhead, for when the caller has none to hand."""
    doc = hqf_pdf.Document()
    doc.set_license(_licence.licensed())
    font = doc.add_font(hqf_pdf.Font.from_path(_out.DEFAULT_FONT))

    content = hqf_pdf.Content()
    content.save_state()
    content.set_fill(hqf_pdf.Rgb(0.91, 0.94, 0.98))
    content.rect(0.0, 742.0, 595.276, 100.0)
    content.fill()
    content.set_fill(hqf_pdf.Rgb(0.2, 0.35, 0.6))
    content.rect(0.0, 738.0, 595.276, 4.0)
    content.fill()
    content.restore_state()

    content.draw_text(font, 22.0, 56.0, 786.0, COMPANY)
    content.draw_text(font, 9.0, 56.0, 764.0, words.letterhead_line())

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    doc.add_page(page)
    return doc.to_bytes()


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

    # The letterhead: the caller's, or one made here to stand in for it.
    if len(sys.argv) > 2:
        letterhead = open(sys.argv[2], "rb").read()
    else:
        letterhead = a_letterhead(words)

    reader = hqf_pdf.Reader(letterhead)
    print(f"the letterhead has {reader.page_count} page(s)")

    doc = hqf_pdf.Document()
    doc.set_license(_licence.licensed())

    template = doc.import_page(reader, 0)
    print(f"imported it: {template.width:.0f} × {template.height:.0f} points")

    font = doc.add_font(hqf_pdf.Font.from_path(_out.DEFAULT_FONT))

    content = hqf_pdf.Content()
    content.draw_form(template)

    top = 680.0
    for line, size in zip(words.lines(), SIZES):
        content.draw_text(font, size, 56.0, top, line)
        top -= size * 2.0

    page = hqf_pdf.Page(template.width, template.height)
    page.set_content(content)
    template.add_to(page)
    doc.add_page(page)

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


if __name__ == "__main__":
    main()