API: Shapes & Geometry
power_pptx.BBox fork
Immutable rectangular region. Construct with from_inches, from_emu,from_shape or from_slide.
from power_pptx import Presentation, BBox
from power_pptx.util import Inches
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])
bb = BBox.from_inches(1, 2, 8, 4) # left, top, width, height
BBox.from_slide(slide) # the whole slide canvas
bb.right, bb.bottom # far edges (EMU)
bb.cx, bb.cy # centre (EMU)
bb.area
left, top, width, height = bb # unpacks — so *bb splats into add_*
bb.as_tuple()
bb.shifted(dx=Inches(1), dy=Inches(0.5))
bb.resized(width=Inches(6), height=Inches(3))
bb.inset(all=Inches(0.2)) # or x=/y=/left=/right=/top=/bottom=
other = bb.sub(0.25, 0.25, 0.5, 0.5) # fractional (fx, fy, fw, fh) sub-region
bb.split_h([1, 1], gap=Inches(0.2)) # columns by ratio
bb.split_v([2, 1], gap=Inches(0.1)) # rows by ratio
bb.grid(cols=3, rows=2, gap_x=Inches(0.1), gap_y=Inches(0.1))
bb.contains(other, tol=0)
bb.intersects(other)
bb.intersection(other) # BBox | None
bb.union(other)power_pptx.shapes.shapetree — the shape tree
slide.shapes is a SlideShapes sequence (index, iterate,len()) with factory methods:
from power_pptx.chart.data import CategoryChartData
from power_pptx.enum.chart import XL_CHART_TYPE
from power_pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
shapes = slide.shapes
shapes.add_text(bb, text="Hi", size_pt=24, bold=True,
color="#0B5CFF", align="center", anchor="middle")
card = shapes.add_shape(MSO_SHAPE.RECTANGLE, *bb, anchor="center")
card.bbox.apply_to(card) # push a BBox back onto a shape
shapes.add_textbox(*bb)
shapes.add_picture("img.png", *bb.sub(0, 0, 0.3, 0.3), anchor="center")
shapes.add_table(3, 4, *bb.sub(0.5, 0.5, 0.5, 0.5), style="clean")
chart_data = CategoryChartData()
chart_data.categories = ["A", "B"]
chart_data.add_series("S1", (1, 2))
shapes.add_chart(XL_CHART_TYPE.COLUMN_CLUSTERED,
Inches(1), Inches(1), Inches(6), Inches(4), chart_data)
shapes.add_connector(MSO_CONNECTOR.STRAIGHT,
Inches(1), Inches(6), Inches(4), Inches(6)) # bare line!
arrow = shapes.add_arrow(start=card, end=(Inches(9), Inches(6)),
head="triangle", color="#0B5CFF",
weight_pt=1.5, inset_pt=6)
shapes.add_group_shape()
shapes.build_freeform(Inches(1), Inches(1), scale=1.0)
shapes.add_svg_picture("diagram.svg", *bb.sub(0.7, 0, 0.3, 0.3),
png_fallback="d.png")
shapes.add_movie("clip.mp4", Inches(5), Inches(5), Inches(3), Inches(2),
poster_frame_image=None)BaseShape and subclasses
Shape (autoshape), Picture, GraphicFrame (table/chart),GroupShape, Connector, FreeformBuilder output,Movie, and the placeholder hierarchy (SlidePlaceholder, TitlePlaceholder, PicturePlaceholder,ChartPlaceholder, TablePlaceholder) all share:
shape = card # any autoshape
shape.left, shape.top, shape.width, shape.height # EMU, mutable
shape.name, shape.shape_id, shape.shape_type
shape.rotation, shape.shadow
shape.click_action # hyperlinks / jump targets
shape.fill, shape.line # FillFormat / LineFormat
shape.ln # the raw <a:ln> element, or None
shape.fill_hex("#0B5CFF") # fork: chainable hex helpers
shape.line_hex("#0D0D0D", weight_pt=1.25)
shape.glow, shape.soft_edges # fork: effect proxies (also shadow, blur,
shape.reflection # inner_shadow, preset_shadow, reflection)
shape.three_d # fork: bevels / extrusion / material
shape.set_text_preserving_format("New text") # fork
shape.bbox # fork: BBox snapshot of the frame
shape.alt_text # accessibility description
shape.title_text # short accessibility label
shape.lint_skip # fork: per-shape linter opt-out
shape.delete() # fork: remove + purge orphaned animationsProbing a shape before you use it
A shape tree is heterogeneous, so the has_* flags are the safe way to narrow one down. Each is unconditionally False on the base class and overridden toTrue on the subclass that can deliver — has_text_frame onShape, has_table / has_chart onGraphicFrame — so testing beats catching an exception, and beats guessing fromshape_type.
# Ask before you reach - each of these is False on shapes that can't.
card.has_text_frame # True -> card.text_frame
table.has_table # True -> table.table
chart.has_chart # True -> chart.chart
card.is_placeholder # False
title = slide.shapes.title
title.is_placeholder # True
title.placeholder_format.idx # 0
title.placeholder_format.type # PP_PLACEHOLDER.TITLE
card.placeholder_format # ValueError: shape is not a placeholderis_placeholder is True when the shape carries a<p:ph> element. placeholder_format is the accessor that goes with it, exposing idx and type; it raisesValueError rather than returning None on a non-placeholder, so guard it with is_placeholder.
Autoshape geometry: adjustments and corner radius
auto_shape_type returns the MSO_SHAPE member behind the shape's preset geometry, and raises ValueError for anything that isn't an autoshape (a text box, a freeform). On a Picture the same name means something different and is writable — there it is the masking shape the image is cropped to.
adjustments is the sequence of yellow-handle values behind that geometry — a rounded rectangle has one, a callout or a chevron more. Values are floats on a 0–1 scale, indexed and assignable (shape.adjustments[0] = 0.25), and writing one rewrites the shape's <a:gd> guides.
The catch is that OOXML stores a corner radius as a fraction of the shorter side, so one design spec ("6pt corners everywhere") is a different number on every differently-sized card. corner_radius is the post-fork wrapper that removes that arithmetic: it reads and writes a real Length, converting to and fromadjustments[0].
from power_pptx.enum.shapes import MSO_SHAPE
from power_pptx.util import Inches, Pt
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(1), Inches(2), Inches(4), Inches(2))
card.auto_shape_type # MSO_SHAPE.ROUNDED_RECTANGLE
len(card.adjustments) # 1 - this geometry exposes one handle
card.adjustments[0] # 0.16667, the PowerPoint default
# corner_radius does the fraction arithmetic for you.
card.corner_radius = Pt(8)
round(card.corner_radius.pt, 2) # 8.0
round(card.adjustments[0], 5) # 0.05556 == Pt(8) / min(width, height)
# The same 8pt spec on a shape half the height is a different fraction...
small = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(6), Inches(2), Inches(3), Inches(1))
small.corner_radius = Pt(8)
round(small.adjustments[0], 5) # 0.11111 - but still 8pt on screen
card.corner_radius = Inches(3) # ValueError: exceeds half the shorter side
triangle.corner_radius # ValueError: geometry has no corner radiusIt applies to the four rounded-rectangle presets — ROUNDED_RECTANGLE,ROUND_1_RECTANGLE, ROUND_2_SAME_RECTANGLE,ROUND_2_DIAG_RECTANGLE — and touches only adjustments[0], leaving the second corner pair of the two-radius geometries under explicit control. It raisesValueError on any other geometry, on a shape with no width or height yet, and on a radius larger than half the shorter side (the largest a preset rounded rectangle can express). Reads report the radius as rendered: these presets pin their adjustment to the 0–0.5 range, so a shape carrying an out-of-range value authored elsewhere reports the legal radius it actually draws at. Round-tripping is exact to within the adjustment's 1/100,000 quantum — round the .pt value if you are asserting on it.
Accessibility, lint opt-outs, and deletion
from power_pptx.animation import Entrance
# Accessibility slots on <p:cNvPr>; "" or None clears either one.
card.alt_text = "Revenue card showing 4.2M, up 12% year on year."
card.title_text = "Revenue"
# Per-shape linter opt-out, stored in the same extension as lint_group.
card.lint_skip = {"MinFontSize"}
badge.lint_skip = {"MinFontSize"}
card.lint_skip # frozenset({'MinFontSize'})
card.lint_skip = set() # clear
# delete() also purges timing entries that pointed at the shape.
Entrance.fade(slide, card)
Entrance.fly_in(slide, badge, direction="left")
len(slide.animations) # 2
badge.delete()
len(slide.animations) # 1 - the orphan <p:spTgt> went with italt_text maps to the descr attribute of the shape's<p:cNvPr> — the slot screen readers announce and PowerPoint shows in itsAlt Text pane. title_text maps to the sibling titleattribute, a short label that complements the longer description. Both read back"" when unset, and assigning "" or None removes the attribute rather than writing an empty one.
lint_skip is a frozenset of issue codes silenced on this shape; assign any set/list/tuple of strings, or an empty one to clear. Cross-shape issues such asShapeCollision and ZOrderAnomaly are suppressed only whenboth shapes opt out — a one-sided opt-out keeps the warning, since the other shape may still want it surfaced. It rides in the same cNvPr/extLst block aslint_group, so it survives save and reopen and is invisible to PowerPoint. Reach for it to silence a rule on one shape; reach for alint group to declare an overlap intentional.
delete() is not just element.getparent().remove(element). Removing the element by hand leaves behind any entry in the slide's timing tree that targeted the shape, and PowerPoint responds to those orphan <p:spTgt> references by offering to repair the file on open. delete() removes the element and then purges the dangling animation entries, so an animated shape can be dropped without leaving the deck in a state that prompts a repair.
Picture extras
picture = shapes.add_picture("img.png", Inches(8), Inches(1), Inches(2))
picture.image # Image (blob, filename, size, ext)
picture.crop_left, picture.crop_top # also crop_right / crop_bottom
picture.effects.transparency = 0.25 # fork: 0..1
picture.effects.brightness = 0.1 # fork: -1..1
picture.effects.contrast = -0.2 # fork
picture.effects.recolor = "grayscale" # fork: sepia | washout | duotone
picture.enclosing_container() # fork: the surrounding card, if any
def builder(slide, bbox): # fork: swap picture for native shapes
slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *bbox).fill_hex("#0B5CFF")
picture.replace_with(builder, padding=Inches(0.05))power_pptx.diagrams fork
| Function | Result dataclass |
|---|---|
horizontal_pipeline(slide, bbox, steps, accent=...) | PipelineResult |
vertical_pipeline(slide, bbox, steps, ...) | PipelineResult |
hub_and_spoke(slide, bbox, centre, spokes, ...) | HubAndSpokeResult |
cycle(slide, bbox, steps, ...) | CycleResult |
decision_tree(slide, bbox, root, branches, ...) | DecisionTreeResult |
comparison_columns(slide, bbox, columns, ...) | ColumnsResult |
Each returns a dataclass exposing the constituent shapes (nodes, connectors, labels) for further tweaks.