python-pptx icon indicating copy to clipboard operation
python-pptx copied to clipboard

Fitting images to placeholder

Open anki-xyz opened this issue 10 years ago • 20 comments

Dear all,

I know that images are cropped to fit the placeholder container width and height. However, I'd like to use something like an automatic adjustment, meaning, if I load an image into the placeholder, the image should be resized/scaled to fit the placeholder having the aspect ratio locked. I was playing around with the placeholder.py file: Removing the cropping resizes the image to the full container width and height, however, when setting the width and height of the placeholder inside of that function, nothing happens. When setting the width after adding the image manually, the images disappear from the slide.

It would be great if there would a solution.

Thanks a lot for the help best anki

anki-xyz avatar Sep 02 '15 12:09 anki-xyz

I found a solution for the issue. Adding a new parameter called method to the insert_picture function I changed a few lines of code in \shapes\placeholder.py:

    def insert_picture(self, image_file, method = 'crop'):
        """
        Return a |PlaceholderPicture| object depicting the image in
        *image_file*, which may be either a path (string) or a file-like
        object. The image is cropped to fill the entire space of the
        placeholder. A |PlaceholderPicture| object has all the properties and
        methods of a |Picture| shape except that the value of its
        :attr:`~._BaseSlidePlaceholder.shape_type` property is
        `MSO_SHAPE_TYPE.PLACEHOLDER` instead of `MSO_SHAPE_TYPE.PICTURE`.
        """
        pic = self._new_placeholder_pic(image_file, method) # pass new parameter "method"
        self._replace_placeholder_with(pic)
        return PlaceholderPicture(pic, self._parent)

    def _new_placeholder_pic(self, image_file, method = 'crop'):
        """
        Return a new `p:pic` element depicting the image in *image_file*,
        suitable for use as a placeholder. In particular this means not
        having an `a:xfrm` element, allowing its extents to be inherited from
        its layout placeholder.
        """
        rId, desc, image_size = self._get_or_add_image(image_file)
        id_, name = self.id, self.name

        # Cropping the image, as in the original file
        if method == 'crop':
            pic = CT_Picture.new_ph_pic(id_, name, desc, rId)
            pic.crop_to_fit(image_size, (self.width, self.height))

        # Adjusting image to placeholder size and replace placeholder.     
        else:
            aspectImg = image_size[0]/image_size[1]
            aspectPh  = self.width / self.height

            if aspectPh > aspectImg:
                h = self.height
                w = int(aspectImg * self.height)

            else:
                w = self.width
                h = int(aspectImg * w)

            pic = CT_Picture.new_pic(id_, name, desc, rId, self.left, self.top, w, h)


        return pic

Fairly, quick and dirty, but does the job. Improvements and implementation are welcome.

anki-xyz avatar Sep 03 '15 11:09 anki-xyz

Hi @Anki11 ,

thx for digging into this and sharing of you hack. Exactly what I had need today. Cropping was not an option, if you need to add matplotlib charts to it :)

But after some fiddling and testing, this is my logic to calculate the final w/h. Instead of multiplying it with the aspectImg, I had to divide it for the else branch, that the output makes sense for me:

            ph_w, ph_h = self.width, self.height
            aspectPh = ph_w / ph_h

            img_w, img_h = image_size
            aspectImg = img_w / img_h

            if aspectPh > aspectImg:
                w = int(ph_h * aspectImg)
                h = ph_h # keep the height

            else:
                w = ph_w # keep the width
                h = int(ph_w / aspectImg) 

spex66 avatar Dec 21 '15 17:12 spex66

Just wondering is this solution in latest pptx code base? because i am still having this issue

vjain419 avatar Nov 22 '16 09:11 vjain419

No

scanny avatar Nov 22 '16 17:11 scanny

any plans to add it next version?

vjain419 avatar Nov 23 '16 09:11 vjain419

+1 If possible I would also like to see this in the official version. It works fine for me

aatwork avatar May 24 '17 16:05 aatwork

Alternatively here is insert_picture_with_fit_method branch, that will NOT change proportions of the image.

It will act almost the same way as original insert_picture method, but instead of stretching image and cropping the oversize parts it will shrink it, keeping aspect ratio of an image and all original content visible. Image will be then aligned in the center of the placeholder (as it happens with original insert_picture method).

This might be preferred when the proportions are critical.

wikiped avatar Jun 15 '17 08:06 wikiped

Okay, so let me see if I understand the behavior you're looking for. Here's what I think you're asking for:

Insert an image into a picture placeholder, such that:

  • the image is completely enclosed by the placeholder
  • the aspect ratio of the image is preserved
  • the image is as large as possible without extending beyond the bounds of the placeholder
  • any margin space remaining between either the top/bottom or left/right sides is distributed evenly between those sides (the image is centered in the short direction).

I'm thinking the basic idea here is to be able to specify the appropriate size for an image in the template deck, then fill it in with any variety of images without having to worry about formatting the image for proper visual size. Like you had a catalog of items and the picture for each item should appear the same size, but the source images are any variety of sizes. Is that about right?

scanny avatar Jun 15 '17 18:06 scanny

Thank you for looking into this.

This is a perfect description of what I am looking for. Intel Deutschland GmbH Registered Address: Am Campeon 10-12, 85579 Neubiberg, Germany Tel: +49 89 99 8853-0, www.intel.de Managing Directors: Christin Eisenschmid, Christian Lamprechter Chairperson of the Supervisory Board: Nicole Lau Registered Office: Munich Commercial Register: Amtsgericht Muenchen HRB 186928

aatwork avatar Jun 15 '17 19:06 aatwork

@scanny Thanks for looking into this and yes, this is basically what I have tried to implement in this patch. I have essentially only added one method _fit_resizing (based on your original _fill_cropping), which returns the same format of crop tuple to .blipFill.crop, but with negative values (to allow shrinking of the image).

Works fine so far with various matplotlib plots of varying sizes I have tried.

I thought boolean crop parameter would suffice for this kind of operation:

placeholder.insert_picture(image, crop=False)

crop defaults to True, so no impact on backward compatibility as far as I can see.

wikiped avatar Jun 15 '17 19:06 wikiped

I would also love this functionality. It would make automatically pushing plots to powerpoint much easier.

relativistic avatar Jun 20 '17 23:06 relativistic

Adding this functionality would be a huge plus and simplify dumping Matplotlib figures into PowerPoint.

mcdevitts avatar Nov 09 '17 05:11 mcdevitts

Any update on if this will be implemented soon?

spurra avatar Feb 15 '18 16:02 spurra

This is what I came up with. It replaces the placeholder with the image, setting the max size to the bounds of the placeholder and preserving aspect ratio. It also centers the image in the placeholder.

def replace_with_image(img, shape, slide):
    pic = slide.shapes.add_picture(img, shape.left, shape.top)

    #calculate max width/height for target size
    ratio = min(shape.width / float(pic.width), shape.height / float(pic.height))

    pic.height = int(pic.height * ratio)
    pic.width = int(pic.width * ratio)

    pic.left = shape.left + ((shape.width - pic.width) / 2)
    pic.top = shape.top + ((shape.height - pic.height) / 2)

    placeholder = shape.element
    placeholder.getparent().remove(placeholder)
    return

sclem avatar Feb 19 '18 18:02 sclem

I implemented adding images to SlidePlaceholders by doing this:


class SlidePlaceholder(_BaseSlidePlaceholder):
    """
    Placeholder shape on a slide. Inherits shape properties from its
    corresponding slide layout placeholder.
    """
    def insert_picture(self, image_file, crop=True):
        """
        Return a |PlaceholderPicture| object depicting the image in
        *image_file*, which may be either a path (string) or a file-like
        object. The image is cropped to fill the entire space of the
        placeholder. A |PlaceholderPicture| object has all the properties and
        methods of a |Picture| shape except that the value of its
        :attr:`~._BaseSlidePlaceholder.shape_type` property is
        `MSO_SHAPE_TYPE.PLACEHOLDER` instead of `MSO_SHAPE_TYPE.PICTURE`.
        """
        pic = self._new_placeholder_pic(image_file, crop) # pass new parameter "method"
        self._replace_placeholder_with(pic)
        return PlaceholderPicture(pic, self._parent)

    def _new_placeholder_pic(self, image_file, crop=True):
        """
        Return a new `p:pic` element depicting the image in *image_file*,
        suitable for use as a placeholder. In particular this means not
        having an `a:xfrm` element, allowing its extents to be inherited from
        its layout placeholder.
        """
        rId, desc, image_size = self._get_or_add_image(image_file)
        id_, name = self.shape_id, self.name

        # Cropping the image, as in the original file
        if crop:
            pic = CT_Picture.new_ph_pic(id_, name, desc, rId)
            pic.crop_to_fit(image_size, (self.width, self.height))

        # Adjusting image to placeholder size and replace placeholder.
        else:
            ph_w, ph_h = self.width, self.height
            aspectPh = ph_w / ph_h

            img_w, img_h = image_size
            aspectImg = img_w / img_h

            if aspectPh > aspectImg:
                w = int(ph_h * aspectImg)
                h = ph_h # keep the height
            else:
                w = ph_w # keep the width
                h = int(ph_w / aspectImg)

            top = self.top + (ph_h - h) / 2
            left = self.left + (ph_w - w) / 2

            pic = CT_Picture.new_pic(id_, name, desc, rId, self.left + (ph_w - w) / 2, self.top, w, h)
        return pic

    def _get_or_add_image(self, image_file):
        """
        Return an (rId, description, image_size) 3-tuple identifying the
        related image part containing *image_file* and describing the image.
        """
        image_part, rId = self.part.get_or_add_image_part(image_file)
        desc, image_size = image_part.desc, image_part._px_size
        return rId, desc, image_size

mcdevitts avatar Feb 21 '18 17:02 mcdevitts

@scanny Is the resize part of next release or is there a patch I can apply to resolve the resize issue?

HarveySummers avatar Jul 31 '18 23:07 HarveySummers

I liked and used the code @sclem posted, but I ran into the issue that when the image placeholder had some animations on them, it would lose these properties due to the placeholder being removed. I adapted his code and used the crop_bottom and equivalent methods (which seems to be recommended in the documentation) to create the following method. I'm just sharing the code in case other people run into this problem as well. I suspect the code can be optimised further, but this works for my use cases:

from PIL import Image

def _add_image(slide, placeholder_id, image_url):
    placeholder = slide.placeholders[placeholder_id]

    # Calculate the image size of the image
    im = Image.open(image_url)
    width, height = im.size

    # Make sure the placeholder doesn't zoom in
    placeholder.height = height
    placeholder.width = width

    # Insert the picture
    placeholder = placeholder.insert_picture(image_url)

    # Calculate ratios and compare
    image_ratio = width / height
    placeholder_ratio = placeholder.width / placeholder.height
    ratio_difference = placeholder_ratio - image_ratio

    # Placeholder width too wide:
    if ratio_difference > 0:
        difference_on_each_side = ratio_difference / 2
        placeholder.crop_left = -difference_on_each_side
        placeholder.crop_right = -difference_on_each_side
    # Placeholder height too high
    else:
        difference_on_each_side = -ratio_difference / 2
        placeholder.crop_bottom = -difference_on_each_side
        placeholder.crop_top = -difference_on_each_side

twinters avatar Aug 13 '18 23:08 twinters

I have created a pull request that is based on the previous comments: https://github.com/scanny/python-pptx/pull/439

aatwork avatar Sep 20 '18 13:09 aatwork

I came across the same issue. What I am looking for is the equivalent to "Fit" in the UI:

grafik

I saved ppts, one with fit, the other with fill and had a look at the source. I could only find a relevant diff in the xml of the slide:

grafik

I am wondering why the values for fit are negative and if there is a way to mimick this behavior without resizing the image or the placeholder.

That's probably what is mentioned here:

This could be elaborated to "center" the image within the original space and perhaps to use "negative cropping" to retain the original placeholder size.

solarjoe avatar Jul 30 '22 05:07 solarjoe

Hi, apologies for reviving an old thread. Since the issue is still open, I assume nothing has been implemented yet.

I just wanted to say that I’d really love to see this functionality as well. Are there any plans to implement this feature?

darrencl avatar Sep 05 '25 05:09 darrencl