8cb1f9f479
Publish SDK (PyPI) / publish (push) Has been cancelled
Publish SDK (npm) / publish (@aionui/officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (windows-latest) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/officecli-sdk) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/sdk) (push) Has been cancelled
Publish SDK (npm) / publish (officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (macos-latest) (push) Has been cancelled
SDK smoke / smoke (ubuntu-latest) (push) Has been cancelled
Skill parity / diff (push) Has been cancelled
379 lines
15 KiB
Python
Executable File
379 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Basic PowerPoint pictures — embed images, position/resize, crop, rotate, hyperlink.
|
|
|
|
SDK twin of pictures-basic.sh (officecli CLI). Both produce an equivalent
|
|
pictures-basic.pptx. This one drives the **officecli Python SDK**
|
|
(`pip install officecli-sdk`): one resident is started and every picture and
|
|
textbox is shipped over the named pipe. Each item is the same
|
|
`{"command","parent","type","props"}` dict you'd put in an `officecli batch`
|
|
list; `doc.send(...)` ships one and returns its envelope (used on slide 5 to
|
|
recover the just-added picture's DOM path before a Set-only effect is applied).
|
|
|
|
This script:
|
|
1. Generates 3 sample PNGs (gradient, geometric, photo-like) in a temp dir
|
|
2. Builds a multi-slide PPTX demoing different picture properties:
|
|
- slide 1: src= file vs URL vs data-URI (three ways to supply an image)
|
|
- slide 2: crop variants — symmetric, vertical/horizontal, per-edge
|
|
- slide 3: rotation
|
|
- slide 4: hyperlinks (click-to-open URL / jump to slide / next-slide action)
|
|
- slide 5: Set-only effects — brightness / contrast / glow / shadow
|
|
|
|
Requirements:
|
|
pip install Pillow officecli-sdk # plus the `officecli` binary on PATH
|
|
|
|
Usage:
|
|
python3 pictures-basic.py
|
|
"""
|
|
|
|
import base64
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
|
|
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
|
|
|
|
|
|
FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pictures-basic.pptx")
|
|
|
|
|
|
def make_gradient(path, w=400, h=300, c1=(231, 76, 60), c2=(52, 152, 219)):
|
|
img = Image.new("RGB", (w, h))
|
|
pix = img.load()
|
|
for y in range(h):
|
|
t = y / (h - 1)
|
|
r = int(c1[0] * (1 - t) + c2[0] * t)
|
|
g = int(c1[1] * (1 - t) + c2[1] * t)
|
|
b = int(c1[2] * (1 - t) + c2[2] * t)
|
|
for x in range(w):
|
|
pix[x, y] = (r, g, b)
|
|
d = ImageDraw.Draw(img)
|
|
d.text((20, 20), "gradient.png", fill=(255, 255, 255))
|
|
img.save(path)
|
|
|
|
|
|
def make_geometric(path, w=400, h=300):
|
|
img = Image.new("RGB", (w, h), (245, 245, 220))
|
|
d = ImageDraw.Draw(img)
|
|
d.ellipse((50, 50, 180, 180), fill=(231, 76, 60), outline=(0, 0, 0), width=3)
|
|
d.rectangle((200, 80, 350, 220), fill=(52, 152, 219), outline=(0, 0, 0), width=3)
|
|
d.polygon([(120, 200), (60, 270), (180, 270)],
|
|
fill=(241, 196, 15), outline=(0, 0, 0))
|
|
d.text((10, 10), "geometric.png", fill=(0, 0, 0))
|
|
img.save(path)
|
|
|
|
|
|
def make_photo(path, w=400, h=300):
|
|
"""A pseudo-photo (radial gradient + noise hint)."""
|
|
img = Image.new("RGB", (w, h))
|
|
cx, cy = w / 2, h / 2
|
|
maxd = (cx ** 2 + cy ** 2) ** 0.5
|
|
pix = img.load()
|
|
for y in range(h):
|
|
for x in range(w):
|
|
d = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5 / maxd
|
|
r = int(255 * (1 - d * 0.7))
|
|
g = int(180 * (1 - d * 0.5))
|
|
b = int(80 * (1 - d * 0.3))
|
|
pix[x, y] = (r, g, b)
|
|
draw = ImageDraw.Draw(img)
|
|
draw.text((10, 10), "photo.png", fill=(255, 255, 255))
|
|
img.save(path)
|
|
|
|
|
|
def png_to_data_uri(path):
|
|
with builtin_open(path, "rb") as f:
|
|
data = base64.b64encode(f.read()).decode()
|
|
return f"data:image/png;base64,{data}"
|
|
|
|
|
|
# officecli (the module) defines its own open(); keep the builtin for reading PNGs.
|
|
builtin_open = open
|
|
|
|
|
|
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 add_pic_path(doc, parent, **props):
|
|
"""Add a picture and return its DOM path from the success envelope."""
|
|
env = add(doc, parent, "picture", **props)
|
|
# envelope: {"success": true, "data": "Added picture at /slide[5]/shape[@id=...]"}
|
|
msg = env.get("data") if isinstance(env, dict) else None
|
|
if not msg:
|
|
msg = env.get("message") if isinstance(env, dict) else None
|
|
if msg and "Added picture at" in msg:
|
|
return msg.split()[-1]
|
|
raise RuntimeError("Could not extract picture path from: " + repr(env))
|
|
|
|
|
|
def main():
|
|
if os.path.exists(FILE):
|
|
os.remove(FILE)
|
|
|
|
workdir = tempfile.mkdtemp(prefix="ocli-pics-")
|
|
try:
|
|
grad = os.path.join(workdir, "gradient.png")
|
|
geo = os.path.join(workdir, "geometric.png")
|
|
photo = os.path.join(workdir, "photo.png")
|
|
make_gradient(grad)
|
|
make_geometric(geo)
|
|
make_photo(photo)
|
|
|
|
print(f"Building {FILE} ...")
|
|
|
|
with officecli.create(FILE, "--force") as doc:
|
|
|
|
# ── Slide 1: three src= forms ─────────────────────────────────────
|
|
add(doc, "/", "slide")
|
|
add(doc, "/slide[1]", "textbox",
|
|
text="Three ways to supply src= (file path / data-URI)",
|
|
size="24", bold="true",
|
|
x="0.5in", y="0.3in", width="12in", height="0.6in")
|
|
|
|
# 1a. File path
|
|
add(doc, "/slide[1]", "picture",
|
|
src=grad,
|
|
x="0.5in", y="1.3in", width="3.5in", height="2.6in",
|
|
alt="gradient image from disk")
|
|
add(doc, "/slide[1]", "textbox",
|
|
text="src=<file path>",
|
|
size="12", italic="true",
|
|
x="0.5in", y="4in", width="3.5in", height="0.4in")
|
|
|
|
# 1b. data-URI
|
|
uri = png_to_data_uri(geo)
|
|
add(doc, "/slide[1]", "picture",
|
|
src=uri,
|
|
x="4.5in", y="1.3in", width="3.5in", height="2.6in",
|
|
alt="geometric shapes embedded as data-URI")
|
|
add(doc, "/slide[1]", "textbox",
|
|
text="src=data:image/png;base64,...",
|
|
size="12", italic="true",
|
|
x="4.5in", y="4in", width="3.5in", height="0.4in")
|
|
|
|
# 1c. Another file (use the photo)
|
|
add(doc, "/slide[1]", "picture",
|
|
src=photo,
|
|
x="8.5in", y="1.3in", width="3.5in", height="2.6in",
|
|
alt="pseudo-photo gradient",
|
|
name="hero-photo",
|
|
compressionState="print")
|
|
add(doc, "/slide[1]", "textbox",
|
|
text='src=<file> + name="hero-photo" + compressionState=print',
|
|
size="12", italic="true",
|
|
x="8.5in", y="4in", width="3.5in", height="0.4in")
|
|
|
|
# ── Slide 2: crop variants ────────────────────────────────────────
|
|
add(doc, "/", "slide")
|
|
add(doc, "/slide[2]", "textbox",
|
|
text="Crop — symmetric / vertical,horizontal / per-edge",
|
|
size="24", bold="true",
|
|
x="0.5in", y="0.3in", width="12in", height="0.6in")
|
|
|
|
# Original (uncropped reference)
|
|
add(doc, "/slide[2]", "picture",
|
|
src=geo,
|
|
x="0.5in", y="1.3in", width="3in", height="2.2in")
|
|
add(doc, "/slide[2]", "textbox",
|
|
text="original (no crop)", size="12",
|
|
x="0.5in", y="3.6in", width="3in", height="0.4in")
|
|
|
|
# crop=20 — symmetric all edges
|
|
add(doc, "/slide[2]", "picture",
|
|
src=geo, crop="20",
|
|
x="4in", y="1.3in", width="3in", height="2.2in")
|
|
add(doc, "/slide[2]", "textbox",
|
|
text="crop=20 (20% off each edge)", size="12",
|
|
x="4in", y="3.6in", width="3in", height="0.4in")
|
|
|
|
# crop=10,30 — vertical 10%, horizontal 30%
|
|
add(doc, "/slide[2]", "picture",
|
|
src=geo, crop="10,30",
|
|
x="7.5in", y="1.3in", width="3in", height="2.2in")
|
|
add(doc, "/slide[2]", "textbox",
|
|
text="crop=10,30 (10% top/bot, 30% left/right)",
|
|
size="12",
|
|
x="7.5in", y="3.6in", width="3.5in", height="0.4in")
|
|
|
|
# Per-edge: cropLeft + cropTop
|
|
add(doc, "/slide[2]", "picture",
|
|
src=geo,
|
|
cropLeft="25", cropTop="25",
|
|
x="0.5in", y="4.3in", width="3in", height="2.2in")
|
|
add(doc, "/slide[2]", "textbox",
|
|
text="cropLeft=25 + cropTop=25",
|
|
size="12",
|
|
x="0.5in", y="6.6in", width="3in", height="0.4in")
|
|
|
|
# 4-value crop: left,top,right,bottom
|
|
add(doc, "/slide[2]", "picture",
|
|
src=geo, crop="5,10,40,20",
|
|
x="4in", y="4.3in", width="3in", height="2.2in")
|
|
add(doc, "/slide[2]", "textbox",
|
|
text="crop=5,10,40,20 (L,T,R,B)",
|
|
size="12",
|
|
x="4in", y="6.6in", width="3in", height="0.4in")
|
|
|
|
# ── Slide 3: rotation ─────────────────────────────────────────────
|
|
add(doc, "/", "slide")
|
|
add(doc, "/slide[3]", "textbox",
|
|
text="Rotation — degrees clockwise",
|
|
size="24", bold="true",
|
|
x="0.5in", y="0.3in", width="12in", height="0.6in")
|
|
|
|
positions = [
|
|
(0.5, 1.5, 0),
|
|
(4.5, 1.5, 30),
|
|
(8.5, 1.5, 90),
|
|
(0.5, 4.5, 180),
|
|
(4.5, 4.5, 270),
|
|
(8.5, 4.5, -45),
|
|
]
|
|
for x, y, deg in positions:
|
|
add(doc, "/slide[3]", "picture",
|
|
src=geo,
|
|
x=f"{x}in", y=f"{y}in", width="3in", height="2.2in",
|
|
rotation=str(deg))
|
|
add(doc, "/slide[3]", "textbox",
|
|
text=f"rotation={deg}",
|
|
size="12",
|
|
x=f"{x}in", y=f"{y + 2.3}in", width="3in", height="0.4in")
|
|
|
|
# ── Slide 4: clickable hyperlinks on pictures ─────────────────────
|
|
add(doc, "/", "slide")
|
|
add(doc, "/slide[4]", "textbox",
|
|
text="Clickable Pictures — link= and tooltip=",
|
|
size="24", bold="true",
|
|
x="0.5in", y="0.3in", width="12in", height="0.6in")
|
|
|
|
# External URL
|
|
add(doc, "/slide[4]", "picture",
|
|
src=grad,
|
|
x="0.5in", y="1.5in", width="3.5in", height="2.6in",
|
|
link="https://example.com",
|
|
tooltip="Open example.com")
|
|
add(doc, "/slide[4]", "textbox",
|
|
text="link=https://example.com",
|
|
size="12",
|
|
x="0.5in", y="4.2in", width="3.5in", height="0.4in")
|
|
|
|
# In-deck slide jump
|
|
add(doc, "/slide[4]", "picture",
|
|
src=geo,
|
|
x="4.5in", y="1.5in", width="3.5in", height="2.6in",
|
|
link="slide[1]",
|
|
tooltip="Back to slide 1")
|
|
add(doc, "/slide[4]", "textbox",
|
|
text="link=slide[1] (jump to slide 1)",
|
|
size="12",
|
|
x="4.5in", y="4.2in", width="3.5in", height="0.4in")
|
|
|
|
# Named action: nextslide
|
|
add(doc, "/slide[4]", "picture",
|
|
src=photo,
|
|
x="8.5in", y="1.5in", width="3.5in", height="2.6in",
|
|
link="nextslide",
|
|
tooltip="Advance one slide")
|
|
add(doc, "/slide[4]", "textbox",
|
|
text="link=nextslide (named action)",
|
|
size="12",
|
|
x="8.5in", y="4.2in", width="3.5in", height="0.4in")
|
|
|
|
# ── Slide 5: Set-only effects — brightness, contrast, glow, shadow ─
|
|
# These four props are schema-declared add:false / set:true. Pattern:
|
|
# Add the picture, then Set the effect on the captured path. Also
|
|
# exercises cropBottom / cropRight by their named form (vs the
|
|
# 4-value crop= shape).
|
|
add(doc, "/", "slide")
|
|
add(doc, "/slide[5]", "textbox",
|
|
text="Picture effects (Set-only) — brightness / contrast / glow / shadow",
|
|
size="24", bold="true",
|
|
x="0.5in", y="0.3in", width="13in", height="0.6in")
|
|
|
|
def add_pic_and_get_path(slide, x, y, **extra):
|
|
"""Add a picture and return its DOM path from the envelope."""
|
|
return add_pic_path(
|
|
doc, f"/slide[{slide}]",
|
|
src=photo,
|
|
x=f"{x}in", y=f"{y}in", width="2.8in", height="2.1in",
|
|
**{k: str(v) for k, v in extra.items()})
|
|
|
|
def label(slide, x, y, text):
|
|
add(doc, f"/slide[{slide}]", "textbox",
|
|
text=text,
|
|
size="11", italic="true",
|
|
x=f"{x}in", y=f"{y}in", width="2.8in", height="0.4in")
|
|
|
|
def set_(path, **props):
|
|
doc.send({"command": "set", "path": path, "props": props})
|
|
|
|
# Reference (untouched)
|
|
add_pic_and_get_path(5, 0.5, 1.2)
|
|
label(5, 0.5, 3.4, "(reference)")
|
|
|
|
# brightness +40 — lifts mid-tones
|
|
p_bright = add_pic_and_get_path(5, 3.6, 1.2)
|
|
set_(p_bright, brightness="40")
|
|
label(5, 3.6, 3.4, "brightness=40")
|
|
|
|
# contrast -30 — flattens
|
|
p_con = add_pic_and_get_path(5, 6.7, 1.2)
|
|
set_(p_con, contrast="-30")
|
|
label(5, 6.7, 3.4, "contrast=-30")
|
|
|
|
# brightness + contrast together
|
|
p_combo = add_pic_and_get_path(5, 9.8, 1.2)
|
|
set_(p_combo, brightness="-20", contrast="40")
|
|
label(5, 9.8, 3.4, "brightness=-20 + contrast=40")
|
|
|
|
# glow — `color-radius-opacity`
|
|
p_glow = add_pic_and_get_path(5, 0.5, 4.2)
|
|
set_(p_glow, glow="FFD700-12-75")
|
|
label(5, 0.5, 6.4, "glow=FFD700-12-75")
|
|
|
|
# shadow — `color-blur-angle-dist-opacity`
|
|
p_shadow = add_pic_and_get_path(5, 3.6, 4.2)
|
|
set_(p_shadow, shadow="000000-10-45-6-50")
|
|
label(5, 3.6, 6.4, "shadow=000000-10-45-6-50")
|
|
|
|
# cropRight + cropBottom — by-name form (vs the 4-value crop=)
|
|
add_pic_and_get_path(5, 6.7, 4.2, cropRight=25, cropBottom=15)
|
|
label(5, 6.7, 6.4, "cropRight=25 + cropBottom=15")
|
|
|
|
# Everything together: trim corners + brightness + glow + shadow
|
|
p_all = add_pic_and_get_path(5, 9.8, 4.2, cropLeft=10, cropTop=10,
|
|
cropRight=10, cropBottom=10)
|
|
set_(p_all,
|
|
brightness="15",
|
|
contrast="20",
|
|
glow="4472C4-8-60",
|
|
shadow="000000-6-135-3-40")
|
|
label(5, 9.8, 6.4, "trimmed + bright + contrast + glow + shadow")
|
|
|
|
doc.send({"command": "save"})
|
|
# context exit closes the resident, flushing the deck to disk.
|
|
|
|
print(f"Created: {FILE}")
|
|
|
|
finally:
|
|
shutil.rmtree(workdir, ignore_errors=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|