python-pptx
python-pptx copied to clipboard
feature: Picture.replace_image()
As discussed on the mailing group (https://groups.google.com/forum/?hl=en#!topic/python-pptx/YJBzJznTTHw), there are some potential uses for replacing an image with a new one. [Think "refreshing" a PPT with updated images.]
Some working code (largely suggested by Steve) is as follows:
from pptx import Presentation
from pptx.util import Px
from PIL import Image
# inputs
PPT_TEMPLATE = 'test.pptx'
REPLACEMENT_IMG = 'pic.png'
# load preso and shape
presso = Presentation(PPT_TEMPLATE)
slide = presso.slides[0]
img = slide.shapes[0]
# get current image info
imgPic = img._pic
imgRID = imgPic.xpath('./p:blipFill/a:blip/@r:embed', namespaces=imgPic.nsmap)[0]
imgPart = slide.related_parts[imgRID]
# get info about replacement image
with open(REPLACEMENT_IMG, 'rb') as f:
rImgBlob = f.read()
rImgWidth, rImgHeight = Image.open(REPLACEMENT_IMG).size
rImgWidth, rImgHeight = Px(rImgWidth), Px(rImgHeight) # change from Px
# replace
imgPart._blob = rImgBlob
# now alter the size and position to suit that of the replacement img
# rescale sizes so image isn't stretched
widthScale = float(rImgWidth) / img.width
heightScale = float(rImgHeight) / img.height
maxScale = max(widthScale, heightScale)
scaledImgWidth, scaledImgHeight = int(rImgWidth / maxScale), int(rImgHeight / maxScale)
# center the image if it's different size to the original
scaledImgLeft = int(img.left + (img.width - scaledImgWidth)/2)
scaledImgTop = int(img.top + (img.height - scaledImgHeight)/2)
# now update
img.left, img.top, img.width, img.height = scaledImgLeft, scaledImgTop, scaledImgWidth, scaledImgHeight
Hi Ian, as I reflect on this feature a little more, I'm thinking it's a little specialized. What do you think about being able to do something like this instead?
old_picture = somehow_get_the_picture_I_want_to_replace()
x, y, cx, cy = old_picture.left, old_picture.top, old_picture.width, old_picture.height
new_picture = shapes.add_picture(x, y, cx, cy, etc.)
old_picture.delete()
Hi Steve,
I think you're right in that it need not be all in one "replace_image()" function, as it can be coded up like you've suggested. If there's the functionality to delete an image (already available?), then it's relatively straight-forward for people to do, if they need to, without getting into the xml, etc.
Then again, it's always interesting to see what others actually think!
Kane
Hi @scanny,
This is quite an old thread and I was wondering if there is there any progress in terms of replacing/deleting a current image in a template?
R
I try the method,console print: imgRID = imgPic.xpath('./p:blipFill/a:blip/@r:embed', namespaces=imgPic.nsmap)[0] TypeError: xpath() got an unexpected keyword argument 'namespaces'
Because i want to keep the anim,just replace picture. Scanny, your method add_picture and delete old_picture can't do that
+1 would like this feature
Since animations are not important for my use cases, I would be satisfied to add a new picture, size/place it according to the old picture, and then just delete the old picture.
I can even accomplish the first two steps, I just can't seem to delete the old picture. When I try the code given above, I receive this error:
old_picture = somehow_get_the_picture_I_want_to_replace()
x, y, cx, cy = old_picture.left, old_picture.top, old_picture.width, old_picture.height
new_picture = shapes.add_picture(x, y, cx, cy, etc.)
old_picture.delete()
AttributeError: 'Picture' object has no attribute 'delete'
I like how simple the code above is. But I have also tried solving with even more lines of code I found in this issue; however, I run into the same error mentioned in the comments.
Does anyone know of a successful work around to delete an old picture?
Would still like to "remove" the old_picture somehow, but I am working around the issue for now by cropping out old_picture. I replaced the following line of code:
old_picture.delete()
with this
old_picture.crop_right = 1
So I am essentially just hiding the old image from view, so it technically is not removed or deleted.
Slide
object doesn't have related_parts
attribute, any work around for this??
Only a Part
subclass (like SlidePart
) will have .related_parts
. Use Slide.part
to get the SlidePart
object for that slide.
Back in the early days, a slide and its part were the same object, which might explain why some code here has that.
getting error on the line: imgRID = imgPic.xpath('./p:blipFill/a:blip/@r:embed', namespaces=imgPic.nsmap)[0]
TypeError: xpath() got an unexpected keyword argument 'namespaces'
Try leaving out the namespaces=..
argument:
imgRID = imgPic.xpath('./p:blipFill/a:blip/@r:embed')[0]
for the above issue i have removed namespaces parameter , now I am getting error on this line : imgPart = slide.related_parts[imgRID]
AttributeError: 'Slide' object has no attribute 'related_parts'. So i used slide.part instead. Now I am getting error.. TypeError: 'SlidePart' object is not subscriptable
Should be:
image_part = slide.part.related_parts[imgRID]
I believe.
ok. thank you
ImportError: cannot import name 'Px' from 'pptx' (/Usrsss/Xxxxx/anaconda3/lib/python3.7/site-packages/pptx/init.py)
while other pptx class are working.
while checking available options in pptx.util Length class, got to know that only available options are
Centipoints
Cm
Emu
Inches
Mm
Pt
So, Px will not work to get size. So in this line rImgWidth, rImgHeight = Px(rImgWidth), Px(rImgHeight)
use any of above method given. and in order to return size in pixel use directly: rImgWidth, rImgHeight = REPLACEMENT_IMG.size
Pixel size is platform dependent. On *nix systems it is often 72 px/inch and on Windows I believe it is (or at least was) 96 px/inch. So you're on your own to make that mapping. Whatever conventional size (inch, mm, etc.) you tell PowerPoint to make the image, it will make it so by scaling and that size will not vary between platforms.
Hi, I'm trying to replace a picture in Master Slide but it ignores (no errors are reported).
As suggested above, this works replacing an image in a regular slide:
from pptx import Presentation
from pptx.util import Cm
image1 = 'image1.jpg'
image2 = 'image2.jpg'
left = top = height = width = Cm(3)
prs = Presentation('Test-MasterWithPicture.pptx')
# Add slide:
slide = prs.slides.add_slide(prs.slide_layouts[0])
# Add picture:
img = slide.shapes.add_picture(image1, left, top, width, height)
# Replace image:
with open(image2, 'rb') as f:
rImgBlob = f.read()
imgPic = img._pic
imgRID = imgPic.xpath('./p:blipFill/a:blip/@r:embed')[0]
imgPart = slide.part.related_parts[imgRID]
imgPart._blob = rImgBlob
prs.save('Test.pptx')
The result is the presentation with image2 instead image1, as expected.
But this, trying to replace the image previously present in the Master Slide, nothing happens (it is created with the Master Slide unchanged):
from pptx import Presentation
from pptx.util import Cm
image2 = 'image2.jpg'
prs = Presentation('Test-MasterWithPicture.pptx')
# Get master slide:
slide = prs.slide_master
# Get Master Slide picture: (the only shape in the master slide)
img = prs.slide_master.shapes[0]
# Replace image:
with open(image2, 'rb') as f:
rImgBlob = f.read()
imgPic = img._pic
imgRID = imgPic.xpath('./p:blipFill/a:blip/@r:embed')[0]
imgPart = slide.part.related_parts[imgRID]
imgPart._blob = rImgBlob
# Add one slide:
someslide = prs.slides.add_slide(prs.slide_layouts[0])
prs.save('Test.pptx')
Am I missing something?
Thanks!!
Update: I figured out why this happens: For each image that is used in a presentation, a physical copy is kept in the folder ppt > media. However, if an image is copied in multiple slides, only one corresponding image file is kept in the media folder and all the slides using it are associated with it using the corresponding xml in ppt > slides > _rels. If however, the copied image is replaced using the Change Picture functionality of PPT, then a new file is created in the media folder and the corresponding slide is linked to that image.
Original message: Hi! I have a simple function to replace images (copied from the first post in this thread and copied here for your reference). It works fine, except if the image in the pptx template was originally copied from another image in the same powerpoint. In that case, the image gets replaced in the end with the source image that was copied, even though it has a different name.
To explain better, consider a powerpoint template with two slides and an image in each, named "Image1" and "Image2". I also have two pngs called "Image1.png" and "Image2.png", which have to replace the corresponding image after running my code. However, if Image1 in the powerpoint template is a copy of image2, the resulting presentation will have Image2.png on both slides. Note that while debugging, everything seems to work fine (correct png path is loaded) but the resulting pptx is incorrect. I have also compared the xml in both cases for the problematic slide and it is the same (only the size of the bounding box is sightly different). This means that I have no way of verifying when looking at a template if it is going to result in the correct output.
Any idea why this happens? How does powerpoint keep track of the source of a copied image (I mean where in the xml is this reflected)?
Edit: It seems the order of slides matter. The images in the lower-numbered slide seem to match the corresponding ones in the higher-numbered slide.
def replace_img_slide(slide, img, img_path):
# Replace the picture in the shape object (img) with the image in img_path.
imgPic = img._pic
imgRID = imgPic.xpath('./p:blipFill/a:blip/@r:embed')[0]
imgPart = slide.part.related_parts[imgRID]
with open(img_path, 'rb') as f:
rImgBlob = f.read()
# replace
imgPart._blob = rImgBlob
Thanks in advance!
p.s. This is my first time posting! Please let me know if I am not including enough info or am overdoing it! I'm learning! Thanks!
to those who wants a solution, summing up from above comments
there are two solutions
Solution 1
new_shape = slide_obj.shapes.add_picture(
img_location,
Inches(shape_left),
Inches(shape_top),
Inches(shape_width),
Inches(shape_height),
)
old_pic = shape_obj._element
new_pic = new_shape._element
old_pic.addnext(new_pic)
old_pic.getparent().remove(old_pic)
slide_obj -> is the slide where you want to add picture
img_location -> path to the image file
shape_obj -> existing picture shape object
shape_left, shape_top, shape_width, shape_height -> are self explanatory
Solution 2:
def _replace_picture(self, slide_obj, shape_obj, img_location):
# noinspection PyProtectedMember
with open(img_location, "rb") as file_obj:
r_img_blob = file_obj.read()
img_pic = shape_obj._pic
img_rid = img_pic.xpath("./p:blipFill/a:blip/@r:embed")[0]
img_part = slide_obj.part.related_parts[img_rid]
img_part._blob = r_img_blob
in this method we’re replacing blob, there'll be a problem with this method when you have a requirement to replicate current slide in a loop.
For example, let’s say you want to replicate a slide containing image for 5 times. Each time you want to put 5 different images namely a, b, c, d, e
So, what’ll happen with this method is you’ll see the last image i.e. ‘e’ in all 5 slides. why ? because we’re replacing blob so the latest blob will be put everywhere
One issue that I am having is this:
Let's suppose I have a slide that has a copy of the same image, so for example the slide would have two dog
photos. If I want to replace just one of those images, I am unable to do so since the images have the same shape._element.blip_rId
attributes. So when I try:
shape = slide.shapes[1] # this is the first dog photo
new_pptx_img = pptx.parts.image.Image.from_file('<image-path>')
slide_part, rId = shape.part, shape._element.blip_rId
image_part = slide_part.related_part(rId)
image_part.blob = new_pptx_img._blob**
It is replacing both dog
photos rather than just the one that I want. Is there a way to select just the one photo??
Here is a sample repo explaining my issue: https://github.com/ethanj20/sample-python-pptx
@lokesh1729 @scanny
One issue that I am having is this:
Let's suppose I have a slide that has a copy of the same image, so for example the slide would have two
dog
photos. If I want to replace just one of those images, I am unable to do so since the images have the sameshape._element.blip_rId
attributes. So when I try:shape = slide.shapes[1] # this is the first dog photo new_pptx_img = pptx.parts.image.Image.from_file('<image-path>') slide_part, rId = shape.part, shape._element.blip_rId image_part = slide_part.related_part(rId) image_part.blob = new_pptx_img._blob**
It is replacing both
dog
photos rather than just the one that I want. Is there a way to select just the one photo??Here is a sample repo explaining my issue: https://github.com/ethanj20/sample-python-pptx
@lokesh1729 @scanny
Any update?
@roylanmartinez
I tried two ways. One is replacing blob, as you mentioned, the problem is when you replace the blob, it gets replaced in both.
def _replace_picture(self, slide_obj, shape_obj, img_location):
# noinspection PyProtectedMember
with open(img_location, "rb", encoding="utf-8") as file_obj:
r_img_blob = file_obj.read()
img_part = self._get_picture_img_part(slide_obj, shape_obj)
img_part._blob = r_img_blob
The other way I tried is a hacky way of adding another image on top of the existing image.
# When the slide is duplicated multiple times the same image will be replaced
# in every place.
# So, below is another method to add the image on top of the image placeholder using the dimensions
# of the placeholder. So, totally there will be two images placed upon each other.
new_shape = slide_obj.shapes.add_picture(
img_location,
pptx.util.Inches(shape_left),
pptx.util.Inches(shape_top),
pptx.util.Inches(shape_width),
pptx.util.Inches(shape_height),
)
old_pic = shape_obj._element
new_pic = new_shape._element
old_pic.addnext(new_pic)
old_pic.getparent().remove(old_pic)