#!/usr/bin/env python3 r""" Complex Textbox Showcase — generates textbox.docx exercising 10 advanced textbox (WPS drawing / w:txbxContent) scenarios for testing officecli compatibility: basic border+fill, multi-paragraph rich text, a nested table, 45-degree rotation + gradient, vertical text, rounded rectangle + shadow, side-by-side metric cards, a borderless transparent box, fixed-height overflow, and z-order stacking (behindDoc + translucent overlay). Scenarios 1, 4, 5, 6, 7, 8, 9, 10 use the HIGH-LEVEL `add --type textbox` command: the box (fill, border, gradient, rotation, vertical text, geometry, corner radius, shadow, no-fill/no-line, wrap, position, z-order) is one `add` item, and its inner paragraphs are formatted with `set` on the `/body/textbox[N]/p[M]` paths (bare bold/italic/color/size/align keys — they apply to every run in the paragraph) plus more `add` paragraph items. Only scenarios 2 and 3 stay on `raw-set` with pre-authored DrawingML — they exercise surface the high-level command does not expose (per-run mixed formatting inside one paragraph, and a nested table). See textbox.md for the breakdown. SDK twin of textbox.sh (officecli CLI). Both produce an equivalent textbox.docx. This twin drives the **officecli Python SDK** (`pip install officecli-sdk`): one resident is started and every item is shipped over the named pipe in a single `doc.batch(...)` round-trip. Each item is the same `{"command", ...}` dict you'd put in an `officecli batch` list — `add` items carry `parent`/`type`/`props`, `set` items carry `path`/`props`, and `raw-set` items carry `part`/`xpath`/`action`/`xml`. Because a batch runs in order and each textbox is appended to the body, the textbox indices are deterministic: S1=1, S2=2, S3=3, S4=4, S5=5, S6=6, S7=7/8/9, S8=10, S9=11, S10=12/13. Usage: pip install officecli-sdk # plus the `officecli` binary on PATH python3 textbox.py """ import os import sys # --- locate the SDK: prefer an installed `officecli-sdk`, else the in-repo copy try: import officecli # pip install officecli-sdk except ImportError: sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "sdk", "python")) import officecli FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "textbox.docx") def para(text, **props): """One `add paragraph` item in batch-shape.""" return {"command": "add", "parent": "/body", "type": "paragraph", "props": {"text": text, **props}} def textbox(xml): """One `raw-set` item that injects a textbox paragraph before the body sectPr — the exact `officecli raw-set /document --xpath //w:body/w:sectPr --action insertbefore --xml ...` the .sh runs.""" return {"command": "raw-set", "part": "/document", "xpath": "//w:body/w:sectPr", "action": "insertbefore", "xml": xml} # ------------------------------------------------- high-level textbox helpers # All scenarios except 2 and 3 are built from these batch items instead of raw XML. def tb_add(**props): """`add --type textbox` batch item — creates the box + its first paragraph.""" return {"command": "add", "parent": "/body", "type": "textbox", "props": props} def tb_fmt(idx, p, **props): """`set` a textbox inner paragraph (bold/italic/color/size/align apply to all runs in that paragraph). idx = the textbox's document order, p = 1-based paragraph.""" return {"command": "set", "path": f"/body/textbox[{idx}]/p[{p}]", "props": props} def tb_para(idx, text): """Append a paragraph inside textbox #idx.""" return {"command": "add", "parent": f"/body/textbox[{idx}]", "type": "paragraph", "props": {"text": text}} def card_items(idx, title, big, label, fill, accent, offset): """Batch items for one S7 metric card (textbox #idx): rounded box + three centred paragraphs (accent title / big number / grey label).""" return [ tb_add(text=title, geometry="roundRect", width="4.72cm", height="3.89cm", fill=fill, wrap="none", textAnchor="center", **{"line.color": accent, "line.width": "1pt", "hRelative": "column", "anchor.x": offset}), tb_fmt(idx, 1, align="center", bold="true", color=accent, size="14"), tb_para(idx, big), tb_fmt(idx, 2, align="center", size="24"), tb_para(idx, label), tb_fmt(idx, 3, align="center", color="888888", size="9"), ] # ---------------------------------------------------------------- raw textbox XML # Scenarios 2 and 3 — the verbatim ... wrappers kept on raw-set. SCENARIO_2 = r''' 0 0 Rich Text Content Bold | Italic | Underline | Strikethrough Red small Green large Blue bold italic Yellow highlight Green highlight white -- Right-aligned quote ''' SCENARIO_3 = r''' 0 0 Table inside textbox: Name Department Score John Engineering 95 Sarah Marketing 78 * Table nested inside a textbox ''' print(f"Building {FILE} ...") with officecli.create(FILE, "--force") as doc: items = [ # ==================== Intro ==================== para("Complex Textbox Examples", style="Heading1", align="center"), para("The following contains multiple complex textbox scenarios for " "testing textbox behavior under various conditions."), # Scenario 1 (HIGH-LEVEL, textbox[1]): solid fill + border + 2 paragraphs para("Scenario 1: Basic Textbox (with border and background)", style="Heading2"), tb_add(text="Basic Textbox", width="15cm", height="3.33cm", fill="E6F3FF", **{"line.color": "0070C0", "line.width": "2pt"}, wrap="topAndBottom", textAnchor="top"), tb_fmt(1, 1, align="center", bold="true", color="0070C0", size="14"), tb_para(1, "This is a textbox with a blue border and light blue background. " "It contains a centered title and a normal paragraph."), # Scenario 2: Multi-paragraph Rich Text Textbox para("Scenario 2: Multi-paragraph Rich Text Textbox", style="Heading2"), textbox(SCENARIO_2), # Scenario 3: Textbox with Nested Table para("Scenario 3: Textbox with Nested Table", style="Heading2"), textbox(SCENARIO_3), # Scenario 4 (HIGH-LEVEL, textbox[4]): rotation + gradient (comma stop list) para("Scenario 4: Rotated Textbox (45 degrees)", style="Heading2"), tb_add(text="Rotated 45", width="6.67cm", height="3.33cm", rotation="45", **{"fill.gradient": "FF6B6B,FFE66D", "line.color": "C0392B", "line.width": "1.5pt", "anchor.x": "4.17cm"}, wrap="topAndBottom", textAnchor="center", hRelative="column"), tb_fmt(4, 1, align="center", bold="true", color="FFFFFF", size="14"), tb_para(4, "Gradient + Rotation"), tb_fmt(4, 2, align="center", color="FFFFFF"), # Scenario 5 (HIGH-LEVEL, textbox[5]): vertical text flow para("Scenario 5: Vertical Text Textbox", style="Heading2"), tb_add(text="Vertical text content", width="2.22cm", height="6.67cm", fill="FFF0F5", **{"line.color": "8B0000", "line.width": "1pt"}, textDirection="eaVert", wrap="topAndBottom", textAnchor="top"), tb_fmt(5, 1, align="center", bold="true", color="8B0000", size="18"), # Scenario 6 (HIGH-LEVEL, textbox[6]): roundRect + corner radius + shadow. # shadow=true's default blur/dist/dir/color/alpha match Word's preset. para("Scenario 6: Rounded Rectangle Textbox", style="Heading2"), tb_add(text="Rounded Rectangle + Shadow", width="15cm", height="4.17cm", geometry="roundRect", cornerRadius="16667", fill="E8F5E9", shadow="true", wrap="topAndBottom", textAnchor="center", **{"line.color": "2E7D32", "line.width": "2.25pt"}), tb_fmt(6, 1, align="center", bold="true", color="2E7D32", size="15"), tb_para(6, "This is a rounded rectangle textbox with an outer shadow effect."), tb_fmt(6, 2, align="center"), tb_para(6, "Uses geometry=roundRect + cornerRadius for rounded corners"), tb_fmt(6, 3, align="center", italic="true", color="666666"), # Scenario 7 (HIGH-LEVEL, textbox[7..9]): three floating metric cards. # Each add is its own host paragraph, so the cards sit at a slight # vertical stagger (the raw-XML original packed all three into one). para("Scenario 7: Side-by-side Textboxes (Card Layout)", style="Heading2"), *card_items(7, "Card A", "128", "Daily Visits", "E3F2FD", "1565C0", "0cm"), *card_items(8, "Card B", "56", "New Orders", "FFF3E0", "E65100", "5.28cm"), *card_items(9, "Card C", "99.8%", "Uptime", "E8F5E9", "2E7D32", "10.56cm"), # Scenario 8 (HIGH-LEVEL, textbox[10]): fill=none + line.color=none → # a fully invisible container (both sentinels emit a:noFill). para("Scenario 8: Borderless Transparent Textbox", style="Heading2"), tb_add(text="Borderless transparent text", width="11.11cm", height="2.22cm", fill="none", wrap="topAndBottom", textAnchor="center", **{"line.color": "none", "hRelative": "column", "anchor.x": "1.39cm"}), tb_fmt(10, 1, align="center", italic="true", size="22", color="AAAAAA"), # Scenario 9 (HIGH-LEVEL, textbox[11]): fixed height, autoFit omitted para("Scenario 9: Text Overflow Textbox", style="Heading2"), tb_add(text="Line 1: This is a fixed-height textbox with too much text to " "test overflow behavior.", width="15cm", height="1.67cm", fill="FCE4EC", **{"line.color": "C62828", "line.width": "1pt"}, wrap="topAndBottom", textAnchor="top"), tb_fmt(11, 1, bold="true", color="C62828"), tb_para(11, "Line 2: In real usage, the textbox height is limited but content can be long."), tb_para(11, "Line 3: Word usually auto-expands the textbox height, but fixed height may truncate."), tb_para(11, "Line 4: This line may be truncated or overflow, depending on bodyPr settings."), tb_para(11, "Line 5: Continuing to test more overflow content..."), tb_para(11, "Line 6: Final overflow line."), # Scenario 10 (HIGH-LEVEL, textbox[12..13]): behindDoc pushes the bottom # box behind body text; relativeHeight sets stacking order (higher=front); # the top box's fill.opacity=80 lets the bottom show through. para("Scenario 10: Textbox Stacking (Z-order)", style="Heading2"), tb_add(text="Bottom layer (behindDoc)", width="8.33cm", height="4.17cm", fill="BBDEFB", wrap="none", behindDoc="true", relativeHeight="251670528", textAnchor="top", **{"line.color": "1565C0", "line.width": "1.5pt", "hRelative": "column", "anchor.x": "0.56cm"}), tb_fmt(12, 1, bold="true", color="1565C0", size="14"), tb_para(12, "This textbox is behind the document content."), tb_para(12, "It should be partially obscured by the top layer textbox."), tb_add(text="Top layer (translucent)", width="8.33cm", height="3.33cm", fill="FFCDD2", wrap="none", relativeHeight="251671552", textAnchor="top", **{"fill.opacity": "80", "line.color": "C62828", "line.width": "1.5pt", "hRelative": "column", "anchor.x": "3.33cm", "vRelative": "paragraph", "anchor.y": "1.11cm"}), tb_fmt(13, 1, bold="true", color="C62828", size="14"), tb_para(13, "This textbox is on top, 80% opacity."), tb_para(13, "It partially obscures the bottom blue textbox."), ] doc.batch(items) print(f" added {len(items)} paragraphs/textboxes") print(f"Generated: {FILE}")