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:
- deep-copies the source's non-picture shape XML, and
- 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
- Open an existing
.pptx.
- 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.
- 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).
- 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).
- Call
prs.save('out.pptx').
- 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.
Bug:
add_pictureon a new slide silently corrupts blipr:embedafter save when the source slide had its picture swapped via direct XML manipulationEnvironment
.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 fromPart.get_or_add_image_part), and I then build a new slide that:Shapes.add_picture()to insert its pictures,the new slide looks correct in memory (blip
r:embedvalues match the rIdsadd_pictureassigned in the new part's rels). However, afterPresentation.save()and reloading the file, the new slide's blipr:embedvalues are silently rewritten to the source slide's rIds. These rIds do not exist in the new slide'spart.rels, so any subsequent access viaslide.part.rels[rId]raisesKeyError, and PowerPoint shows missing/wrong images.Steps to reproduce
.pptx.image_part, new_rid = slide.part.get_or_add_image_part(new_image_path)blip.set(qn('r:embed'), new_rid)(whereblipis 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.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 callnew.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).new.part.relsand the blipr:embedvalues before save — they are consistent (blips point to rIds that exist innew.part.rels, pointing to the correct image parts).prs.save('out.pptx').Presentation('out.pptx')and inspect the same slide.Expected: blip
r:embedvalues remain the ones assigned byadd_picture, pointing to the new image parts.Actual: blip
r:embedvalues 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'spart.rels, soslide.part.rels[rId]raisesKeyError.Minimal reproducer
Expected output:
Actual output:
Notes / observations
r:embedvalues on the new slide are identical to the source slide's blipr:embedvalues at the moment ofsave()— as though something re-emitted the source's blip attributes into the new slide's XML on serialization.add_picture..pptxZIP directly (duplicateslideN.xml/slideN.xml.rels, patch[Content_Types].xml,ppt/_rels/presentation.xml.rels, andppt/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
.pptxopens 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 followsslide.part.rels[rId]raisesKeyError, which is how I originally noticed the problem.