power-pptx

Lint & Audit

The layout linter is the last line of defence in the space-aware stack: it measures the slide and reports anything that would look broken in PowerPoint.

slide.lint()

from power_pptx import Presentation

prs = Presentation("deck.pptx")
for slide in prs.slides:
    report = slide.lint()
    for issue in report.issues:
        print(issue.severity.value, issue)

Issue types

TypeMeaningDefault severity
TextOverflowEstimated text extent exceeds its frame (Pillow metrics)warning
OffSlideShape extends past the slide boundserror
ShapeCollisionUnintentional overlap between shapeswarning
LayerOrderViolationA shape declares layer_above but is drawn below that layer, so it will be hiddenerror

auto_fix and tidy

report = slide.lint()
report.auto_fix()          # mutates: clamps OffSlide, resolves overflow,
                           # restacks contradicted layer declarations

# One call instead of lint -> fix -> re-lint
slide.tidy()
slide.tidy(fix_grid_drift=True)    # opt into grid snapping
slide.tidy(fix_layer_order=False)  # leave z-order alone

Auto-fix handles the issues that need no designer judgment: OffSlide (clamped back on-slide), TextOverflow (flipped toauto_size = TEXT_TO_FIT_SHAPE), OffGridDrift (snapped, opt-in viatidy(fix_grid_drift=True)) and LayerOrderViolation (restacked).ShapeCollision is deliberately left alone — nudging shapes apart almost always breaks the design. If the overlap was intentional, say so instead.

Declaring an intentional overlap

ShapeCollision is the noisiest rule, because deliberate layering — a badge on a KPI card, an accent bar on a panel — looks exactly like a copy-paste bug from a bounding box alone. Tell the linter what you meant and it stops guessing. Three ways, narrowest last:

from power_pptx.enum.shapes import MSO_SHAPE
from power_pptx.util import Inches

# A KPI card: backing panel, accent bar, value text, and a delta badge
# hanging off the top-right corner.
card = slide.shapes.add_shape(
    MSO_SHAPE.ROUNDED_RECTANGLE, Inches(0.8), Inches(1.5),
    Inches(3.4), Inches(2.0))
accent_bar = slide.shapes.add_shape(
    MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(1.5), Inches(3.4), Inches(0.2))
value_box = slide.shapes.add_textbox(
    Inches(1.0), Inches(2.0), Inches(3.0), Inches(0.9))
badge = slide.shapes.add_shape(
    MSO_SHAPE.OVAL, Inches(3.6), Inches(1.2), Inches(1.0), Inches(0.6))

# 1. Group tag - n-ary and symmetric: everything sharing a non-empty
#    tag may overlap everything else in the tag.
slide.lint_group("kpi-1", card, accent_bar, value_box)
slide.lint_group_overlaps(card, accent_bar, value_box)  # auto-names it

# 2. Pairwise allowance - licenses exactly one pair, nothing else.
badge.allow_overlap_with(card)
badge.disallow_overlap_with(card)           # revoke
badge.overlap_allowances                    # frozenset[int] of shape ids

# 3. Layer hints - the only form that also asserts z-order.
card.layer = "card"
badge.layer_above = "card"
MechanismScopeReach for it when
lint_groupn-ary, symmetricSeveral shapes form one visual cluster and any of them may overlap any other
allow_overlap_withone pairExactly one overlap is meant to be legal and you want the rest still policed. Both shapes must be on the same slide — allowances are keyed on a shape id, which repeats across a deck, so naming a shape from another slide raises ValueError.
layer / layer_abovedirectionalThe stacking order is part of the design and you want it enforced, not merely tolerated

An allowance is written one-sided but read symmetrically: it takes only one of the pair to vouch for the overlap, so calling it on either shape is equivalent. Allowances accumulate rather than replace, and revoking one that was never granted is a no-op.

Layer hints are the only form that can fail. Declaringlayer_above = "card" asserts this shape is painted on top of every overlapping shape whose layer is "card". If the shape tree says otherwise, you get a LayerOrderViolation at error severity, because the declaration records what you meant and the drawing order is what failed to deliver it:

# 'badge' was added to the slide *before* 'card', so it is painted
# underneath the thing it claims to sit on top of.
card.layer = "card"
badge.layer_above = "card"

report = slide.lint()
report.has_errors        # True
report.auto_fix()
# ["Restacked 'Delta badge' above 'KPI card' to honour layer_above='card'."]

A layer name describes a stratum of the design rather than one cluster, so any number of unrelated shapes may share it. Only overlapping pairs are checked — a layer declaration between shapes that never touch is inert, not wrong. All three forms round-trip through save/open in the shape's cNvPr element and are invisible to PowerPoint.

Linting on save

Every Presentation carries a save-time gate, whatever built it:

prs.lint_on_save = "off"      # default - no checks, no cost
prs.lint_on_save = "warn"     # log error-severity issues, still write
prs.lint_on_save = "raise"    # raise LintError instead of writing

prs.save("out.pptx")

Only error-severity issues count. The lint pass runs before anything is written, so "raise" never leaves a bad file on disk — it raisespower_pptx.exc.LintError naming the offending slide indexes. "warn"logs on the power_pptx.presentation logger and writes the file anyway. The setting lives on the in-memory deck only; re-open the saved file and it is back to"off".

The from_spec lint gate

from power_pptx.compose import from_spec

prs = from_spec({
    "slides": [{"layout": "title", "title": "Q4 Review"}],
    "lint": "raise",                  # raise | warn | off
})

Spec slides can also carry the intent declarations themselves — a "shapes" entry accepts lint_group, allow_overlap_with, layer andlayer_above, so a generator declares deliberate layering at generation time and the built deck lints clean with no tagging pass. SeeCompose & Templates.

Whole-deck audit

from power_pptx import audit

report = audit(prs)
print(report.markdown())

The audit aggregates, per deck:

  • lint issues across every slide
  • broken or missing pictures
  • empty slides
  • uncommon-font warnings (fonts recipients are unlikely to have)
  • oversized-picture warnings (bloat)

AuditReport.markdown() formats the lot for a chat reply or CI log.