8cb1f9f479
Publish SDK (PyPI) / publish (push) Waiting to run
Publish SDK (npm) / publish (@aionui/officecli-sdk) (push) Waiting to run
Publish SDK (npm) / publish (@officecli/officecli-sdk) (push) Waiting to run
Publish SDK (npm) / publish (@officecli/sdk) (push) Waiting to run
Publish SDK (npm) / publish (officecli-sdk) (push) Waiting to run
SDK smoke / smoke (windows-latest) (push) Waiting to run
SDK smoke / smoke (macos-latest) (push) Waiting to run
SDK smoke / smoke (ubuntu-latest) (push) Waiting to run
Skill parity / diff (push) Waiting to run
212 lines
9.9 KiB
Python
212 lines
9.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
pictures.py — embed and lay out images in a Word document (.docx).
|
|
|
|
SDK twin of pictures.sh (officecli CLI). Both produce an equivalent
|
|
pictures.docx. This one drives the **officecli Python SDK**
|
|
(`pip install officecli-sdk`): one resident is started and every paragraph and
|
|
picture is shipped over the named pipe. Each item is the same
|
|
`{"command","parent","type","props"}` dict you'd put in an `officecli batch`
|
|
list; `add(...)` ships one and returns its envelope.
|
|
|
|
In docx a picture is a run inside a paragraph, so every picture is added to a
|
|
paragraph path (/body/p[N]); floating layout (wrap / behindText / hAlign /
|
|
vAlign / hPosition / vPosition) requires props anchor=true, while inline
|
|
pictures sit in the text flow like a big character.
|
|
|
|
This script:
|
|
1. Synthesizes two sample PNGs (a square logo + a wide banner) in-dir
|
|
2. Builds a document demoing docx picture properties:
|
|
- 1: inline picture (in the text flow, width/height sizing)
|
|
- 2: cropped picture (crop=L,T,R,B percent per edge)
|
|
- 3: alt text (accessibility / screen readers)
|
|
- 4: behind-text watermark (anchor + wrap=none + behindText, centered)
|
|
- 5: square text wrap (text flows around a right-aligned float)
|
|
- 6: absolute position (anchor + wrap=tight + hPosition/vPosition)
|
|
- 7: clickable picture (link= external URL)
|
|
- 8: decorative picture (decorative=true — screen readers skip it)
|
|
|
|
Requirements:
|
|
pip install Pillow officecli-sdk # plus the `officecli` binary on PATH
|
|
|
|
Usage:
|
|
python3 pictures.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
try:
|
|
from PIL import Image, ImageDraw
|
|
except ImportError:
|
|
print("ERROR: Pillow not installed. Run: pip install Pillow")
|
|
sys.exit(1)
|
|
|
|
# --- 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
|
|
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
FILE = os.path.join(HERE, "pictures.docx")
|
|
LOGO = os.path.join(HERE, "pictures-logo.png")
|
|
BANNER = os.path.join(HERE, "pictures-banner.png")
|
|
|
|
|
|
def make_logo(path, w=300, h=300):
|
|
img = Image.new("RGB", (w, h), (245, 245, 220))
|
|
d = ImageDraw.Draw(img)
|
|
d.ellipse((40, 40, 260, 260), fill=(52, 152, 219), outline=(0, 0, 0), width=4)
|
|
d.polygon([(150, 80), (210, 220), (90, 220)], fill=(241, 196, 15), outline=(0, 0, 0))
|
|
d.text((110, 140), "LOGO", fill=(0, 0, 0))
|
|
img.save(path)
|
|
|
|
|
|
def make_banner(path, w=800, h=200, c1=(231, 76, 60), c2=(142, 68, 173)):
|
|
img = Image.new("RGB", (w, h))
|
|
pix = img.load()
|
|
for x in range(w):
|
|
t = x / (w - 1)
|
|
col = tuple(int(c1[i] * (1 - t) + c2[i] * t) for i in range(3))
|
|
for y in range(h):
|
|
pix[x, y] = col
|
|
ImageDraw.Draw(img).text((20, 20), "banner.png", fill=(255, 255, 255))
|
|
img.save(path)
|
|
|
|
|
|
def add(doc, parent, typ, **props):
|
|
"""Ship one `add` item over the pipe; return the parsed envelope."""
|
|
return doc.send({"command": "add", "parent": parent, "type": typ, "props": props})
|
|
|
|
|
|
def para(doc, text="", **props):
|
|
add(doc, "/body", "paragraph", text=text, **props)
|
|
|
|
|
|
def main():
|
|
if os.path.exists(FILE):
|
|
os.remove(FILE)
|
|
|
|
make_logo(LOGO)
|
|
make_banner(BANNER)
|
|
|
|
print(f"Building {FILE} ...")
|
|
|
|
with officecli.create(FILE, "--force") as doc:
|
|
|
|
# ── 1. Inline picture — sits in the text flow like a large character ──
|
|
para(doc, "1. Inline Picture", style="Heading1")
|
|
para(doc, "An inline picture flows with the paragraph text — no anchor, "
|
|
"no wrap; it occupies its own line box like an oversized glyph:")
|
|
para(doc, "")
|
|
# Features: inline picture (default, no anchor); width / height sizing
|
|
add(doc, "/body/p[3]", "picture",
|
|
src=LOGO, width="3cm", height="3cm")
|
|
|
|
# ── 2. Cropped picture — trim edges via crop=L,T,R,B (percent) ────────
|
|
para(doc, "2. Cropped Picture", style="Heading1")
|
|
para(doc, "crop=L,T,R,B trims each edge by a percentage of the source. "
|
|
"Here 10% left, 5% top, 15% right, 8% bottom (per-edge "
|
|
"cropLeft/cropTop/cropRight/cropBottom also accepted on add):")
|
|
para(doc, "")
|
|
# Features: crop=L,T,R,B four-value form (percent of original per edge)
|
|
add(doc, "/body/p[6]", "picture",
|
|
src=BANNER, crop="10,5,15,8", width="10cm", height="2.5cm")
|
|
|
|
# ── 3. Picture with alt text — accessibility / screen readers ─────────
|
|
para(doc, "3. Alt Text (Accessibility)", style="Heading1")
|
|
para(doc, "alt= writes the DocProperties description read aloud by "
|
|
"screen readers. Aliases: altText, description.")
|
|
para(doc, "")
|
|
# Features: alt (alternative text for accessibility)
|
|
add(doc, "/body/p[9]", "picture",
|
|
src=LOGO, width="3cm", height="3cm",
|
|
alt="Company logo: a blue circle enclosing a yellow triangle")
|
|
|
|
# ── 4. Behind-text watermark — floating picture behind the text ───────
|
|
para(doc, "4. Behind-Text Watermark", style="Heading1")
|
|
para(doc, "A floating picture with anchor=true, wrap=none and "
|
|
"behindText=true sits behind the text like a watermark. It is "
|
|
"centered on the page margins via hAlign=center + vAlign=center. "
|
|
"This paragraph text should render on top of the faint image "
|
|
"behind it, demonstrating the behind-text z-order stacking that "
|
|
"a plain inline picture cannot achieve.")
|
|
# Features: anchor=true (floating), wrap=none + behindText=true, hAlign/vAlign=center
|
|
add(doc, "/body/p[11]", "picture",
|
|
src=BANNER, anchor="true", wrap="none", behindText="true",
|
|
hAlign="center", vAlign="center",
|
|
hRelative="margin", vRelative="margin",
|
|
width="12cm", height="3cm",
|
|
alt="Decorative watermark banner")
|
|
|
|
# ── 5. Square text-wrap — body text flows around a floating picture ───
|
|
para(doc, "5. Square Text Wrap", style="Heading1")
|
|
para(doc, "With anchor=true and wrap=square, the surrounding paragraph "
|
|
"text flows around the picture's bounding box. The picture "
|
|
"below is right-aligned to the margin, so this long paragraph "
|
|
"wraps down its left side. Keep reading to see the text reflow "
|
|
"around the floated image on the right — square wrap uses a "
|
|
"rectangular boundary regardless of the image's own shape, so "
|
|
"text keeps a clean vertical edge against the picture. The "
|
|
"remaining lines continue underneath once the text clears the "
|
|
"bottom of the anchored picture's bounding rectangle.")
|
|
# Features: wrap=square (text flows around bounding box), hAlign=right rel. margin
|
|
add(doc, "/body/p[13]", "picture",
|
|
src=LOGO, anchor="true", wrap="square",
|
|
hAlign="right", hRelative="margin", vRelative="paragraph",
|
|
width="3.5cm", height="3.5cm",
|
|
alt="Logo floated right with square wrap")
|
|
|
|
# ── 6. Tight wrap + absolute position — hPosition / vPosition ─────────
|
|
para(doc, "6. Absolute Position (hPosition / vPosition)", style="Heading1")
|
|
para(doc, "Instead of relative alignment, a floating picture can be "
|
|
"pinned to an absolute offset from its reference frame. Here "
|
|
"hPosition=2cm and vPosition=1cm place the picture 2cm from the "
|
|
"left margin and 1cm down, with wrap=tight so text hugs the "
|
|
"boundary. This paragraph provides enough text for the wrap to "
|
|
"be visible against the absolutely-positioned image.")
|
|
# Features: anchor=true, wrap=tight, hPosition/vPosition (absolute, unit-qualified)
|
|
add(doc, "/body/p[15]", "picture",
|
|
src=LOGO, anchor="true", wrap="tight",
|
|
hPosition="2cm", vPosition="1cm",
|
|
hRelative="margin", vRelative="paragraph",
|
|
width="3cm", height="3cm",
|
|
alt="Logo at absolute 2cm,1cm offset with tight wrap")
|
|
|
|
# ── 7. Clickable picture — link= makes the image a hyperlink ──────────
|
|
para(doc, "7. Clickable Picture (link)", style="Heading1")
|
|
para(doc, "link= wraps the picture in a click hyperlink. An absolute URL "
|
|
"round-trips as an external relationship; a #anchor or bookmark "
|
|
"name becomes an internal jump.")
|
|
para(doc, "")
|
|
# Features: link (external URL hyperlink on the image); alt on a clickable image
|
|
add(doc, "/body/p[18]", "picture",
|
|
src=BANNER, width="10cm", height="2.5cm",
|
|
link="https://example.com",
|
|
alt="Banner linking to example.com")
|
|
|
|
# ── 8. Decorative picture — mark as decorative for accessibility ──────
|
|
para(doc, "8. Decorative Picture (accessibility)", style="Heading1")
|
|
para(doc, "decorative=true marks the image as decorative: screen readers "
|
|
"skip it entirely (no alt text is announced). Stored as an "
|
|
"adec:decorative extension under the picture's docPr. Use it for "
|
|
"purely ornamental images that carry no information.")
|
|
para(doc, "")
|
|
# Features: decorative=true (accessibility — screen readers skip the image)
|
|
add(doc, "/body/p[21]", "picture",
|
|
src=BANNER, width="10cm", height="2.5cm",
|
|
decorative="true")
|
|
|
|
doc.send({"command": "save"})
|
|
# context exit closes the resident, flushing the document to disk.
|
|
|
|
print(f"Created: {FILE}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|