scanny / python-pptx

Create Open XML PowerPoint documents in Python
MIT License
2.37k stars 513 forks source link

presentation.save() error in 0.6.21 #763

Closed brownmk closed 2 years ago

brownmk commented 2 years ago

Below is my code to clone one slide in an presentation, then save the presentation as a new file. The code works in 0.6.18, but failed in 0.6.21. I read the following post:

The issue is related to https://github.com/scanny/python-pptx/issues/754

Change in bold were made to my code based on the above discussion and it now fails at the last prs.save() step.

Input test file is: Debug.template.pptx

Thank you so much for maintaining this awesome library.

The error is:

0.6.21 Traceback (most recent call last): File "./t.py", line 41, in prs.save("new.pptx") File "/usr/local/lib/python3.8/site-packages/pptx/presentation.py", line 39, in save self.part.save(file) File "/usr/local/lib/python3.8/site-packages/pptx/parts/presentation.py", line 107, in save self.package.save(path_or_stream) File "/usr/local/lib/python3.8/site-packages/pptx/opc/package.py", line 153, in save PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts())) File "/usr/local/lib/python3.8/site-packages/pptx/opc/serialized.py", line 76, in write cls(pkg_file, pkg_rels, parts)._write() File "/usr/local/lib/python3.8/site-packages/pptx/opc/serialized.py", line 83, in _write self._write_parts(phys_writer) File "/usr/local/lib/python3.8/site-packages/pptx/opc/serialized.py", line 104, in _write_parts phys_writer.write(part.partname.rels_uri, part.rels.xml) File "/usr/local/lib/python3.8/site-packages/pptx/opc/package.py", line 597, in xml rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, rel.is_external) File "/usr/local/lib/python3.8/site-packages/pptx/opc/oxml.py", line 98, in add_rel relationship = CT_Relationship.new(rId, reltype, target, target_mode) File "/usr/local/lib/python3.8/site-packages/pptx/opc/oxml.py", line 82, in new relationship.target_ref = target File "/usr/local/lib/python3.8/site-packages/pptx/oxml/xmlchemy.py", line 268, in set_attr_value str_value = self._simple_type.to_xml(value) File "/usr/local/lib/python3.8/site-packages/pptx/oxml/simpletypes.py", line 26, in to_xml cls.validate(value) File "/usr/local/lib/python3.8/site-packages/pptx/oxml/simpletypes.py", line 118, in validate cls.validate_string(value) File "/usr/local/lib/python3.8/site-packages/pptx/oxml/simpletypes.py", line 70, in validate_string raise TypeError("value must be a string, got %s" % type(value)) TypeError: value must be a string, got <class 'pptx.parts.slide.SlideLayoutPart'>

Code:

import copy
import pptx

def copy_slide(pres, slide_index):
    def _get_blank_slide_layout(pres):
         layout_items_count = [len(layout.placeholders) for layout in pres.slide_layouts]
         min_items = min(layout_items_count)
         blank_layout_id = layout_items_count.index(min_items)
         return pres.slide_layouts[blank_layout_id]

    blank_slide_layout = _get_blank_slide_layout(pres)
    source=pres.slides[slide_index]
    dest = pres.slides.add_slide(blank_slide_layout)

    for shp in source.shapes:
        el = shp.element
        newel = copy.deepcopy(el)
        dest.shapes._spTree.insert_element_before(newel, 'p:extLst')

    # incompatibility introduce at 0.6.21
    # see https://github.com/scanny/python-pptx/issues/754
    **rels = source.part.rels
    old_ver=True
    if [int(x) for x in pptx.__version__.split(".")] >= [0, 6, 21]:
        rels=rels._rels
        old_ver=False**

    #for key, value in source.part.rels.items():
    for key, value in rels.items():
        # Make sure we don't copy a notesSlide relation as that won't exist
        if not "notesSlide" in value.reltype:
            if old_ver:
                dest.part.rels.add_relationship(value.reltype, value._target, value.rId)
            else:
                **dest.part.rels._add_relationship(value.reltype, value._target, value.rId)**

print(pptx.__version__)
prs=pptx.Presentation('Debug.template.pptx')
copy_slide(prs, 0)
prs.save("new.pptx")
scanny commented 2 years ago

The new Relationships._add_relationship() has a different signature than the old one:
https://github.com/scanny/python-pptx/blob/master/pptx/opc/package.py#L600

In particular, the new one is: ._add_relationship(self, reltype, target, is_external=False). So when you assign the (truthy) rId to is_external the writer is looking for a URL (like for a hyperlink) in the target value and hence the error that value._target is not a string.

If you want to specify the rId, which I suppose you do, then you'll need this line instead of the ._add_relationship() line:

dest.part.rels._rels[value.rId] = _Relationship(
    dest.part.rels._base_uri,
    value.rId,
    value.reltype,
    target_mode=RTM.EXTERNAL if value.is_external else RTM.INTERNAL,
    target=value.target,
)
brownmk commented 2 years ago

Thank you for the quick reply. Now the last line triggers an error (value.target is wrong):

target=value.target,

AttributeError: '_Relationship' object has no attribute 'target'

Thank you!

brownmk commented 2 years ago

Looks like I need to use value._target, just to confirm.

brownmk commented 2 years ago

It will be great if the copy_slide (or clone_slide) method is natively supported in the module. Thank you!

scanny commented 2 years ago

Yeah, that's not likely to happen soon, it's more complicated in the general case, like when the slide contains a chart you need to clone not only the slide part but also the chart part and the excel part that underlies the chart, so a simple approach like this one isn't going to work for that.

But glad you got it working :)