#!/usr/bin/env python3 """Produit une lettre commerciale Arcodange en PDF, depuis le gabarit maison. POURQUOI CE SCRIPT EXISTE. Deux choses ne se font pas à la main sans se tromper. 1. `weasyprint` NE SAIT PAS rendre Apple Color Emoji : c'est un format bitmap `sbix` qu'il ignore, et les trois emblèmes 🏹💻🪽 sortent en carrés vides. Il faut les incorporer en images. Elles sont ici en PNG, rendues une fois pour toutes par `test/emoji2png.ts` (Chromium, lui, lit la police système). 2. Un gabarit qui porterait ces images en base64 pèserait 400 Ko dont 97 % de charabia. On les garde en fichiers et on les incorpore À LA GÉNÉRATION, pour que le gabarit reste relisible et que le PDF reste autonome. Usage : python3 correspondance/lettre.py corps.html sortie.pdf \ --titre "Contracts and invoices — cycles M1 to M4" \ --date "24 August 2026" --prenom Evan \ --destinataire "Evan Sforzo" --fonction "Chief Executive Officer" \ --societe "Kissmetrics Inc." \ --adresse "2850 34th Street North, 307 — St. Petersburg, Florida 33713 — United States" \ --copie "Hendrik Rootering" \ --pied "Arcodange × Kissmetrics Inc. — 24 August 2026" `corps.html` ne contient que le corps : les

,

, , encadrés. L'en-tête, le bloc destinataire, la signature et le pied de page viennent du gabarit. Sans `--copie`, la ligne « cc » disparaît. """ import argparse, base64, pathlib, re, subprocess, sys ICI = pathlib.Path(__file__).parent GABARIT = ICI / "lettre.template.html" EMBLEMES = {"ARC": "arc.png", "LAPTOP": "laptop.png", "AILE": "aile.png"} def data_uri(chemin: pathlib.Path) -> str: return "data:image/png;base64," + base64.b64encode(chemin.read_bytes()).decode() def main() -> int: a = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) a.add_argument("corps"); a.add_argument("sortie") for champ in ("titre", "date", "prenom", "destinataire", "fonction", "societe", "adresse", "pied"): a.add_argument(f"--{champ}", required=True) a.add_argument("--copie", default="") a.add_argument("--garder-html", action="store_true", help="conserve le HTML intermédiaire à côté du PDF") o = a.parse_args() html = GABARIT.read_text(encoding="utf-8") for cle, fichier in EMBLEMES.items(): p = ICI / "emblemes" / fichier if not p.exists(): print(f"emblème manquant : {p}\n" f" le régénérer : cd test && deno run -A emoji2png.ts 🏹 💻 🪽", file=sys.stderr) return 2 html = html.replace("{{" + cle + "}}", data_uri(p)) for cle, val in (("TITRE", o.titre), ("DATE", o.date), ("PRENOM", o.prenom), ("DESTINATAIRE", o.destinataire), ("FONCTION", o.fonction), ("SOCIETE", o.societe), ("ADRESSE", o.adresse), ("COPIE", o.copie), ("PIED", o.pied)): html = html.replace("{{" + cle + "}}", val) # Pas de destinataire en copie : on retire la ligne entière, pas seulement # son contenu, sinon il reste un « cc » orphelin. if not o.copie: html = re.sub(r'
\s*]*>cc\s* \s*', "", html) # Le corps remplace tout ce qui sépare le sous-titre de la signature. corps = pathlib.Path(o.corps).read_text(encoding="utf-8") deb = html.index("

") fin = html.index('
') entete = html[deb:html.index("

Dear ")] html = html[:deb] + entete + f"

Dear {o.prenom},

\n\n" + corps + "\n\n" + html[fin:] tmp = pathlib.Path(o.sortie).with_suffix(".html") tmp.write_text(html, encoding="utf-8") r = subprocess.run(["weasyprint", str(tmp), o.sortie]) if r.returncode == 0 and not o.garder_html: tmp.unlink() if r.returncode == 0: pages = subprocess.run(["pdfinfo", o.sortie], capture_output=True, text=True).stdout n = next((l.split()[1] for l in pages.splitlines() if l.startswith("Pages")), "?") print(f"ok — {o.sortie} ({n} page(s))") if n not in ("1", "2"): print(" ATTENTION : au-delà de deux pages, une lettre d'affaires se lit mal. " "Resserrer le corps plutôt que la typographie.", file=sys.stderr) return r.returncode if __name__ == "__main__": raise SystemExit(main())