diff --git a/correspondance/README.md b/correspondance/README.md new file mode 100644 index 0000000..7ba3383 --- /dev/null +++ b/correspondance/README.md @@ -0,0 +1,79 @@ +# Correspondance commerciale — le format maison + +Le gabarit des lettres qu'Arcodange adresse à ses clients et prospects. Fixé le +24 août 2026 avec le dossier KissMetrics, et validé par l'opérateur : « on peut +se souvenir de ce format pour les communications avec KM et futurs +prospects/clients ». + +## Produire une lettre + +```bash +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 porte que le corps — les `

`, `

`, ``, encadrés. +L'en-tête, le bloc destinataire, la signature et le pied viennent du gabarit. +Sans `--copie`, la ligne « cc » disparaît entièrement. + +Le script vérifie le nombre de pages et proteste au-delà de deux : une lettre +d'affaires qui déborde ne se lit pas. Resserrer le CORPS, jamais la typographie. + +## Les choix, et pourquoi + +**Charter pour le texte, Optima pour les titres.** Charter a été dessinée par +Matthew Carter pour tenir le petit corps là où d'autres se délitent — elle reste +lisible à l'écran comme sur papier bon marché. Optima lui donne un contrepoint +humaniste sans raideur. Les deux sont incorporées au PDF : le rendu est le même +chez le destinataire, quelle que soit sa machine. Chiffres elzéviriens activés, +pour que les montants dans le texte s'alignent au lieu de faire des bâtons. + +**Deux encadrés, deux usages.** Le gris (`.calme`) porte une question ou une +demande — elle ne doit pas se noyer dans un paragraphe. Le rouge sourd +(`.encart`) porte ce qui doit être lu même en diagonale. **Un seul par lettre** ; +deux, et plus rien ne ressort. + +**Les tableaux portent un ``.** Sans lui, un tableau qui se coupe entre +deux pages perd son en-tête et devient illisible. Avec, il se répète. + +**Les trois emblèmes 🏹💻🪽** viennent du site — *« Gabriel 🪽 Radureau, pour 🏹 +réussir vos projets 💻 »*. L'arc et l'ange sont dans le nom lui-même. Ils +figurent en marque sous le mot-marque, et l'aile seule entre prénom et nom dans +la signature. + +## Le piège des emblèmes + +> [!IMPORTANT] +> **`weasyprint` ne sait pas rendre Apple Color Emoji.** C'est un format bitmap +> `sbix` qu'il ignore : les trois emblèmes sortent en carrés vides, sans erreur +> ni avertissement. Vérifié le 24/08/2026. + +D'où `emblemes/*.png`, rendus une fois pour toutes par `test/emoji2png.ts`, qui +passe par Chromium — lui lit la police système. Pour les régénérer ou en ajouter : + +```bash +cd test && deno run -A emoji2png.ts ../correspondance/emblemes 🏹 💻 🪽 +``` + +Les PNG sont incorporés en base64 **à la génération**, pas dans le gabarit : un +gabarit de 400 Ko dont 97 % de charabia ne se relit pas. Le PDF produit, lui, +reste autonome. + +## Ce que le format ne fait pas + +Il ne remplace pas le message d'accompagnement. La lettre est le document qu'on +joint ; le mot sur Slack ou par courriel reste séparé, plus court, et dit +pourquoi on écrit. Les deux doivent rester d'accord — vérifier que la lettre ne +mentionne aucune pièce absente de l'envoi. + +## Exemple de référence + +`1_DOCUMENTS/prospects/KissMetrics/relances/2026-08-24_lettre_KM.html` — la +lettre du 24/08/2026, dont ce gabarit est extrait. Elle se régénère à +l'identique, au mot près, ce qui est le test du gabarit. diff --git a/correspondance/emblemes/aile.png b/correspondance/emblemes/aile.png new file mode 100644 index 0000000..5d6da54 Binary files /dev/null and b/correspondance/emblemes/aile.png differ diff --git a/correspondance/emblemes/arc.png b/correspondance/emblemes/arc.png new file mode 100644 index 0000000..261f1c6 Binary files /dev/null and b/correspondance/emblemes/arc.png differ diff --git a/correspondance/emblemes/laptop.png b/correspondance/emblemes/laptop.png new file mode 100644 index 0000000..1e2bdbd Binary files /dev/null and b/correspondance/emblemes/laptop.png differ diff --git a/correspondance/lettre.py b/correspondance/lettre.py new file mode 100755 index 0000000..e5e5571 --- /dev/null +++ b/correspondance/lettre.py @@ -0,0 +1,96 @@ +#!/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()) diff --git a/correspondance/lettre.template.html b/correspondance/lettre.template.html new file mode 100644 index 0000000..f1b47c7 --- /dev/null +++ b/correspondance/lettre.template.html @@ -0,0 +1,102 @@ + +{{TITRE}} — Arcodange + + +
+
ARCODANGE
+
+
+ SARL au capital de 1 000 € — SIREN 999 657 455 R.C.S. Évry
+ 73 boulevard de l'Yerres, 91000 Évry-Courcouronnes, France
+ VAT FR00 999 657 455 — gabrielradureau@arcodange.fr +
+
+ +
+ To
+ {{DESTINATAIRE}} — {{FONCTION}}, {{SOCIETE}}
+ {{ADRESSE}}
+ cc  {{COPIE}} +
+ +

{{TITRE}}

+

{{DATE}}

+ +

Dear {{PRENOM}},

+ + + +

1 — Une section

+ +

Corps de texte en Charter. Les montants et les points qui +portent la décision se mettent en gras ; les citations en italique.

+ +
+

Une question posée franchement se met dans +un encadré gris — elle ne doit pas se noyer dans un paragraphe.

+
+ +
+

L'encadré rouge sourd est réservé à ce qui doit être lu +même en diagonale. Un par lettre, pas deux.

+
+ +
+

Gabriel Radureau

+

Gérant — Arcodange

+
+ + diff --git a/test/emoji2png.ts b/test/emoji2png.ts new file mode 100644 index 0000000..5430865 --- /dev/null +++ b/test/emoji2png.ts @@ -0,0 +1,16 @@ +// Rend un emoji en PNG transparent via Chromium, qui sait lire Apple Color +// Emoji là où weasyprint échoue (format bitmap sbix, non géré). +import { chromium } from "playwright"; +const [outDir, ...emojis] = Deno.args; +const b = await chromium.launch({ headless: true }); +const p = await (await b.newContext({ deviceScaleFactor: 8 })).newPage(); +for (const [i, e] of emojis.entries()) { + await p.setContent( + `${e}`); + const el = p.locator("#g"); + const f = `${outDir}/emoji-${i + 1}.png`; + await el.screenshot({ path: f, omitBackground: true }); + console.log(` ${e} -> ${f}`); +} +await b.close();