power-pptx

Pictures & Media

Images are the one kind of content a deck can't fake with shapes and text. This page covers getting them onto a slide at the right size, adjusting them in place without an image editor, embedding vector artwork that stays crisp, and attaching video or audio.

Adding a picture

shapes.add_picture(image_file, left, top, width=None, height=None) takes either a filesystem path or any binary file-like object — a BytesIO holding a chart you just rendered works exactly like a path. The two extents are where most of the behaviour lives:

from power_pptx import Presentation
from power_pptx.util import Inches

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])          # blank layout

# 1. Neither extent given -> the image's native size (pixels / dpi).
native = slide.shapes.add_picture("hero.jpg", Inches(0.5), Inches(0.5))

# 2. One extent given -> the other is derived, aspect ratio preserved.
scaled = slide.shapes.add_picture("hero.jpg", Inches(0.5), Inches(3), width=Inches(4))

# 3. Both given -> the image is stretched to fit, aspect ratio ignored.
squashed = slide.shapes.add_picture("hero.jpg", Inches(5), Inches(3), Inches(4), Inches(1))

# A file-like object works anywhere a path does.
with open("hero.jpg", "rb") as f:
    from_stream = slide.shapes.add_picture(f, Inches(5), Inches(0.5), height=Inches(1.5))
  • Neither width nor height — the image is placed at its native size, computed as pixels ÷ dpi. A 800×600 image saved at 72 dpi lands 11.1 inches wide, which is wider than a 4:3 slide. Screen-resolution assets are almost always bigger than you expect, so pass an extent unless you know the dpi.
  • One of them — the other is derived from the source image's aspect ratio. This is the form to reach for: it makes distortion structurally impossible.
  • Both — the picture is stretched to exactly that box, aspect ratio discarded. Use it only when you know the ratios already agree, or when you intend tocrop rather than squash.

The returned Picture exposes the source image through pic.image, which is where to look when you need to reason about a file you didn't pick yourself:

pic = slide.shapes.add_picture("hero.jpg", Inches(1), Inches(1), width=Inches(4))

print(pic.image.size)          # (800, 600)  - pixels
print(pic.image.dpi)           # (72, 72)
print(pic.image.content_type)  # image/jpeg
print(pic.image.ext)           # jpg

Anchoring instead of arithmetic

Because the derived extent isn't known until after the picture exists, "logo in the bottom-right corner with a quarter-inch margin" normally means adding the picture, reading back its width, and then reassigning left and top.add_picture accepts anchor and margin so the library does that second pass for you:

logo = slide.shapes.add_picture(
    "logo.png",
    anchor="bottom-right",
    margin=Inches(0.25),
    height=Inches(0.5),          # width follows from the aspect ratio
)

anchor takes "top-left", "top-center","top-right", "middle-left", "middle-center" (or plain"center"), "middle-right", "bottom-left","bottom-center" and "bottom-right"; British spellings of "centre" are accepted. Pass container= a parent shape to anchor inside a card rather than against the slide. left and top are ignored whenanchor is given.

Cropping

Cropping is how you fill a box of a fixed aspect ratio with an image of a different one without distortion: set both extents, then trim the overflow. The fourcrop_* properties are read/write floats expressed as a fraction of thesource image, not of the shape — 0.15 means "discard 15% of the original picture from this edge":

pic = slide.shapes.add_picture("wide.jpg", Inches(1), Inches(1),
                               Inches(4), Inches(3))

pic.crop_left = 0.15      # discard the leftmost 15% of the source image
pic.crop_right = 0.15
pic.crop_top = 0.05
pic.crop_bottom = 0.05

The shape's position and size never change when you crop; the visible window into the image moves instead. Values greater than 1.0 are legal, and negative values extend the edge beyond the image boundary, padding it out with empty space. Crops round-trip through save and reopen, and they are the same properties PowerPoint's own crop handles write.

Image effects

picture.effects is a post-fork accessor over the OOXML image filters that live on the <a:blip> element. These are baked into the deck, so they survive round-trips and render identically in PowerPoint, Keynote and LibreOffice — no pre-processing step, no second copy of the asset.

Reads never mutate: a picture with no filters applied reports 0.0 for the numeric adjustments and None for recolor. Writing a neutral value removes the underlying element again rather than leaving a dead one behind.

pic = slide.shapes.add_picture(
    "hero.jpg", 0, 0, width=prs.slide_width, height=prs.slide_height
)

pic.effects.transparency = 0.30      # 0.0 opaque .. 1.0 invisible
pic.effects.brightness = 0.10        # -1.0 .. 1.0, 0.0 = unchanged
pic.effects.contrast = -0.05         # -1.0 .. 1.0, 0.0 = unchanged
  • transparency0.0 (opaque, the default) to 1.0(invisible). The classic use is knocking a photo back behind text.
  • brightness and contrast-1.0 to 1.0,0.0 meaning unchanged. Both write to the same <a:lum>element, so setting one does not disturb the other.

Out-of-range values raise ValueError rather than silently clamping.

Recolour presets

effects.recolor is a property, not a method. Assigning a new value clears whatever recolour was there before, so the four modes are mutually exclusive by construction:

pic.effects.recolor = "grayscale"
pic.effects.recolor = "sepia"
pic.effects.recolor = "washout"
pic.effects.recolor = "duotone"       # neutral grey duotone
pic.effects.recolor = None            # clear it

print(pic.effects.recolor)            # None

"grayscale" writes <a:grayscl>; "washout" writes a two-level threshold (<a:biLevel thresh="50%"/>); "sepia" is a duotone using the warm brown / cream pair PowerPoint uses for its own sepia preset. Any other string — "black_and_white", say — raises ValueError, and validation happens before anything is mutated, so a typo leaves the existing effect intact.

Duotone

A duotone maps the image's shadows to one colour and its highlights to another. It is the single most effective way to make stock photography look like it belongs to your brand, because it forces every image in the deck onto the same two-colour ramp:

from power_pptx.dml.color import RGBColor

pic.effects.set_duotone(RGBColor(0x12, 0x1E, 0x4D), "#A8C0FF")
pic.effects.set_duotone((18, 30, 77), (168, 192, 255))   # tuples work too

print(pic.effects.recolor)     # "duotone"
pic.effects.recolor = None     # the way to clear it

Either colour may be an RGBColor, a hex string (with or without #), or a plain (r, g, b) tuple. There is no dedicated clear method — assigneffects.recolor = None to drop it. Note that reading recolor back after set_duotone returns "duotone" unless the colours happen to be the sepia pair, in which case it reports "sepia".

For shape-level effects — shadows, glows, soft edges, gradient fills — seeEffects & Gradients.

SVG with a PNG fallback

add_picture does not accept SVG: it routes through Pillow, which can't decode vector markup, so an .svg path raisesUnidentifiedImageError. Use add_svg_picture instead.

The reason it is a separate method is that Office requires every embedded SVG to shipalongside a raster fallback. The slide's <a:blip> points at a PNG, and a Microsoft <asvg:svgBlip> extension points at the SVG. Office 2016 and later render the vector and keep it crisp at any zoom or print size; everything older, and every thumbnail generator, quietly falls back to the PNG. add_svg_picture writes both parts and wires up the extension:

from power_pptx._svg import CairoSvgUnavailable

# Bring your own raster fallback - no optional dependency needed.
slide.shapes.add_svg_picture(
    "logo.svg",
    Inches(0.5), Inches(0.5),
    width=Inches(1.5), height=Inches(0.75),
    png_fallback="logo.png",
)

# Omit png_fallback and the SVG is rasterised with the optional `cairosvg`.
try:
    slide.shapes.add_svg_picture("logo.svg", Inches(3), Inches(0.5))
except CairoSvgUnavailable:
    ...   # cairosvg not installed - pass png_fallback= instead

The saved package therefore contains two media parts per SVG picture — one.png and one .svg. That is expected, not duplication you should try to remove.

When png_fallback is omitted the SVG is rasterised withcairosvg, anoptional dependency. If it isn't installed you getpower_pptx._svg.CairoSvgUnavailable with an install hint — supplying your ownpng_fallback keeps installs slim and gives you control over how the raster version looks. left, top, width andheight behave exactly as in add_picture, with the extents defaulting to the PNG's native size.

Video and audio

shapes.add_movie(...) embeds a media file and registers the play controls. It is marked experimental upstream and carries real constraints, all of which come from the same root cause: the library does not decode the media file, it only packages it.

movie = slide.shapes.add_movie(
    "clip.mp4",
    Inches(1), Inches(1), Inches(6), Inches(3.375),   # 16:9, sized by you
    poster_frame_image="hero.jpg",
    mime_type="video/mp4",
)

print(movie.shape_type)                  # MEDIA (16)
print(movie.media_type)                  # MOVIE (3)
print(movie.poster_frame.content_type)   # image/jpeg
  • Size is mandatory. There is no auto-scaling as there is for pictures, because nothing here reads the video's dimensions. Work out the aspect ratio yourself — 6 × 3.375 inches for 16:9.
  • Pass the real mime_type. The default is"video/unknown"; the file is never sniffed. PowerPoint uses the declared type to pick a decoder, so "video/mp4", "video/quicktime","video/x-ms-wmv" and friends are what make playback work on the recipient's machine.
  • The poster frame is a still image you supply. It cannot be extracted from the video. Omit poster_frame_image and you get the bundled "media loudspeaker" icon — fine for audio, wrong for video. Read it back later viamovie.poster_frame, which returns an Image or None.
  • Playback needs the codec, not just the container. H.264 in MP4 is the safe choice across Windows, macOS and the web viewer. The file is embedded in the package, so the deck grows by the size of the video.

Audio uses the same call — pass an audio path and an audio MIME type, and skip the poster frame so the loudspeaker icon is used:

audio = slide.shapes.add_movie(
    "voiceover.m4a",
    Inches(0.5), Inches(0.5), Inches(0.8), Inches(0.8),
    mime_type="audio/mp4",
)

print(audio.media_type)          # MOVIE (3) - even for audio

One wrinkle worth knowing: media_type reports PP_MEDIA_TYPE.MOVIEunconditionally, including for audio, and shape_type isMSO_SHAPE_TYPE.MEDIA. Don't use media_type to tell audio from video — check the MIME type of the related media part instead. Play-control timing is registered on the enclosing slide even when the movie is added inside a group.

Alt text on pictures

A picture is the one shape whose meaning is completely invisible to a screen reader, soalt_text matters more here than anywhere else in a deck. It maps to thedescr attribute of the shape's <p:cNvPr> element — the same slot PowerPoint's Alt Text pane writes, so what you set here is what a reviewer sees in the pane:

pic = slide.shapes.add_picture("hero.jpg", Inches(1), Inches(1), width=Inches(4))

pic.alt_text = "Line chart: Q4 revenue up 18% year over year, driven by EMEA."
pic.title_text = "Q4 revenue"          # short label, complements alt_text

print(pic.alt_text)                    # round-trips through save/open
pic.alt_text = ""                      # assigning "" or None removes descr

Describe what the image tells the audience, not what it depicts: "Q4 revenue up 18%, driven by EMEA" is useful, "bar chart" is not. Reading returns "" when nothing has been set; assigning "" or None removes the attribute entirely.title_text is the companion short label (the title attribute) for when you want a one-line name as well as a description. Both round-trip through save and reopen.

Generated decks miss alt text by default, since nothing in the API forces you to write it.audit_accessibility(prs) in power_pptx.accessibility reports aMissingAltText issue at error severity for every picture with no description — the cheapest way to catch the gap before a deck ships. Note that an emptyalt_text is still reported: there is no "mark as decorative" flag, so a logo you genuinely want ignored will keep showing up in the report.