RTF: \line statt \par fuer Newlines zwischen Runs

Rhinos TextEntity-RTF-Parser rendert \par offenbar nicht als
Zeilenumbruch innerhalb eines Textfeldes. \line dagegen funktioniert
als soft line break.

Globales pending_newlines-Counting ueber alle Runs hinweg: jedes \n
im Text-Run wird zu einem \line, der erst VOR dem naechsten echten
Text emittiert wird. Damit bleiben auch Leerzeilen (mehrere \n
hintereinander) als mehrere \line erhalten.

Why: User-Vergleich Screenshots — WYSIWYG zeigt korrekte Leerzeile
zwischen Heading und Paragraph, Rhino rendert beide Runs auf der
gleichen Zeile konkateniert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 08:38:50 +02:00
parent 91bc03184e
commit 5abf1c0137
+38 -24
View File
@@ -377,36 +377,50 @@ 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).
body_parts = []
pending_newlines = 0 # Newlines die noch emittiert werden muessen
def _emit_text_segment(run, seg):
if not seg: return
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
codes.append("\\cf{}".format(ci) if ci > 0 else "\\cf0")
fsp = run.get("fontSizePx")
if fsp and abs(fsp - BASE_PX) > 0.1:
rtf_fs = max(2, int(round(20.0 * fsp / BASE_PX)))
codes.append("\\fs{}".format(rtf_fs))
else:
codes.append("\\fs20")
codes.append("\\b" if run.get("bold") else "\\b0")
codes.append("\\i" if run.get("italic") else "\\i0")
codes.append("\\ul" if run.get("underline") else "\\ulnone")
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)))
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:
body_parts.append("\\par\n")
if not seg:
# Leere Section (aufeinanderfolgende \\n) → leerer
# Paragraph mit einem Space, damit Rhinos Parser eine
# echte Leerzeile rendert
body_parts.append(" ")
continue
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
codes.append("\\cf{}".format(ci) if ci > 0 else "\\cf0")
fsp = run.get("fontSizePx")
if fsp and abs(fsp - BASE_PX) > 0.1:
rtf_fs = max(2, int(round(20.0 * fsp / BASE_PX)))
codes.append("\\fs{}".format(rtf_fs))
else:
codes.append("\\fs20")
codes.append("\\b" if run.get("bold") else "\\b0")
codes.append("\\i" if run.get("italic") else "\\i0")
codes.append("\\ul" if run.get("underline") else "\\ulnone")
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)))
pending_newlines += 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 ")
# ────────────────────────────────────────────────────────────────
# PASS 2: RTF-Header mit JETZT vollstaendigen Tables + Body