RTF: Groups + \par zwischen Segmenten

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 13:01:02 +02:00
parent 5abf1c0137
commit 29699b5eda
+22 -19
View File
@@ -377,15 +377,14 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20):
out.append("\\u{}?".format(v)) out.append("\\u{}?".format(v))
return "".join(out) return "".join(out)
# Globales Newline-Tracking ueber ALLE Runs hinweg — ein `\n` # Ansatz: jedes Text-Segment in eigene {}-Group mit lokalen Codes.
# zwischen Runs (= eigener Newline-Run) ergibt EINEN \\line. # Zwischen Segmenten (und ueber Run-Grenzen hinweg) \\par fuer
# Mehrere aufeinanderfolgende `\n` ergeben entsprechend mehrere # Paragraph-Breaks. So bleibt Per-Run-Formatting (Groups isolieren
# \\line in Reihe (= Leerzeile). # State) UND Mehrzeiligkeit (\\par ist Rhinos echter Linebreak).
body_parts = [] 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): def _emit_group(run, seg):
if not seg: return
codes = [] codes = []
codes.append("\\f{}".format(font_idx(run.get("font") or default_font))) codes.append("\\f{}".format(font_idx(run.get("font") or default_font)))
ci = color_idx(run.get("color")) if run.get("color") else 0 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") if run.get("sup"): codes.append("\\super")
elif run.get("sub"): codes.append("\\sub") elif run.get("sub"): codes.append("\\sub")
else: codes.append("\\nosupersub") 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: for run in runs:
raw = run.get("text") or "" 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") segments = raw.split("\n")
for i, seg in enumerate(segments): for i, seg in enumerate(segments):
if i > 0: if i > 0:
pending_newlines += 1 pending_pars += 1
if seg: if seg:
# Pending Newlines emittieren bevor Text kommt # Pending \\par vor dem naechsten Inhalt — aber nur wenn
for _ in range(pending_newlines): # schon mal Inhalt da war (sonst fuehrender \\par bringt
body_parts.append("\\line ") # Leerzeile am Anfang)
pending_newlines = 0 if first_emitted:
_emit_text_segment(run, seg) for _ in range(pending_pars):
# Trailing newlines auch noch emittieren body_parts.append("\\par ")
for _ in range(pending_newlines): pending_pars = 0
body_parts.append("\\line ") _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 # PASS 2: RTF-Header mit JETZT vollstaendigen Tables + Body