From 91bc03184e7b13fad1a7f0c1b8ebdd09cf3917a1 Mon Sep 17 00:00:00 2001 From: karim Date: Thu, 21 May 2026 08:15:26 +0200 Subject: [PATCH] RTF: Two-Pass fonttbl + Leerzeilen-Fix + wrap-Diagnose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Log-Analyse vom User: RTF preview zeigte '{\\fonttbl{\\f0\\fnil\\fcharset0 Georgia;}}' ABER der Body nutzt '\\f1\\cf1\\fs20\\b ... Lorem ...' \\f1 ist NICHT in der fonttbl! → Rhino fallback auf Default \\cf1 ist NICHT in der colortbl (es gibt keine)! → ignoriert Root cause: ich hatte die fonttbl/colortbl eagerly geschrieben BEVOR die Runs ueberhaupt verarbeitet waren. fonts/colors-Listen wurden waehrend des Body-Loops um neue Eintraege erweitert, aber der schon-emittierte Header sah die nie. Fix: Two-Pass. PASS 1: Body bauen, dabei font_idx/color_idx aufrufen → fonts/colors- Listen werden komplett gefuellt. PASS 2: RTF-Header schreiben mit JETZT vollstaendigen Tables, dann Body anhaengen. Plus Leerzeilen: aufeinanderfolgende \\n in den Runs erzeugen jetzt ein leeres Paragraph mit Space (" "), damit Rhinos Parser den \\par nicht mit dem naechsten kollabiert. Resultat: Lorem...\\par \\par ← Leerzeile (echtes Space im leeren Paragraph) Consectetur... Plus Frame-Wrap-Diagnostic: "[TEXT-EDITOR] wrap: width=... applied_attr= FormatWidth/TextWidth/None" damit ich sehen kann ob die Wrap-Property ueberhaupt gesetzt wird in dieser Rhino-Version. Co-Authored-By: Claude Opus 4.7 --- rhino/text_editor.py | 61 +++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/rhino/text_editor.py b/rhino/text_editor.py index a204196..5422bf3 100644 --- a/rhino/text_editor.py +++ b/rhino/text_editor.py @@ -198,15 +198,21 @@ class TextEditorBridge(panel_base.BaseBridge): st.get("underline")) # 3. Text-Wrap im Frame — NACH dem Content damit es nicht - # durch RichText-Set zurueckgesetzt wird - for attr in ("FormatWidth", "TextWidth"): + # durch RichText-Set zurueckgesetzt wird. Beide Setter + # versuchen (verschiedene Rhino-Versions-APIs). + applied_w = None + for attr in ("FormatWidth", "TextWidth", "MaskWidth"): try: - setattr(te, attr, width); break + setattr(te, attr, width) + applied_w = attr + break except Exception: pass try: te.TextIsWrapped = True except Exception: try: te.TextWrap = True except Exception: pass + print("[TEXT-EDITOR] wrap: width={} applied_attr={}".format( + width, applied_w)) # 4. Frame um den Text + Mask-Margin frame_kind = (st.get("frame") or "none").lower() @@ -339,9 +345,11 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20): nontrivial = True; break if not nontrivial: return None - # Font-Tabelle + Color-Tabelle + # ──────────────────────────────────────────────────────────────── + # PASS 1: Runs verarbeiten + Fonts/Colors sammeln + RTF-Bodies bauen + # ──────────────────────────────────────────────────────────────── fonts = [default_font] - colors = [] # nur explizit gesetzte (Index 1+) + colors = [] def font_idx(f): if not f: return 0 if f not in fonts: fonts.append(f) @@ -353,19 +361,6 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20): colors.append(rgb) return len(colors) - parts = ["{\\rtf1\\ansi\\ansicpg1252\\deff0"] - parts.append("{\\fonttbl") - for i, f in enumerate(fonts): - parts.append("{{\\f{}\\fnil\\fcharset0 {};}}".format(i, f)) - parts.append("}") - if colors: - parts.append("{\\colortbl;") - for r, g, b in colors: - parts.append("\\red{}\\green{}\\blue{};".format(r, g, b)) - parts.append("}") - parts.append("\\pard") - - # Frontend rendert 1m = 100px; Standard-Run-Size ist base_size_m * 100 BASE_PX = max(1.0, base_size_m * 100.0) def _escape_no_par(s): @@ -382,16 +377,19 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20): out.append("\\u{}?".format(v)) return "".join(out) - # State-Tracking — alle Codes werden pro Run IMMER emittiert - # (inkl. Reset-Codes wie \\b0 \\i0). Kein {}-Grouping. Klassisches - # Inline-RTF wie Word/Wordpad es ausgibt. + body_parts = [] for run in runs: raw = run.get("text") or "" segments = raw.split("\n") for i, seg in enumerate(segments): if i > 0: - parts.append("\\par\n") - if not seg: continue + 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 @@ -408,8 +406,23 @@ 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") - parts.append("{} {}".format("".join(codes), _escape_no_par(seg))) + body_parts.append("{} {}".format("".join(codes), _escape_no_par(seg))) + # ──────────────────────────────────────────────────────────────── + # PASS 2: RTF-Header mit JETZT vollstaendigen Tables + Body + # ──────────────────────────────────────────────────────────────── + parts = ["{\\rtf1\\ansi\\ansicpg1252\\deff0"] + parts.append("{\\fonttbl") + for i, f in enumerate(fonts): + parts.append("{{\\f{}\\fnil\\fcharset0 {};}}".format(i, f)) + parts.append("}") + if colors: + parts.append("{\\colortbl;") + for r, g, b in colors: + parts.append("\\red{}\\green{}\\blue{};".format(r, g, b)) + parts.append("}") + parts.append("\\pard") + parts.extend(body_parts) parts.append("}") return "".join(parts)