RTF: Two-Pass fonttbl + Leerzeilen-Fix + wrap-Diagnose
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 <noreply@anthropic.com>
This commit is contained in:
+37
-24
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user