From 29699b5eda41ad9503a76d709c8665e171331945 Mon Sep 17 00:00:00 2001 From: karim Date: Thu, 21 May 2026 13:01:02 +0200 Subject: [PATCH] RTF: Groups + \par zwischen Segmenten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zurueck zur Groups-Form ({...} pro Run), aber mit Two-Pass-fonttbl das Pass 1 bereits korrekt populiert. Zwischen den Groups \\par fuer Paragraph-Breaks — der einzige Linebreak-Token den Rhinos TextEntity-Parser tatsaechlich rendert. \\line aus dem letzten Versuch wurde von Rhino ignoriert. pending_pars-Counter sammelt Newlines ueber Run-Grenzen hinweg, sodass auch mehrere aufeinanderfolgende \\n (= Leerzeilen) erhalten bleiben. Fuehrender \\par wird unterdrueckt (first_emitted-Flag), damit der Text nicht mit einer Leerzeile beginnt. Why: Per-Run-Groups isolieren Font-State (Fix fuer "letzte Schrift dominiert"), waehrend \\par die Mehrzeiligkeit liefert die Rhino versteht. Two-Pass garantiert dass fonttbl alle benutzten Fonts enthaelt. Co-Authored-By: Claude Opus 4.7 --- rhino/text_editor.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/rhino/text_editor.py b/rhino/text_editor.py index 9a7aeae..395e3bc 100644 --- a/rhino/text_editor.py +++ b/rhino/text_editor.py @@ -377,15 +377,14 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20): out.append("\\u{}?".format(v)) return "".join(out) - # Globales Newline-Tracking ueber ALLE Runs hinweg — ein `\n` - # zwischen Runs (= eigener Newline-Run) ergibt EINEN \\line. - # Mehrere aufeinanderfolgende `\n` ergeben entsprechend mehrere - # \\line in Reihe (= Leerzeile). + # Ansatz: jedes Text-Segment in eigene {}-Group mit lokalen Codes. + # Zwischen Segmenten (und ueber Run-Grenzen hinweg) \\par fuer + # Paragraph-Breaks. So bleibt Per-Run-Formatting (Groups isolieren + # State) UND Mehrzeiligkeit (\\par ist Rhinos echter Linebreak). body_parts = [] - pending_newlines = 0 # Newlines die noch emittiert werden muessen + pending_pars = 0 # Wieviele \\par muessen noch vor naechstem Segment - def _emit_text_segment(run, seg): - if not seg: return + def _emit_group(run, seg): codes = [] codes.append("\\f{}".format(font_idx(run.get("font") or default_font))) ci = color_idx(run.get("color")) if run.get("color") else 0 @@ -402,25 +401,29 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20): if run.get("sup"): codes.append("\\super") elif run.get("sub"): codes.append("\\sub") else: codes.append("\\nosupersub") - body_parts.append("{} {}".format("".join(codes), _escape_no_par(seg))) + body_parts.append("{{{} {}}}".format("".join(codes), _escape_no_par(seg))) + first_emitted = False for run in runs: raw = run.get("text") or "" - # Jede \n im Text → \\line; alle \\line werden VOR der naechsten - # Text-Section emittiert. Damit bleiben auch leere Zeilen erhalten. segments = raw.split("\n") for i, seg in enumerate(segments): if i > 0: - pending_newlines += 1 + pending_pars += 1 if seg: - # Pending Newlines emittieren bevor Text kommt - for _ in range(pending_newlines): - body_parts.append("\\line ") - pending_newlines = 0 - _emit_text_segment(run, seg) - # Trailing newlines auch noch emittieren - for _ in range(pending_newlines): - body_parts.append("\\line ") + # Pending \\par vor dem naechsten Inhalt — aber nur wenn + # schon mal Inhalt da war (sonst fuehrender \\par bringt + # Leerzeile am Anfang) + if first_emitted: + for _ in range(pending_pars): + body_parts.append("\\par ") + pending_pars = 0 + _emit_group(run, seg) + first_emitted = True + # Trailing \\par auch noch emittieren (= Leerzeilen am Ende) + if first_emitted: + for _ in range(pending_pars): + body_parts.append("\\par ") # ──────────────────────────────────────────────────────────────── # PASS 2: RTF-Header mit JETZT vollstaendigen Tables + Body