Skip to content

add_picture on new slide silently corrupts blip r:embed after save when source slide was modified via direct XML swap #1142

Description

@ccndb1

Bug: add_picture on a new slide silently corrupts blip r:embed after save when the source slide had its picture swapped via direct XML manipulation

Environment

  • python-pptx version: 1.0.2
  • Python version: 3.10.12
  • OS: Ubuntu 22.04 (also reproduced on Windows)
  • File format: .pptx (Office Open XML)

Summary

When a source slide's picture has been modified by directly setting its <a:blip r:embed="…"> to a new relationship ID (obtained from Part.get_or_add_image_part), and I then build a new slide that:

  1. deep-copies the source's non-picture shape XML, and
  2. uses Shapes.add_picture() to insert its pictures,

the new slide looks correct in memory (blip r:embed values match the rIds add_picture assigned in the new part's rels). However, after Presentation.save() and reloading the file, the new slide's blip r:embed values are silently rewritten to the source slide's rIds. These rIds do not exist in the new slide's part.rels, so any subsequent access via slide.part.rels[rId] raises KeyError, and PowerPoint shows missing/wrong images.

Steps to reproduce

  1. Open an existing .pptx.
  2. Pick a source slide that has a picture shape; swap that picture's image by:
    • image_part, new_rid = slide.part.get_or_add_image_part(new_image_path)
    • blip.set(qn('r:embed'), new_rid) (where blip is the <a:blip> under the picture shape)
      This adds new relationships (e.g. rId7, rId8) to the source slide's part and points its shape blips at them.
  3. Create a new blank slide via prs.slides.add_slide(layout), clear its default shape tree, deep-copy the source's <p:sp> (text box) elements into the new slide's shape tree, and call new.shapes.add_picture(path, left, top, width, height) for each picture (with a different image path for the pictures you want to replace, and the source's original image blob for decorative pictures like logos and strips).
  4. Inspect new.part.rels and the blip r:embed values before save — they are consistent (blips point to rIds that exist in new.part.rels, pointing to the correct image parts).
  5. Call prs.save('out.pptx').
  6. Reload with Presentation('out.pptx') and inspect the same slide.

Expected: blip r:embed values remain the ones assigned by add_picture, pointing to the new image parts.

Actual: blip r:embed values on the new slide have been replaced with the source slide's blip rIds (the ones set in step 2, e.g. rId7, rId8). Those rIds are not present in the new slide's part.rels, so slide.part.rels[rId] raises KeyError.

Minimal reproducer

import copy
from pptx import Presentation
from pptx.util import Inches
from pptx.oxml.ns import qn

SRC     = 'input.pptx'         # any pptx with at least one slide containing a picture
OUT     = 'out.pptx'
IMG_A   = 'a.jpg'              # image used for the source-slide swap
IMG_B   = 'b.jpg'              # image used for the new-slide add_picture (must differ from IMG_A)

def blip_of(shape):
    return shape._element.find('.//' + qn('a:blip'))

prs = Presentation(SRC)

# --- Step 1: swap image on source slide's first picture shape via direct XML ---
src = prs.slides[0]                                       # pick any slide with a picture
pic = next(s for s in src.shapes if s.shape_type == 13)   # first picture shape
_, new_rid = src.part.get_or_add_image_part(IMG_A)
blip_of(pic).set(qn('r:embed'), new_rid)

# --- Step 2: build a new slide that copies source's non-picture shapes and
#             adds a NEW picture via add_picture ---
new = prs.slides.add_slide(prs.slide_layouts[6])
tree = new.shapes._spTree
for c in list(tree):
    tree.remove(c)

for sh in src.shapes:
    if sh.shape_type == 13:  # PICTURE
        # For the picture we want to replace, use a different image path:
        new.shapes.add_picture(IMG_B, sh.left, sh.top, sh.width, sh.height)
    else:
        tree.append(copy.deepcopy(sh._element))

# Before save: blip on the new slide references an rId that exists in new.part.rels
new_pic = next(s for s in new.shapes if s.shape_type == 13)
rid_before = blip_of(new_pic).get(qn('r:embed'))
print('before save, new blip r:embed =', rid_before,
      '→', new.part.rels[rid_before].target_part.partname)

prs.save(OUT)

# After save+reload: blip r:embed on the new slide is now the SOURCE's rId,
# which does not exist in the new slide's part.rels → KeyError.
prs2 = Presentation(OUT)
new2 = prs2.slides[-1]
new_pic2 = next(s for s in new2.shapes if s.shape_type == 13)
rid_after = blip_of(new_pic2).get(qn('r:embed'))
print('after reload, new blip r:embed =', rid_after)
print('rels on reloaded new slide     =', list(new2.part.rels.keys()))
try:
    print('resolved target               =', new2.part.rels[rid_after].target_part.partname)
except KeyError as e:
    print('KeyError on rels lookup       :', e)

Expected output:

before save, new blip r:embed = rIdX → /ppt/media/<b.jpg>
after reload, new blip r:embed = rIdX
resolved target               = /ppt/media/<b.jpg>

Actual output:

before save, new blip r:embed = rIdX → /ppt/media/<b.jpg>
after reload, new blip r:embed = rIdY          # source's rId, not rIdX
rels on reloaded new slide     = ['rId1', 'rId2', …]   # rIdY not in this list
KeyError on rels lookup       : "no relationship with key 'rIdY'"

Notes / observations

  • The corruption happens only on the new slide's blips; the source slide is fine after save+reload.
  • The corrupted r:embed values on the new slide are identical to the source slide's blip r:embed values at the moment of save() — as though something re-emitted the source's blip attributes into the new slide's XML on serialization.
  • Direct blip-swap on an existing (non-copied) slide does persist correctly through save+reload; the bug appears only when the modified source slide is later used to build a new slide that also uses add_picture.
  • Workaround I used: bypass python-pptx for the slide duplication step and manipulate the .pptx ZIP directly (duplicate slideN.xml / slideN.xml.rels, patch [Content_Types].xml, ppt/_rels/presentation.xml.rels, and ppt/presentation.xml), then reopen with python-pptx for further edits. This produces a valid file with correct rIds.

Impact

Silent corruption — no exception is raised during save. The resulting .pptx opens without complaint but the affected pictures on the new slide either display the wrong image or are missing, depending on the reader. Downstream code that follows slide.part.rels[rId] raises KeyError, which is how I originally noticed the problem.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions