David Zemens
David Zemens

Reputation: 53663

C# OpenXML SDK - Inserting a new slide from slide masters

I am attempting to implement the solutions given here and/or here.

I have a .pptx file that contains zero slides initially. One of the layouts is named "One content". For now, I just want to produce a new PPTX file with a single slide based on this layout. Should be trivial, no? No, apparently not.

In file OpenXmlUtils.cs I have the following method which I use to create a new PPTX from the "template" file:

public static void CopyTemplate(string template, string target)
{
    string targetPath = Path.GetFullPath(target);
    string targetFolder = Path.GetDirectoryName(targetPath);
    if (!System.IO.Directory.Exists(targetFolder))
    {
        System.IO.Directory.CreateDirectory(targetFolder);
    }
    System.IO.File.Copy(template, targetPath, true);
}

My PPTWriter.cs broken down to MCVE:

public PPTOpenXMLWriter(string templatePath, string presSaveAsPath)
{
    if (File.Exists(presSaveAsPath)) { File.Delete(presSaveAsPath); }

    OpenXmlUtils.CopyTemplate(templatePath, presSaveAsPath);

    _createPresentation(presSaveAsPath);

}

private void _createPresentation(string presSaveAsPath)
{
    using (PresentationDocument presentationDocument = PresentationDocument.Open(presSaveAsPath, true))
    {

        string layoutName = "One content";

        _insertNewSlide(presentationDocument.PresentationPart, layoutName);

        presentationDocument.Save();
    }
}    

private void _insertNewSlide(PresentationPart presentationPart, string layoutName)
{
    Slide slide = new Slide(new CommonSlideData(new ShapeTree()));
    SlidePart slidePart = presentationPart.AddNewPart<SlidePart>();
    slide.Save(slidePart);
    SlideMasterPart slideMasterPart = presentationPart.SlideMasterParts.FirstOrDefault();
    SlideLayoutPart slideLayoutPart = slideMasterPart.SlideLayoutParts.SingleOrDefault
            (sl => sl.SlideLayout.CommonSlideData.Name.Value.Equals(layoutName, StringComparison.OrdinalIgnoreCase));
    slidePart.AddPart<SlideLayoutPart>(slideLayoutPart);
    slidePart.Slide.CommonSlideData = (CommonSlideData)slideLayoutPart.SlideLayout.CommonSlideData.Clone();

    SlideIdList slideIdList = null;
    if ( presentationPart.Presentation.SlideIdList is null)
    {
        presentationPart.Presentation.SlideIdList = new SlideIdList();
    }
    slideIdList = presentationPart.Presentation.SlideIdList;
    // find the highest id
    uint maxSlideId = 0;
    if (slideIdList.ChildElements.Count() > 0)
        maxSlideId = slideIdList.ChildElements
            .Cast<SlideId>()
            .Max(x => x.Id.Value);

    // Insert the new slide into the slide list after the previous slide.
    SlideId newSlideId = new SlideId();
    slideIdList.Append(newSlideId);
    newSlideId.Id = maxSlideId;
    newSlideId.RelationshipId = presentationPart.GetIdOfPart(slidePart);

    // Save the modified presentation.
    presentationPart.Presentation.Save();
}

The resulting file is corrupt and needs to be "repaired" by PowerPoint, after which repair process the slide layout is not the layout that was specified. In fact it's a completely different layout with a radically different XML structure and all I can gather is that it's somehow defaulting back to the ordinally first layout in the master ("Title"), because it doesn't know how to handle whatever it's actually been given via OpenXML.

This seems like it ought to be a fairly common use-case, and perhaps my expectations are wrong, but it seems like given an already existing slide layout, you ought to be able to (relatively easily) create a new slide based on that layout which will contain all of the same placeholder shapes, etc.

Upvotes: 2

Views: 2978

Answers (2)

Matt Fitzmaurice
Matt Fitzmaurice

Reputation: 1426

Got it. The following is working for my test scenarios (thanks to your code for help):

    presentationPart.InsertNewSlide("CV Full page");
    presentationPart.InsertNewSlide("CV Half page");
    presentationPart.InsertNewSlide("Credential full page");
    presentationPart.InsertNewSlide("CV or Credential 5 to a page", 3);

    public static void InsertNewSlide(this PresentationPart presentationPart, string layoutName, int? position = null)
    {
        Slide slide = new Slide();
        SlidePart slidePart = presentationPart.AddNewPart<SlidePart>();
        slide.Save(slidePart);

        SlideMasterPart slideMasterPart = presentationPart.SlideMasterParts.FirstOrDefault();
        SlideLayoutPart slideLayoutPart = slideMasterPart.GetSlideLayoutPartByLayoutName(layoutName);

        slidePart.AddPart(slideLayoutPart, slideMasterPart.GetIdOfPart(slideLayoutPart));
        slidePart.Slide.CommonSlideData = (CommonSlideData)slideLayoutPart.SlideLayout.CommonSlideData.Clone();

        string id = slideMasterPart.GetIdOfPart(slideLayoutPart);
        slidePart.CloneSlideLayout(slideLayoutPart, id);

        slideMasterPart.AddPart(slideLayoutPart, id);
        presentationPart.SetSlideID(slidePart, position);
    }

    public static void SetSlideID(this PresentationPart presentationPart, SlidePart slidePart, int? position = null)
    {
        SlideIdList slideIdList = presentationPart.Presentation.SlideIdList;
        if (slideIdList == null)
        {
            slideIdList = new SlideIdList();
            presentationPart.Presentation.SlideIdList = slideIdList;
        }

        if (position != null && position > slideIdList.Count())
            throw new InvalidOperationException($"Unable to set slide to position '{position}'. There are only '{slideIdList.Count()}' slides.");

        uint newId = slideIdList.ChildElements.Count() == 0 ? 256 : slideIdList.GetMaxSlideId() + 1;
        if (position == null)
        {
            var newSlideId = slideIdList.AppendChild(new SlideId());
            newSlideId.Id = newId;
            newSlideId.RelationshipId = presentationPart.GetIdOfPart(slidePart);
        }
        else
        {
            SlideId nextSlideId = (SlideId)slideIdList.ChildElements[position.Value - 1];
            var newSlideId = slideIdList.InsertBefore(new SlideId(), nextSlideId);
            newSlideId.Id = newId;
            newSlideId.RelationshipId = presentationPart.GetIdOfPart(slidePart);
        }
    }

    public static uint GetMaxSlideId(this SlideIdList slideIdList)
    {
        uint maxSlideId = 0;
        if (slideIdList.ChildElements.Count() > 0)
            maxSlideId = slideIdList.ChildElements
                .Cast<SlideId>()
                .Max(x => x.Id.Value);
        return maxSlideId;
    }

    public static SlideLayoutPart GetSlideLayoutPartByLayoutName(this SlideMasterPart slideMasterPart, string layoutName)
    {
        return slideMasterPart.SlideLayoutParts.SingleOrDefault
                (sl => sl.SlideLayout.CommonSlideData.Name.Value.Equals(layoutName, StringComparison.OrdinalIgnoreCase));
    }

    public static void CloneSlideLayout(this SlidePart newSlidePart, SlideLayoutPart slPart, string id)
    {
        /* ensure we added the rel ID to this part */
        newSlidePart.AddPart(slPart, id);
        using (Stream stream = slPart.GetStream()) { newSlidePart.SlideLayoutPart.FeedData(stream); }

        newSlidePart.Slide.CommonSlideData = (CommonSlideData)slPart.SlideLayout.CommonSlideData.Clone();

        foreach (ImagePart iPart in slPart.ImageParts)
            newSlidePart.AddPart(iPart, slPart.GetIdOfPart(iPart));
    }

Upvotes: 3

David Zemens
David Zemens

Reputation: 53663

I noticed some discrepancies in the slide's .rels, from the correct, manually produced slide:

<?xml version="1.0" encoding="UTF-8" standalone="true"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship Target="../slideLayouts/slideLayout8.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Id="rId1"/>
</Relationships>

And the incorrect one looked like:

<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="R522c7c9989a04964" Target="/ppt/slideLayouts/slideLayout8.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"/>
<Relationship Id="rId5" Target="/ppt/media/image2.bin" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"/>
</Relationships>

Two discrepancies, which I believe are as follows:

  • The image2.bin I believe I traced this back to a 1x1 pixel autoshape "object" that was present on several of the slide masters. I manually removed that from each slide master where it existed, and resaved my template pptx file.
  • The slide is missing the rel ID back to the slide layout, seems easy enough. I added some extension methods to the OpenXmlUtils class, and modified the _insertNewSlide method as follows:

private void _insertNewSlide(PresentationPart presentationPart, string layoutName)
{
    Slide slide = new Slide();
    SlidePart slidePart = presentationPart.AddNewPart<SlidePart>();

    slide.Save(slidePart);
    SlideMasterPart slideMasterPart = presentationPart.SlideMasterParts.FirstOrDefault();
    SlideLayoutPart slideLayoutPart = slideMasterPart.GetSlideLayoutPartByLayoutName(layoutName); // extension method

    /* ensure we added the rel ID to this part */
    slidePart.AddPart<SlideLayoutPart>(slideLayoutPart, slideMasterPart.GetIdOfPart(slideLayoutPart));

    slidePart.Slide.CommonSlideData = (CommonSlideData)slideLayoutPart.SlideLayout.CommonSlideData.Clone();

    slidePart.CloneSlideLayout(slideLayoutPart); // extension method

    presentationPart.AppendSlide(slidePart); // extension method

}

I've added the following extension methods in OpenXmlUtils.cs:

public static void CloneSlideLayout(this SlidePart newSlidePart, SlideLayoutPart slPart, string id)
{
    // creates a Slide from a SlideLayout

    /* ensure we added the rel ID to this part */
    newSlidePart.AddPart(slPart, id);
    using (Stream stream = slPart.GetStream()) { newSlidePart.SlideLayoutPart.FeedData(stream); }

    newSlidePart.Slide.CommonSlideData = (CommonSlideData)slPart.SlideLayout.CommonSlideData.Clone();

    foreach (ImagePart iPart in slPart.ImageParts)
    {
        newSlidePart.AddPart<ImagePart>(iPart, slPart.GetIdOfPart(iPart));
    }

}

public static uint GetNextSlideId(this SlideIdList slideIdList)
{
    uint nextId;
    uint maxId = GetMaxSlideId(slideIdList);
    if (maxId == 0)
    {
        // Slide Id must be >= 256
        nextId = 256;
    }
    else
    {
        nextId = maxId++;
    }
    return nextId;
}
public static uint GetMaxSlideId(this SlideIdList slideIdList)
{

    // find the highest id
    uint maxSlideId = 0;
    if (slideIdList.ChildElements.Count() > 0)
        maxSlideId = slideIdList.ChildElements
            .Cast<SlideId>()
            .Max(x => x.Id.Value);
    return maxSlideId;
}
public static SlideLayoutPart GetSlideLayoutPartByLayoutName(this SlideMasterPart slideMasterPart, string layoutName)
{
    return slideMasterPart.SlideLayoutParts.SingleOrDefault
            (sl => sl.SlideLayout.CommonSlideData.Name.Value.Equals(layoutName, StringComparison.OrdinalIgnoreCase));
}

public static void AppendSlide(this PresentationPart presentationPart, SlidePart newSlidePart)
{
        SlideMasterPart slideMasterPart = presentationPart.SlideMasterParts.FirstOrDefault();
        SlideLayoutPart slideLayoutPart = slideMasterPart.GetSlideLayoutPartByLayoutName(layoutName);

        Slide slide = new Slide(  );
        SlidePart slidePart = presentationPart.AddNewPart<SlidePart>();
        slide.Save(slidePart);

        string id = slideMasterPart.GetIdOfPart(slideLayoutPart);
        slidePart.CloneSlideLayout(slideLayoutPart, id);

        presentationPart.AppendSlide(slidePart); 
}

Having implemented these changes, I can successfully produce the "One content" slide from the master, and it looks like most of the other layouts are output correctly as well, but if I try to create an instance of each slide layout, there is still a "repair" issue which I'll need to isolate.

Update:

Upvotes: 0

Related Questions