scanny / python-pptx

Create Open XML PowerPoint documents in Python
MIT License
2.26k stars 499 forks source link

Wrong position and size for grouped shapes #899

Open baevpetr opened 1 year ago

baevpetr commented 1 year ago

Hello, @scanny!

For a project, I'm trying to group/cluster/build a hierarchy of shapes on slides. Trying to do that using positions of a given shape and its sizes. For visualizing I'm using matplotlib library.

Here are screenshots from a .pptx file under investigation:

Screenshot 2023-07-04 at 02 09 42 Screenshot 2023-07-04 at 02 09 46

And here are plots with lines that represent the boundaries of every given shape on every slide:

1_1 1_2

Here enclosing boxes are group shapes with their own positions and sizes. For group shapes it's fine, but it's not fine for nested shapes...

Code that I'm using for correct coordinates:

def get_all_shapes(slide):
    deq = deque(slide.shapes)
    shapes = []

    while deq:
        cur_shape = deq.popleft()
        if cur_shape.shape_type == MSO_SHAPE_TYPE.GROUP:
            outer_shape = cur_shape
            inner_shapes = cur_shape.shapes

            min_y = min([shape.top for shape in inner_shapes])
            min_x = min([shape.left for shape in inner_shapes])

            max_y = max([shape.top + shape.height for shape in inner_shapes])
            max_x = max([shape.left + shape.width for shape in inner_shapes])

            # max(1, *) for lines, arrows, thin shapes
            original_height = max(1, max_y - min_y)
            original_width = max(1, max_x - min_x)

            height_coeff = outer_shape.height / original_height
            width_coeff = outer_shape.width / original_width

            for inner_shape in inner_shapes:
                inner_shape.height = int(height_coeff * inner_shape.height)
                inner_shape.width = int(width_coeff * inner_shape.width)

                inner_shape.top = outer_shape.top + int(height_coeff * (inner_shape.top - min_y))
                inner_shape.left = outer_shape.left + int(width_coeff * (inner_shape.left - min_x))

            deq.extend(inner_shapes)
            shapes.append(outer_shape)
        else:
            shapes.append(cur_shape)

    return shapes

We calculate scaling coefficients and use them to find out the correct size and position of every nested shape.

After that fix we have:

2_1 2_2

Am I doing something wrong and the library is capable of handling this?

sedrew commented 1 year ago

Hi, I'll take a look at your problem over the weekend. There is another option, you can create a template and use the library to access presentation objects and change their contents depending on the business logic.

scanny commented 8 months ago

@baevpetr this is an interesting problem, one I might need to address myself at some point. Can you be more specific about what the problem is?

Is it that scaling applied to a "container" shape is not directly reflected on a "contained" shape?

There is no mechanism for getting this from the library so far. I expect your approach is pretty much what is required if you need that sort of thing. It's kind of an edge-case requirement in that most users of the library would never have an interest in that sort of thing, but could be important for document indexing.

If you can say more I'd be happy to think more with you about how it might make sense to support this sort of thing in the general case.

A good place to start might be seeing what PowerPoint reports for position and size when you select a shape inside a scaled group.

YuMingtao0503 commented 8 months ago

In some cases, the position and size of group type shapes are incorrect, but I have found that even if they are in the wrong position, their relative positions are still relatively accurate. Therefore, there is a way to convert them

prs = Presentation("your_ppt.pptx")
slide = prs.slides[13]
for m,shape in enumerate(slide.shapes):  
    if shape.shape_type == 6:
        # true size and xy
        group_top_left_x = shape.left
        group_top_left_y = shape.top
        group_width = shape.width
        group_height = shape.height
        # false size and xy
        shape_top_left_x = min([ sp.left for sp in shape.shapes])
        shape_top_left_y = min([ sp.top for sp in shape.shapes])
        shape_width = max([sp.left + sp.width for sp in shape.shapes]) - shape_top_left_x
        shape_height = max([sp.top + sp.height for sp in shape.shapes]) - shape_top_left_y
        # scale xy
        px = group_width/shape_width
        py = group_height/shape_height
        group_shape_xy = []
        for n,sp in enumerate(shape.shapes):
            group_shape_left = (sp.left - shape_top_left_x) * group_width /shape_width + group_top_left_x
            group_shape_top = (sp.top - shape_top_left_y) * group_height /shape_height + group_top_left_y
            group_shape_width = sp.width * group_width / shape_width
            group_shape_height = sp.height * group_height/shape_height
            group_shape_xy.append([int(group_shape_left),int(group_shape_top),int(group_shape_width),int(group_shape_height)])

then group_shape_xysaved relatively real location information