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"))
|
st.get("underline"))
|
||||||
|
|
||||||
# 3. Text-Wrap im Frame — NACH dem Content damit es nicht
|
# 3. Text-Wrap im Frame — NACH dem Content damit es nicht
|
||||||
# durch RichText-Set zurueckgesetzt wird
|
# durch RichText-Set zurueckgesetzt wird. Beide Setter
|
||||||
for attr in ("FormatWidth", "TextWidth"):
|
# versuchen (verschiedene Rhino-Versions-APIs).
|
||||||
|
applied_w = None
|
||||||
|
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
|
||||||
try:
|
try:
|
||||||
setattr(te, attr, width); break
|
setattr(te, attr, width)
|
||||||
|
applied_w = attr
|
||||||
|
break
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
try: te.TextIsWrapped = True
|
try: te.TextIsWrapped = True
|
||||||
except Exception:
|
except Exception:
|
||||||
try: te.TextWrap = True
|
try: te.TextWrap = True
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
print("[TEXT-EDITOR] wrap: width={} applied_attr={}".format(
|
||||||
|
width, applied_w))
|
||||||
|
|
||||||
# 4. Frame um den Text + Mask-Margin
|
# 4. Frame um den Text + Mask-Margin
|
||||||
frame_kind = (st.get("frame") or "none").lower()
|
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
|
nontrivial = True; break
|
||||||
if not nontrivial: return None
|
if not nontrivial: return None
|
||||||
|
|
||||||
# Font-Tabelle + Color-Tabelle
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
# PASS 1: Runs verarbeiten + Fonts/Colors sammeln + RTF-Bodies bauen
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
fonts = [default_font]
|
fonts = [default_font]
|
||||||
colors = [] # nur explizit gesetzte (Index 1+)
|
colors = []
|
||||||
def font_idx(f):
|
def font_idx(f):
|
||||||
if not f: return 0
|
if not f: return 0
|
||||||
if f not in fonts: fonts.append(f)
|
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)
|
colors.append(rgb)
|
||||||
return len(colors)
|
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)
|
BASE_PX = max(1.0, base_size_m * 100.0)
|
||||||
|
|
||||||
def _escape_no_par(s):
|
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))
|
out.append("\\u{}?".format(v))
|
||||||
return "".join(out)
|
return "".join(out)
|
||||||
|
|
||||||
# State-Tracking — alle Codes werden pro Run IMMER emittiert
|
body_parts = []
|
||||||
# (inkl. Reset-Codes wie \\b0 \\i0). Kein {}-Grouping. Klassisches
|
|
||||||
# Inline-RTF wie Word/Wordpad es ausgibt.
|
|
||||||
for run in runs:
|
for run in runs:
|
||||||
raw = run.get("text") or ""
|
raw = run.get("text") or ""
|
||||||
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:
|
||||||
parts.append("\\par\n")
|
body_parts.append("\\par\n")
|
||||||
if not seg: continue
|
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 = []
|
||||||
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
|
||||||
@@ -408,8 +406,23 @@ 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")
|
||||||
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("}")
|
parts.append("}")
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user