scanny / python-pptx

Create Open XML PowerPoint documents in Python
MIT License
2.44k stars 528 forks source link

Fitting images to placeholder #176

Open anki-xyz opened 9 years ago

anki-xyz commented 9 years ago

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 commented 9 years ago

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.

spex66 commented 8 years ago

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) 
vjain419 commented 7 years ago

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

scanny commented 7 years ago

No

vjain419 commented 7 years ago

any plans to add it next version?

aatwork commented 7 years ago

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

wikiped commented 7 years ago

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.

scanny commented 7 years ago

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:

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?

aatwork commented 7 years ago

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

wikiped commented 7 years ago

@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.

relativistic commented 7 years ago

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

mcdevitts commented 6 years ago

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

spurra commented 6 years ago

Any update on if this will be implemented soon?

sclem commented 6 years ago

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
mcdevitts commented 6 years ago

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
HarveySummers commented 6 years ago

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

twinters commented 6 years ago

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
aatwork commented 6 years ago

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

solarjoe commented 2 years ago

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.