Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to duplicate a shape #533

Open
sevire opened this issue Jul 17, 2019 · 10 comments
Open

Ability to duplicate a shape #533

sevire opened this issue Jul 17, 2019 · 10 comments

Comments

@sevire
Copy link

sevire commented Jul 17, 2019

My use case is that I want to use a shape created by the user as a template for other shapes created programmatically. Specifically I am generating a timeline from a spreadsheet and each point in the timeline comprises a milestone shape, a text box and a line which drops down to the date.

I want the user to design and format each of the template shapes and then my programme will duplicate the shapes for each point on the timeline, so retaining all the color, size, shape type and other attributes.

I am having to explicitly read all the formatting information from the shape, store it and then create a brand new shape and then apply all the attributes, so I am probably duplicating quite a lot of the work that would need to be done in duplicating a shape.

@sevire
Copy link
Author

sevire commented Jul 17, 2019

Note I'd quite like to help with implementing this if it's seen as appropriate, although I've never contributed to an open source project before so may need some help!

@mszbot
Copy link

mszbot commented Aug 9, 2019

This would be a great feature to have.

Looks like someone tried to implement a 'shape_clone()' method in the past. No idea if it works. #307

Might be a good starting point?

@scanny
Copy link
Owner

scanny commented Aug 10, 2019

I think a key first question would be whether the shape is free-form or just selected from one of the 168-or-so preset shapes like rectangle and star.

If it's a free-form shape with only straight-line segments, you could just store vertices and use the existing free-form shape API.

Also, each autoshape (i.e. rect or star or whatever but not picture or line) has its own textframe, so you may not need to have a separate textbox shape. You may need to do some hacking to get the text to show outside the shape bounds though.

Copying a shape is generally easily done with a deepcopy() of its XML, something like this:

import copy
from pptx.shapes.autoshape import Shape

def clone_shape(shape):
    """Add a duplicate of `shape` to the slide on which it appears."""
    # ---access required XML elements---
    sp = shape._sp
    spTree = sp.getparent()
    # ---clone shape element---
    new_sp = copy.deepcopy(sp)
    # ---add it to slide---
    spTree.append(new_sp)
    # ---create a proxy object for the new sp element---
    new_shape = Shape(new_sp, None)
    # ---give it a unique shape-id---
    new_shape.shape_id = shape.shape_id + 1000
    # ---return the new proxy object---
    return new_shape

The key thing is to maintain unique shape-ids for each shape, otherwise you get a repair error.

Hyperlinks on shapes or embedded in shape text can also be a problem requiring more sophisticated solutions, but as long as you avoid those a solution like this should work. Also, if the shape is a child of a group-shape you'll need something more sophisticated.

@lokesh1729
Copy link

lokesh1729 commented Dec 7, 2019

@scanny : i wanted to duplicate table GraphicFrame object which contains table, what changes should i need to do to above code ? kindly suggest...

@lokesh1729
Copy link

@scanny : also, shape_id is a read-only attribute, how can we set that ?

https://sourcegraph.com/github.com/scanny/python-pptx/-/blob/pptx/shapes/base.py#L180

@lokesh1729
Copy link

never mind, i modified the above function like this and it worked

def clone_shape(shape, left, top, width, height):
        """Add a duplicate of `shape` to the slide on which it appears."""
        shape_obj = shape.element
        sp_tree = shape_obj.getparent()
        new_sp = copy.deepcopy(shape_obj)
        sp_tree.append(new_sp)
        new_shape = (
            GraphicFrame(new_sp, None)
            if isinstance(shape, GraphicFrame)
            else Shape(new_sp, None)
        )
        new_shape.left = left
        new_shape.top = top
        new_shape.width = width
        new_shape.height = height
        return new_shape

@lokesh1729
Copy link

@scanny : my above code throwing repair error, is there any way to add shape_id to new_shape ?? i tried to set by new_shape.shape_id = <some random number> and new_shape._element.shape_id = <some random number> but not able to, it's throwing can't set attribute... is there any other way...

also referring to your code above, GraphicFrame doesn't have _sp , what to use for it ?

@lokesh1729
Copy link

@scanny : i am assuming that setting shape_id is not possible as there might have some design issues hence it was set as read-only attribute.

Is there a way to add a table to the slide using the same properties of existing table so that new table should look exactly like the previous one ???

something like this

existing_table = slide_obj.shapes[2]
properties = get_properties(existing_table) # some imaginary function
new_table = slide_obj.shapes.add_table(**properties)

@lokesh1729
Copy link

to sum-up my issue,

i have a table in a slide, i want to add a copy of that table adjacent to it... i tried above clone_shape code but it ends up with repair error, i cannot assign shape id to the new shape due to restriction by this library...

so, now i came up with this code

import copy
import Renderer

# Renderer is my class which has text renderer,
# table renderer etc and also it has some utility
# methods such as _get_slide_by_id etc...


obj = Renderer("/path/to/my/template.pptx")
slide = obj._get_slide_by_id(317)
shape = obj._get_shape_by_id(slide, 13)
new_shape = slide.shapes.add_table(
    len(shape.table.rows),
    len(shape.table.columns),
    shape.width + shape.left,
    shape.top,
    shape.width,
    shape.height,
)
new_shape._element = copy.deepcopy(shape._element)
obj.save("/tmp/sample.pptx")

but the table properties are not copied to the new shape, i even tried

new_shape.table._tbl = copy.deepcopy(shape.table._tbl)
new_shape.table._graphic_frame = copy.deepcopy(shape.table._graphic_frame)

can anyone help

@lokesh1729
Copy link

#589 i forked this repo and did these changes, with this it's working

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants