diff --git a/.gitignore b/.gitignore index 1e00536b..22c2f215 100644 --- a/.gitignore +++ b/.gitignore @@ -43,8 +43,8 @@ _ReSharper.Caches/ .fake/ .paket/ -AssemblyInfo.Generated.cs +AssemblyInfo.g.cs TestFiles/PublishSlides/output/ .ionide/ -temp/ \ No newline at end of file +temp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d4b5ac..b6b94ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [3.0.0-beta02] - January 8, 2025 + +- Public exposure of `IFluentPresentationBuilder` ergonomic API for the code behind `PresentationBuilder` + ## [2.4.1] - January 4, 2025 - fix: Bug fix/document assembler paragraph properties not copied from template #91 diff --git a/Clippit.Tests/Clippit.Tests.csproj b/Clippit.Tests/Clippit.Tests.csproj index b4c7f613..ea40ef25 100644 --- a/Clippit.Tests/Clippit.Tests.csproj +++ b/Clippit.Tests/Clippit.Tests.csproj @@ -3,8 +3,6 @@ Exe net9.0 - true - true true diff --git a/Clippit.Tests/PowerPoint/PresentationBuilderSlidePublishingTests.Fluent.cs b/Clippit.Tests/PowerPoint/PresentationBuilderSlidePublishingTests.Fluent.cs new file mode 100644 index 00000000..eb1c6963 --- /dev/null +++ b/Clippit.Tests/PowerPoint/PresentationBuilderSlidePublishingTests.Fluent.cs @@ -0,0 +1,112 @@ +using System.Xml.Linq; +using Clippit.PowerPoint; +using Clippit.PowerPoint.Fluent; +using DocumentFormat.OpenXml.Packaging; +using Xunit; + +namespace Clippit.Tests.PowerPoint; + +public partial class PresentationBuilderSlidePublishingTests +{ + [Theory] + [ClassData(typeof(PublishingTestData))] + public async Task PublishUsingMemDocs(string sourcePath) + { + var fileName = Path.GetFileNameWithoutExtension(sourcePath); + var targetDir = Path.Combine(TargetDirectory, fileName); + if (Directory.Exists(targetDir)) + Directory.Delete(targetDir, true); + Directory.CreateDirectory(targetDir); + + await using var srcStream = File.Open(sourcePath, FileMode.Open); + var openSettings = new OpenSettings { AutoSave = false }; + using var srcDoc = OpenXmlExtensions.OpenPresentation(srcStream, false, openSettings); + ArgumentNullException.ThrowIfNull(srcDoc.PresentationPart); + + var slideNumber = 0; + var slidesIds = PresentationBuilderTools.GetSlideIdsInOrder(srcDoc); + foreach (var slideId in slidesIds) + { + var srcSlidePart = (SlidePart)srcDoc.PresentationPart.GetPartById(slideId); + var title = PresentationBuilderTools.GetSlideTitle(srcSlidePart.GetXElement()); + + using var stream = new MemoryStream(); + using (var newDocument = PresentationBuilder.NewDocument(stream)) + { + using (var builder = PresentationBuilder.Create(newDocument)) + { + var newSlidePart = builder.AddSlidePart(srcSlidePart); + + // Remove the show attribute from the slide element (if it exists) + var slideDocument = newSlidePart.GetXDocument(); + slideDocument.Root?.Attribute(NoNamespace.show)?.Remove(); + } + + // Set the title of the new presentation to the title of the slide + newDocument.PackageProperties.Title = title; + } + + var slideFileName = string.Concat(fileName, $"_{++slideNumber:000}.pptx"); + await using var fs = File.Create(Path.Combine(targetDir, slideFileName)); + stream.Position = 0; + await stream.CopyToAsync(fs, TestContext.Current.CancellationToken); + + srcSlidePart.RemoveAnnotations(); + srcSlidePart.UnloadRootElement(); + } + + Log.WriteLine($"GC Total Memory: {GC.GetTotalMemory(false) / 1024 / 1024} MB"); + } + + [Theory] + [ClassData(typeof(PublishingTestData))] + public async Task MergeAllPowerPointBack(string sourcePath) + { + var fileName = Path.GetFileNameWithoutExtension(sourcePath); + var targetDir = Path.Combine(TargetDirectory, fileName); + if (!Directory.Exists(targetDir)) + Assert.Skip("Directory not found: " + targetDir); + + var slides = Directory.GetFiles(targetDir, "*.pptx", SearchOption.TopDirectoryOnly); + if (slides.Length < 1) + Assert.Skip("Not enough slides to merge."); + Array.Sort(slides); + + // Create a memory stream from the original presentation + using var ms = new MemoryStream(); + await using (var fs = File.OpenRead(sourcePath)) + await fs.CopyToAsync(ms, TestContext.Current.CancellationToken); + + // Use the first slide as the base document + var setting = new OpenSettings { AutoSave = false }; + using (var baseDoc = PresentationDocument.Open(ms, true, setting)) + { + ArgumentNullException.ThrowIfNull(baseDoc.PresentationPart); + + // Merge the remaining slides into the base document (one builder instance) + using (var builder = PresentationBuilder.Create(baseDoc)) + { + foreach (var path in slides) + { + using var doc = PresentationDocument.Open(path, false, setting); + ArgumentNullException.ThrowIfNull(doc.PresentationPart); + + // Add all slides in the correct order + foreach (var slidePath in PresentationBuilderTools.GetSlideIdsInOrder(doc)) + { + var slidePart = (SlidePart)doc.PresentationPart.GetPartById(slidePath); + builder.AddSlidePart(slidePart); + } + } + } + + baseDoc.PackageProperties.Title = $"{fileName} - Merged Deck X2"; + } + + // Save the merged document to a file + var resultFile = Path.Combine(TargetDirectory, $"{fileName}_MergedDeckX2.pptx"); + ms.Position = 0; + await using var resFile = File.Create(resultFile); + await ms.CopyToAsync(resFile, TestContext.Current.CancellationToken); + } +} diff --git a/Clippit.Tests/PowerPoint/PresentationBuilderSlidePublishingTests.cs b/Clippit.Tests/PowerPoint/PresentationBuilderSlidePublishingTests.cs index da524a0f..b7851445 100644 --- a/Clippit.Tests/PowerPoint/PresentationBuilderSlidePublishingTests.cs +++ b/Clippit.Tests/PowerPoint/PresentationBuilderSlidePublishingTests.cs @@ -4,7 +4,7 @@ namespace Clippit.Tests.PowerPoint { - public class PresentationBuilderSlidePublishingTests : TestsBase + public partial class PresentationBuilderSlidePublishingTests : TestsBase { private const string SourceDirectory = "../../../../TestFiles/PublishSlides/"; private const string TargetDirectory = "../../../../TestFiles/PublishSlides/output"; @@ -79,6 +79,7 @@ public void ExtractSlideWithExtendedChart() using var srcStream = File.Open(sourcePath, FileMode.Open); var openSettings = new OpenSettings { AutoSave = false }; using var srcDoc = OpenXmlExtensions.OpenPresentation(srcStream, false, openSettings); + ArgumentNullException.ThrowIfNull(srcDoc.PresentationPart); var srcEmbeddingCount = srcDoc .PresentationPart.SlideParts.SelectMany(slide => slide.ExtendedChartParts) @@ -88,6 +89,7 @@ public void ExtractSlideWithExtendedChart() var slide = PresentationBuilder.PublishSlides(srcDoc, Path.GetFileName(sourcePath)).First(); using var streamDoc = new OpenXmlMemoryStreamDocument(slide); using var slideDoc = streamDoc.GetPresentationDocument(openSettings); + ArgumentNullException.ThrowIfNull(slideDoc.PresentationPart); var slideEmbeddingCount = slideDoc .PresentationPart.SlideParts.Select(slide => slide.ExtendedChartParts) @@ -133,6 +135,7 @@ public void ExtractMasters(string fileName) using var streamDoc = new OpenXmlMemoryStreamDocument(onlyMaster); using var resDoc = streamDoc.GetPresentationDocument(); + ArgumentNullException.ThrowIfNull(resDoc.PresentationPart); Assert.Empty(resDoc.PresentationPart.SlideParts); Assert.InRange(resDoc.PresentationPart.SlideMasterParts.Count(), 1, numberOfMasters); diff --git a/Clippit/Clippit.csproj.DotSettings b/Clippit/Clippit.csproj.DotSettings index bf4e5992..f1971690 100644 --- a/Clippit/Clippit.csproj.DotSettings +++ b/Clippit/Clippit.csproj.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Copy.cs b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Copy.cs new file mode 100644 index 00000000..e3d8a599 --- /dev/null +++ b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Copy.cs @@ -0,0 +1,1121 @@ +using System.Xml; +using System.Xml.Linq; +using Clippit.Internal; +using DocumentFormat.OpenXml.Experimental; +using DocumentFormat.OpenXml.Features; +using DocumentFormat.OpenXml.Packaging; +using Path = System.IO.Path; +using PBT = Clippit.PowerPoint.Fluent.PresentationBuilderTools; + +namespace Clippit.PowerPoint.Fluent; + +internal sealed partial class FluentPresentationBuilder +{ + private void CopyStartingParts(PresentationDocument sourceDocument) + { + // A Core File Properties part does not have implicit or explicit relationships to other parts. + var srcCorePart = sourceDocument.CoreFilePropertiesPart; + if (srcCorePart?.GetXDocument().Root is not null && _newDocument.CoreFilePropertiesPart is null) + { + _newDocument.AddCoreFilePropertiesPart(); + var newXDoc = _newDocument.CoreFilePropertiesPart.GetXDocument(); + newXDoc.Declaration.Standalone = "yes"; + newXDoc.Declaration.Encoding = "UTF-8"; + var sourceXDoc = srcCorePart.GetXDocument(); + newXDoc.Add(sourceXDoc.Root); + } + + // An application attributes part does not have implicit or explicit relationships to other parts. + if (sourceDocument.ExtendedFilePropertiesPart is { } extPart && _newDocument.ExtendedFilePropertiesPart is null) + { + _newDocument.AddExtendedFilePropertiesPart(); + var newXDoc = _newDocument.ExtendedFilePropertiesPart.GetXDocument(); + newXDoc.Declaration.Standalone = "yes"; + newXDoc.Declaration.Encoding = "UTF-8"; + newXDoc.Add(extPart.GetXDocument().Root); + } + + // An custom file properties part does not have implicit or explicit relationships to other parts. + if (sourceDocument.CustomFilePropertiesPart is { } customPart && _newDocument.CustomFilePropertiesPart is null) + { + _newDocument.AddCustomFilePropertiesPart(); + var newXDoc = _newDocument.CustomFilePropertiesPart.GetXDocument(); + newXDoc.Declaration.Standalone = "yes"; + newXDoc.Declaration.Encoding = "UTF-8"; + newXDoc.Add(customPart.GetXDocument().Root); + } + } + +#if false + // TODO need to handle the following + + { P.custShowLst, 80 }, + { P.photoAlbum, 90 }, + { P.custDataLst, 100 }, + { P.kinsoku, 120 }, + { P.modifyVerifier, 150 }, +#endif + // Copy handout master, notes master, presentation properties and view properties, if they exist + private void CopyPresentationParts(PresentationDocument sourceDocument) + { + var newPresentation = _newDocument.PresentationPart.GetXDocument(); + var newPresentationRoot = newPresentation.Root!; + + // Copy slide and note slide sizes + var oldPresentationDoc = sourceDocument.PresentationPart.GetXDocument(); + + foreach (var att in oldPresentationDoc.Root.Attributes()) + { + if (!att.IsNamespaceDeclaration && newPresentationRoot.Attribute(att.Name) is null) + newPresentationRoot.Add(oldPresentationDoc.Root.Attribute(att.Name)); + } + + if ( + oldPresentationDoc.Root.Elements(P.sldSz).FirstOrDefault() is { } oldElement + && newPresentationRoot.Element(P.sldSz) is null + ) + { + newPresentationRoot.Add(oldElement); + } + + // Copy Font Parts + if ( + oldPresentationDoc.Root.Element(P.embeddedFontLst) is { } embeddedFontLst + && newPresentationRoot.Element(P.embeddedFontLst) is null + ) + { + var newFontLst = new XElement(P.embeddedFontLst); + foreach (var font in embeddedFontLst.Elements(P.embeddedFont)) + { + var newEmbeddedFont = new XElement(P.embeddedFont, font.Elements(P.font)); + + if (font.Element(P.regular) is not null) + newEmbeddedFont.Add(CreateEmbeddedFontPart(sourceDocument, font, P.regular)); + if (font.Element(P.bold) is not null) + newEmbeddedFont.Add(CreateEmbeddedFontPart(sourceDocument, font, P.bold)); + if (font.Element(P.italic) is not null) + newEmbeddedFont.Add(CreateEmbeddedFontPart(sourceDocument, font, P.italic)); + if (font.Element(P.boldItalic) is not null) + newEmbeddedFont.Add(CreateEmbeddedFontPart(sourceDocument, font, P.boldItalic)); + + newFontLst.Add(newEmbeddedFont); + } + newPresentationRoot.Add(newFontLst); + } + + if (newPresentationRoot.Element(P.defaultTextStyle) is null) + newPresentationRoot.Add(oldPresentationDoc.Root.Element(P.defaultTextStyle)); + if (newPresentationRoot.Element(P.extLst) is null) + newPresentationRoot.Add(SanitizeExtLst(oldPresentationDoc.Root.Elements(P.extLst))); + + // + // + // + // + // + // + // + + // Copy Handout Master + if ( + sourceDocument.PresentationPart.HandoutMasterPart is { } oldMaster + && _newDocument.PresentationPart.HandoutMasterPart is null + ) + { + var newMaster = _newDocument.PresentationPart.AddNewPart(); + + // Copy theme for master + var newThemePart = newMaster.AddNewPart(); + newThemePart.PutXDocument(new XDocument(oldMaster.ThemePart.GetXDocument())); + CopyRelatedPartsForContentParts(oldMaster.ThemePart, newThemePart, [newThemePart.GetXDocument().Root]); + + // Copy master + newMaster.PutXDocument(new XDocument(oldMaster.GetXDocument())); + PBT.AddRelationships(oldMaster, newMaster, [newMaster.GetXDocument().Root]); + CopyRelatedPartsForContentParts(oldMaster, newMaster, [newMaster.GetXDocument().Root]); + + newPresentationRoot.Add( + new XElement( + P.handoutMasterIdLst, + new XElement( + P.handoutMasterId, + new XAttribute(R.id, _newDocument.PresentationPart.GetIdOfPart(newMaster)) + ) + ) + ); + } + + // Copy Notes Master + CopyNotesMaster(sourceDocument); + + // Copy Presentation Properties + if ( + sourceDocument.PresentationPart.PresentationPropertiesPart is { } presentationPropertiesPart + && _newDocument.PresentationPart.PresentationPropertiesPart is null + ) + { + var newPart = _newDocument.PresentationPart.AddNewPart(); + var xd1 = presentationPropertiesPart.GetXDocument(); + xd1.Descendants(P.custShow).Remove(); + newPart.PutXDocument(xd1); + } + + // Copy View Properties + if ( + sourceDocument.PresentationPart.ViewPropertiesPart is { } viewPropertiesPart + && _newDocument.PresentationPart.ViewPropertiesPart is null + ) + { + var newPart = _newDocument.PresentationPart.AddNewPart(); + var xd = viewPropertiesPart.GetXDocument(); + xd.Descendants(P.outlineViewPr).Elements(P.sldLst).Remove(); + newPart.PutXDocument(xd); + } + + foreach ( + var legacyDocTextInfo in sourceDocument.PresentationPart.Parts.Where(p => + p.OpenXmlPart.RelationshipType + == "http://schemas.microsoft.com/office/2006/relationships/legacyDocTextInfo" + ) + ) + { + var newPart = _newDocument.PresentationPart.AddNewPart(); + using var stream = legacyDocTextInfo.OpenXmlPart.GetStream(); + newPart.FeedData(stream); + } + + var listOfRootChildren = newPresentationRoot.Elements().ToList(); + foreach (var rc in listOfRootChildren) + rc.Remove(); + newPresentationRoot.Add(listOfRootChildren.OrderBy(e => PBT.OrderPresentation.GetValueOrDefault(e.Name, 999))); + } + + /// + /// This method remove custom p:ext elements from the extLst element, especially ones that reference custom metadata + /// Example: + /// + /// + /// + /// + /// List of all from source presentation.xml + /// Modified copy of all elements + private static IEnumerable SanitizeExtLst(IEnumerable extLstList) + { + foreach (var srcExtLst in extLstList) + { + // Deep clone the element + var extLst = new XElement(srcExtLst); + + // Sanitize all p:ext elements with r:Id attributes on any child element + foreach (var ext in extLst.Elements(P.ext).ToList()) + { + var hasRid = ext.Descendants().Any(e => e.Attributes().Any(a => a.Name == R.id)); + if (hasRid) + ext.Remove(); + } + + yield return extLst; + } + } + + private XElement CreateEmbeddedFontPart(PresentationDocument sourceDocument, XElement font, XName fontXName) + { + var oldFontPartId = (string)font.Element(fontXName).Attributes(R.id).FirstOrDefault(); + if (!sourceDocument.PresentationPart.TryGetPartById(oldFontPartId, out var oldFontPart)) + return null; + if (oldFontPart is not FontPart) + throw new FormatException($"Part {oldFontPartId} is not {nameof(FontPart)}"); + + var fontPartType = oldFontPart.ContentType switch + { + "application/x-fontdata" => FontPartType.FontData, + "application/x-font-ttf" => FontPartType.FontTtf, + _ => FontPartType.FontOdttf, + }; + + var newFontPartId = Relationships.GetNewRelationshipId(); + var newFontPart = _newDocument.PresentationPart.AddFontPart(fontPartType, newFontPartId); + using (var stream = oldFontPart.GetStream()) + newFontPart.FeedData(stream); + return new XElement(fontXName, new XAttribute(R.id, newFontPartId)); + } + + // Copies notes master and notesSz element from presentation + private void CopyNotesMaster(PresentationDocument sourceDocument) + { + // Copy notesSz element from presentation + var newPresentation = _newDocument.PresentationPart.GetXDocument(); + var oldPresentationDoc = sourceDocument.PresentationPart.GetXDocument(); + var oldElement = oldPresentationDoc.Root.Element(P.notesSz); + newPresentation.Root.Element(P.notesSz).ReplaceWith(oldElement); + + // Copy Notes Master + if ( + sourceDocument.PresentationPart.NotesMasterPart is { } oldMaster + && _newDocument.PresentationPart.NotesMasterPart is null + ) + { + var newMaster = _newDocument.PresentationPart.AddNewPart(); + + // Copy theme for master + if (oldMaster.ThemePart is { } themePart) + { + var newThemePart = newMaster.AddNewPart(); + newThemePart.PutXDocument(new XDocument(themePart.GetXDocument())); + CopyRelatedPartsForContentParts(themePart, newThemePart, [newThemePart.GetXDocument().Root]); + } + + // Copy master + newMaster.PutXDocument(new XDocument(oldMaster.GetXDocument())); + PBT.AddRelationships(oldMaster, newMaster, [newMaster.GetXDocument().Root]); + CopyRelatedPartsForContentParts(oldMaster, newMaster, [newMaster.GetXDocument().Root]); + + newPresentation.Root.Add( + new XElement( + P.notesMasterIdLst, + new XElement( + P.notesMasterId, + new XAttribute(R.id, _newDocument.PresentationPart.GetIdOfPart(newMaster)) + ) + ) + ); + } + } + + private void CopyComments(PresentationDocument oldDocument, SlidePart oldSlide, SlidePart newSlide) + { + newSlide.AddNewPart(); + newSlide.SlideCommentsPart.PutXDocument(new XDocument(oldSlide.SlideCommentsPart.GetXDocument())); + var newSlideComments = newSlide.SlideCommentsPart.GetXDocument(); + var oldAuthors = oldDocument.PresentationPart.CommentAuthorsPart.GetXDocument(); + foreach (var comment in newSlideComments.Root.Elements(P.cm)) + { + var newAuthor = FindCommentsAuthor(comment, oldAuthors); + // Update last index value for new comment + comment.Attribute(NoNamespace.authorId).SetValue(newAuthor.Attribute(NoNamespace.id).Value); + var lastIndex = Convert.ToUInt32(newAuthor.Attribute(NoNamespace.lastIdx).Value); + comment.Attribute(NoNamespace.idx).SetValue(lastIndex.ToString()); + newAuthor.Attribute(NoNamespace.lastIdx).SetValue(Convert.ToString(lastIndex + 1)); + } + } + + private XElement FindCommentsAuthor(XElement comment, XDocument oldAuthors) + { + var oldAuthor = oldAuthors + .Root.Elements(P.cmAuthor) + .FirstOrDefault(f => f.Attribute(NoNamespace.id).Value == comment.Attribute(NoNamespace.authorId).Value); + if (_newDocument.PresentationPart.CommentAuthorsPart is null) + { + _newDocument.PresentationPart.AddNewPart(); + _newDocument.PresentationPart.CommentAuthorsPart.PutXDocument( + new XDocument( + new XElement( + P.cmAuthorLst, + new XAttribute(XNamespace.Xmlns + "a", A.a), + new XAttribute(XNamespace.Xmlns + "r", R.r), + new XAttribute(XNamespace.Xmlns + "p", P.p) + ) + ) + ); + } + var authors = _newDocument.PresentationPart.CommentAuthorsPart.GetXDocument(); + var newAuthor = authors + .Root.Elements(P.cmAuthor) + .FirstOrDefault(f => + f.Attribute(NoNamespace.initials).Value == oldAuthor.Attribute(NoNamespace.initials).Value + ); + if (newAuthor is null) + { + uint newId = 0; + var ids = authors.Root.Descendants(P.cmAuthor).Select(f => (uint)f.Attribute(NoNamespace.id)).ToList(); + if (ids.Count != 0) + newId = ids.Max() + 1; + + newAuthor = new XElement( + P.cmAuthor, + new XAttribute(NoNamespace.id, newId.ToString()), + new XAttribute(NoNamespace.name, oldAuthor.Attribute(NoNamespace.name).Value), + new XAttribute(NoNamespace.initials, oldAuthor.Attribute(NoNamespace.initials).Value), + new XAttribute(NoNamespace.lastIdx, "1"), + new XAttribute(NoNamespace.clrIdx, newId.ToString()) + ); + authors.Root.Add(newAuthor); + } + + return newAuthor; + } + + private void CopyTableStyles(PresentationDocument oldDocument, OpenXmlPart newContentPart) + { + if (oldDocument.PresentationPart.TableStylesPart is null) + return; + + var oldTableStylesDocument = oldDocument.PresentationPart.TableStylesPart.GetXDocument(); + var oldTableStyles = oldTableStylesDocument.Root.Elements(A.tblStyle).ToList(); + + foreach (var table in newContentPart.GetXDocument().Descendants(A.tableStyleId)) + { + var styleId = table.Value; + if (string.IsNullOrEmpty(styleId)) + continue; + + // Find old style + var oldStyle = oldTableStyles.FirstOrDefault(f => f.Attribute(NoNamespace.styleId).Value == styleId); + if (oldStyle is null) + continue; + + // Create new TableStylesPart, if needed + XDocument tableStyles; + if (_newDocument.PresentationPart.TableStylesPart is null) + { + var newStylesPart = _newDocument.PresentationPart.AddNewPart(); + tableStyles = new XDocument( + new XElement( + A.tblStyleLst, + new XAttribute(XNamespace.Xmlns + "a", A.a), + new XAttribute(NoNamespace.def, styleId) + ) + ); + newStylesPart.PutXDocument(tableStyles); + } + else + tableStyles = _newDocument.PresentationPart.TableStylesPart.GetXDocument(); + + // Search new TableStylesPart to see if it contains the ID + if ( + tableStyles + .Root.Elements(A.tblStyle) + .FirstOrDefault(f => f.Attribute(NoNamespace.styleId).Value == styleId) + is not null + ) + continue; + + // Copy style to new part + tableStyles.Root.Add(oldStyle); + } + } + + private void CopyRelatedPartsForContentParts( + OpenXmlPart oldContentPart, + OpenXmlPart newContentPart, + IEnumerable newContent + ) + { + var relevantElements = newContent + .DescendantsAndSelf() + .Where(d => + d.Name == VML.imagedata + || d.Name == VML.fill + || d.Name == VML.stroke + || d.Name == A.blip + || d.Name == SVG.svgBlip + ) + .ToList(); + foreach (var imageReference in relevantElements) + { + CopyRelatedImage(oldContentPart, newContentPart, imageReference, R.embed); + CopyRelatedImage(oldContentPart, newContentPart, imageReference, R.pict); + CopyRelatedImage(oldContentPart, newContentPart, imageReference, R.id); + CopyRelatedImage(oldContentPart, newContentPart, imageReference, O.relid); + } + + relevantElements = newContent + .DescendantsAndSelf() + .Where(d => d.Name == A.videoFile || d.Name == A.quickTimeFile) + .ToList(); + foreach (var imageReference in relevantElements) + { + CopyRelatedMedia(oldContentPart, newContentPart, imageReference, R.link, "video"); + } + + relevantElements = newContent + .DescendantsAndSelf() + .Where(d => d.Name == P14.media || d.Name == PAV.srcMedia) + .ToList(); + foreach (var imageReference in relevantElements) + { + CopyRelatedMedia(oldContentPart, newContentPart, imageReference, R.embed, "media"); + PBT.CopyRelatedMediaExternalRelationship(oldContentPart, newContentPart, imageReference, R.link); + } + + foreach (var extendedReference in newContent.DescendantsAndSelf(A14.imgLayer)) + { + PBT.CopyExtendedPart(oldContentPart, newContentPart, extendedReference, R.embed); + } + + foreach (var contentPartReference in newContent.DescendantsAndSelf(P.contentPart)) + { + PBT.CopyInkPart(oldContentPart, newContentPart, contentPartReference, R.id); + } + + foreach (var contentPartReference in newContent.DescendantsAndSelf(P.control)) + { + PBT.CopyActiveXPart(oldContentPart, newContentPart, contentPartReference, R.id); + } + + foreach (var contentPartReference in newContent.DescendantsAndSelf(Plegacy.textdata)) + { + PBT.CopyLegacyDiagramText(oldContentPart, newContentPart, contentPartReference, "id"); + } + + foreach ( + var diagramReference in newContent + .DescendantsAndSelf() + .Where(d => d.Name == DGM.relIds || d.Name == A.relIds) + ) + { + // dm attribute + var relId = diagramReference.Attribute(R.dm).Value; + if (newContentPart.HasRelationship(relId)) + continue; + + var oldPart = oldContentPart.GetPartById(relId); + OpenXmlPart newPart = newContentPart.AddNewPart(); + newPart.GetXDocument().Add(oldPart.GetXDocument().Root); + diagramReference.Attribute(R.dm).Value = newContentPart.GetIdOfPart(newPart); + PBT.AddRelationships(oldPart, newPart, [newPart.GetXDocument().Root]); + CopyRelatedPartsForContentParts(oldPart, newPart, [newPart.GetXDocument().Root]); + + // lo attribute + relId = diagramReference.Attribute(R.lo).Value; + if (newContentPart.HasRelationship(relId)) + continue; + + oldPart = oldContentPart.GetPartById(relId); + newPart = newContentPart.AddNewPart(); + newPart.GetXDocument().Add(oldPart.GetXDocument().Root); + diagramReference.Attribute(R.lo).Value = newContentPart.GetIdOfPart(newPart); + PBT.AddRelationships(oldPart, newPart, [newPart.GetXDocument().Root]); + CopyRelatedPartsForContentParts(oldPart, newPart, [newPart.GetXDocument().Root]); + + // qs attribute + relId = diagramReference.Attribute(R.qs).Value; + if (newContentPart.HasRelationship(relId)) + continue; + + oldPart = oldContentPart.GetPartById(relId); + newPart = newContentPart.AddNewPart(); + newPart.GetXDocument().Add(oldPart.GetXDocument().Root); + diagramReference.Attribute(R.qs).Value = newContentPart.GetIdOfPart(newPart); + PBT.AddRelationships(oldPart, newPart, [newPart.GetXDocument().Root]); + CopyRelatedPartsForContentParts(oldPart, newPart, [newPart.GetXDocument().Root]); + + // cs attribute + relId = diagramReference.Attribute(R.cs).Value; + if (newContentPart.HasRelationship(relId)) + continue; + + oldPart = oldContentPart.GetPartById(relId); + newPart = newContentPart.AddNewPart(); + newPart.GetXDocument().Add(oldPart.GetXDocument().Root); + diagramReference.Attribute(R.cs).Value = newContentPart.GetIdOfPart(newPart); + PBT.AddRelationships(oldPart, newPart, [newPart.GetXDocument().Root]); + CopyRelatedPartsForContentParts(oldPart, newPart, [newPart.GetXDocument().Root]); + } + + foreach ( + var oleReference in newContent + .DescendantsAndSelf() + .Where(d => d.Name == P.oleObj || d.Name == P.externalData) + ) + { + var relId = oleReference.Attribute(R.id).Value; + + // First look to see if this relId has already been added to the new document. + // This is necessary for those parts that get processed with both old and new ids, such as the comments + // part. This is not necessary for parts such as the main document part, but this code won't malfunction + // in that case. + if (newContentPart.HasRelationship(relId)) + continue; + + var oldPartIdPair = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); + if (oldPartIdPair != default) + { + var oldPart = oldPartIdPair.OpenXmlPart; + OpenXmlPart newPart = null; + newPart = oldPart switch + { + EmbeddedObjectPart => newContentPart switch + { + DialogsheetPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), + HandoutMasterPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), + NotesMasterPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), + NotesSlidePart part => part.AddEmbeddedObjectPart(oldPart.ContentType), + SlideLayoutPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), + SlideMasterPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), + SlidePart part => part.AddEmbeddedObjectPart(oldPart.ContentType), + _ => newPart, + }, + EmbeddedPackagePart => newContentPart switch + { + ChartPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), + HandoutMasterPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), + NotesMasterPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), + NotesSlidePart part => part.AddEmbeddedPackagePart(oldPart.ContentType), + SlideLayoutPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), + SlideMasterPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), + SlidePart part => part.AddEmbeddedPackagePart(oldPart.ContentType), + _ => newPart, + }, + _ => newPart, + }; + using (var oldObject = oldPart.GetStream(FileMode.Open, FileAccess.Read)) + { + newPart.FeedData(oldObject); + } + oleReference.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); + } + else + { + var er = oldContentPart.GetExternalRelationship(relId); + var newEr = newContentPart.AddExternalRelationship(er.RelationshipType, er.Uri); + oleReference.Attribute(R.id).Set(newEr.Id); + } + } + + foreach (var chartReference in newContent.DescendantsAndSelf(C.chart)) + { + var relId = (string)chartReference.Attribute(R.id); + if (newContentPart.HasRelationship(relId)) + continue; + + var oldPartIdPair2 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); + if (oldPartIdPair2.OpenXmlPart is ChartPart oldPart) + { + var oldChart = oldPart.GetXDocument(); + var newPart = newContentPart.AddNewPart(); + var newChart = newPart.GetXDocument(); + newChart.Add(oldChart.Root); + chartReference.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); + PBT.CopyChartObjects(oldPart, newPart); + CopyRelatedPartsForContentParts(oldPart, newPart, [newChart.Root]); + } + } + + foreach (var chartReference in newContent.DescendantsAndSelf(Cx.chart)) + { + var relId = (string)chartReference.Attribute(R.id); + if (newContentPart.HasRelationship(relId)) + continue; + + var oldPartIdPair2 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); + if (oldPartIdPair2.OpenXmlPart is ExtendedChartPart oldPart) + { + var oldChart = oldPart.GetXDocument(); + var newPart = newContentPart.AddNewPart(); + var newChart = newPart.GetXDocument(); + newChart.Add(oldChart.Root); + chartReference.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); + PBT.CopyExtendedChartObjects(oldPart, newPart); + CopyRelatedPartsForContentParts(oldPart, newPart, [newChart.Root]); + } + } + + foreach (var userShape in newContent.DescendantsAndSelf(C.userShapes)) + { + var relId = (string)userShape.Attribute(R.id); + if (newContentPart.HasRelationship(relId)) + continue; + + var oldPartIdPair3 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); + if (oldPartIdPair3.OpenXmlPart is ChartDrawingPart oldPart) + { + var oldXDoc = oldPart.GetXDocument(); + var newPart = newContentPart.AddNewPart(); + var newXDoc = newPart.GetXDocument(); + newXDoc.Add(oldXDoc.Root); + userShape.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); + PBT.AddRelationships(oldPart, newPart, newContent); + CopyRelatedPartsForContentParts(oldPart, newPart, [newXDoc.Root]); + } + } + + foreach (var tags in newContent.DescendantsAndSelf(P.tags)) + { + var relId = (string)tags.Attribute(R.id); + if (newContentPart.HasRelationship(relId)) + continue; + + var oldPartIdPair4 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); + if (oldPartIdPair4.OpenXmlPart is UserDefinedTagsPart oldPart) + { + var oldXDoc = oldPart.GetXDocument(); + var newPart = newContentPart.AddNewPart(); + var newXDoc = newPart.GetXDocument(); + newXDoc.Add(oldXDoc.Root); + tags.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); + } + } + + foreach (var custData in newContent.DescendantsAndSelf(P.custData)) + { + var relId = (string)custData.Attribute(R.id); + if (string.IsNullOrEmpty(relId) || newContentPart.Parts.Any(p => p.RelationshipId == relId)) + continue; + + var oldPartIdPair9 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); + if (oldPartIdPair9 != default) + { + var newPart = _newDocument.PresentationPart.AddCustomXmlPart(CustomXmlPartType.CustomXml); + using (var stream = oldPartIdPair9.OpenXmlPart.GetStream()) + newPart.FeedData(stream); + foreach ( + var itemProps in oldPartIdPair9.OpenXmlPart.Parts.Where(p => + p.OpenXmlPart.ContentType + == "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" + ) + ) + { + var newId2 = Relationships.GetNewRelationshipId(); + var cxpp = newPart.AddNewPart( + "application/vnd.openxmlformats-officedocument.customXmlProperties+xml", + newId2 + ); + using var stream = itemProps.OpenXmlPart.GetStream(); + cxpp.FeedData(stream); + } + var newId = Relationships.GetNewRelationshipId(); + newContentPart.CreateRelationshipToPart(newPart, newId); + custData.Attribute(R.id).Value = newId; + } + } + + foreach (var soundReference in newContent.DescendantsAndSelf().Where(d => d.Name == A.audioFile)) + PBT.CopyRelatedSound(_newDocument, oldContentPart, newContentPart, soundReference, R.link); + + if ( + (oldContentPart is ChartsheetPart && newContentPart is ChartsheetPart) + || (oldContentPart is DialogsheetPart && newContentPart is DialogsheetPart) + || (oldContentPart is HandoutMasterPart && newContentPart is HandoutMasterPart) + || (oldContentPart is InternationalMacroSheetPart && newContentPart is InternationalMacroSheetPart) + || (oldContentPart is MacroSheetPart && newContentPart is MacroSheetPart) + || (oldContentPart is NotesMasterPart && newContentPart is NotesMasterPart) + || (oldContentPart is NotesSlidePart && newContentPart is NotesSlidePart) + || (oldContentPart is SlideLayoutPart && newContentPart is SlideLayoutPart) + || (oldContentPart is SlideMasterPart && newContentPart is SlideMasterPart) + || (oldContentPart is SlidePart && newContentPart is SlidePart) + || (oldContentPart is WorksheetPart && newContentPart is WorksheetPart) + ) + { + foreach ( + var soundReference in newContent + .DescendantsAndSelf() + .Where(d => + d.Name == P.snd + || d.Name == P.sndTgt + || d.Name == A.wavAudioFile + || d.Name == A.snd + || d.Name == PAV.srcMedia + ) + ) + PBT.CopyRelatedSound(_newDocument, oldContentPart, newContentPart, soundReference, R.embed); + + var vmlDrawingParts = oldContentPart switch + { + ChartsheetPart part => part.VmlDrawingParts, + DialogsheetPart part => part.VmlDrawingParts, + HandoutMasterPart part => part.VmlDrawingParts, + InternationalMacroSheetPart part => part.VmlDrawingParts, + MacroSheetPart part => part.VmlDrawingParts, + NotesMasterPart part => part.VmlDrawingParts, + NotesSlidePart part => part.VmlDrawingParts, + SlideLayoutPart part => part.VmlDrawingParts, + SlideMasterPart part => part.VmlDrawingParts, + SlidePart part => part.VmlDrawingParts, + WorksheetPart part => part.VmlDrawingParts, + _ => null, + }; + + if (vmlDrawingParts is not null) + { + // Transitional: Copy VML Drawing parts, implicit relationship + foreach (var vmlPart in vmlDrawingParts) + { + var newVmlPart = newContentPart switch + { + ChartsheetPart part => part.AddNewPart(), + DialogsheetPart part => part.AddNewPart(), + HandoutMasterPart part => part.AddNewPart(), + InternationalMacroSheetPart part => part.AddNewPart(), + MacroSheetPart part => part.AddNewPart(), + NotesMasterPart part => part.AddNewPart(), + NotesSlidePart part => part.AddNewPart(), + SlideLayoutPart part => part.AddNewPart(), + SlideMasterPart part => part.AddNewPart(), + SlidePart part => part.AddNewPart(), + WorksheetPart part => part.AddNewPart(), + _ => null, + }; + + try + { + var xd = new XDocument(vmlPart.GetXDocument()); + foreach (var item in xd.Descendants(O.ink)) + { + if (item.Attribute("i") is { } attr) + { + var i = attr.Value; + i = i.Replace(" ", "\r\n"); + attr.Value = i; + } + } + newVmlPart.PutXDocument(xd); + + PBT.AddRelationships(vmlPart, newVmlPart, [newVmlPart.GetXDocument().Root]); + CopyRelatedPartsForContentParts(vmlPart, newVmlPart, [newVmlPart.GetXDocument().Root]); + } + catch (XmlException) + { + using var srcStream = vmlPart.GetStream(); + using var dstStream = newVmlPart.GetStream(FileMode.Create, FileAccess.Write); + srcStream.CopyTo(dstStream); + } + } + } + } + } + + private void CopyRelatedImage( + OpenXmlPart oldContentPart, + OpenXmlPart newContentPart, + XElement imageReference, + XName attributeName + ) + { + // First look to see if this relId has already been added to the new document. + // This is necessary for those parts that get processed with both old and new ids, such as the comments + // part. This is not necessary for parts such as the main document part, but this code won't malfunction + // in that case. + + var relId = (string)imageReference.Attribute(attributeName); + if (newContentPart.HasRelationship(relId)) + return; + + var oldPartIdPair = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); + if (oldPartIdPair != default) + { + var oldPart = oldPartIdPair.OpenXmlPart as ImagePart; + var temp = GetOrAddImageCopy(oldPart); + if (temp.ImagePart is null) + { + var contentType = oldPart?.ContentType; + var targetExtension = contentType switch + { + "image/bmp" => ".bmp", + "image/gif" => ".gif", + "image/png" => ".png", + "image/tiff" => ".tiff", + "image/x-icon" => ".ico", + "image/x-pcx" => ".pcx", + "image/jpeg" => ".jpg", + "image/x-emf" => ".emf", + "image/x-wmf" => ".wmf", + "image/svg+xml" => ".svg", + _ => ".image", + }; + newContentPart.Features.GetRequired().Register(contentType, targetExtension); + + var newPart = newContentPart switch + { + ChartDrawingPart part => part.AddImagePart(contentType), + ChartPart part => part.AddImagePart(contentType), + ChartsheetPart part => part.AddImagePart(contentType), + DiagramDataPart part => part.AddImagePart(contentType), + DiagramLayoutDefinitionPart part => part.AddImagePart(contentType), + DiagramPersistLayoutPart part => part.AddImagePart(contentType), + DrawingsPart part => part.AddImagePart(contentType), + HandoutMasterPart part => part.AddImagePart(contentType), + NotesMasterPart part => part.AddImagePart(contentType), + NotesSlidePart part => part.AddImagePart(contentType), + RibbonAndBackstageCustomizationsPart part => part.AddImagePart(contentType), + RibbonExtensibilityPart part => part.AddImagePart(contentType), + SlideLayoutPart part => part.AddImagePart(contentType), + SlideMasterPart part => part.AddImagePart(contentType), + SlidePart part => part.AddImagePart(contentType), + ThemeOverridePart part => part.AddImagePart(contentType), + ThemePart part => part.AddImagePart(contentType), + VmlDrawingPart part => part.AddImagePart(contentType), + WorksheetPart part => part.AddImagePart(contentType), + _ => null, + }; + + temp.ImagePart = newPart; + var id = newContentPart.GetIdOfPart(newPart); + temp.AddContentPartRelTypeResourceIdTupple(newContentPart, newPart.RelationshipType, id); + + using (var stream = oldPart.GetStream()) + newPart.FeedData(stream); + imageReference.SetAttributeValue(attributeName, id); + } + else + { + var refRel = newContentPart.DataPartReferenceRelationships.FirstOrDefault(rr => + temp.ContentPartRelTypeIdList.Exists(cpr => + cpr.ContentPart == newContentPart && cpr.RelationshipId == rr.Id + ) + ); + if (refRel is not null) + { + var relationshipId = temp + .ContentPartRelTypeIdList.First(cpr => + cpr.ContentPart == newContentPart && cpr.RelationshipId == refRel.Id + ) + .RelationshipId; + imageReference.SetAttributeValue(attributeName, relationshipId); + return; + } + + var cpr2 = temp.ContentPartRelTypeIdList.FirstOrDefault(c => c.ContentPart == newContentPart); + if (cpr2 is not null) + { + imageReference.SetAttributeValue(attributeName, cpr2.RelationshipId); + } + else + { + var imagePart = (ImagePart)temp.ImagePart; + var existingImagePart = newContentPart.AddPart(imagePart); + var newId = newContentPart.GetIdOfPart(existingImagePart); + temp.AddContentPartRelTypeResourceIdTupple(newContentPart, imagePart.RelationshipType, newId); + imageReference.SetAttributeValue(attributeName, newId); + } + } + } + else + { + var er = oldContentPart.ExternalRelationships.FirstOrDefault(r => r.Id == relId); + if (er is not null) + { + var newEr = newContentPart.AddExternalRelationship(er.RelationshipType, er.Uri); + imageReference.SetAttributeValue(R.id, newEr.Id); + } + else + { + var newPart = newContentPart + .OpenXmlPackage.GetPackage() + .GetParts() + .FirstOrDefault(p => p.Uri == newContentPart.Uri); + if (newPart is not null && !newPart.Relationships.Contains(relId)) + { + newPart.Relationships.Create( + new Uri("NULL", UriKind.RelativeOrAbsolute), + System.IO.Packaging.TargetMode.Internal, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + relId + ); + } + } + } + } + + private void CopyRelatedMedia( + OpenXmlPart oldContentPart, + OpenXmlPart newContentPart, + XElement imageReference, + XName attributeName, + string mediaRelationshipType + ) + { + var relId = (string)imageReference.Attribute(attributeName); + if (string.IsNullOrEmpty(relId) || newContentPart.DataPartReferenceRelationships.Any(dpr => dpr.Id == relId)) // First look to see if this relId has already been added to the new document. + return; + + var oldRel = oldContentPart.DataPartReferenceRelationships.FirstOrDefault(dpr => dpr.Id == relId); + if (oldRel is null) + return; + + var oldPart = oldRel.DataPart; + var temp = GetOrAddMediaCopy(oldPart); + if (temp.DataPart is null) + { + var ct = oldPart.ContentType; + var ext = Path.GetExtension(oldPart.Uri.OriginalString); + var newPart = newContentPart.OpenXmlPackage.CreateMediaDataPart(ct, ext); + using (var stream = oldPart.GetStream()) + newPart.FeedData(stream); + string id = null; + string relationshipType = null; + + switch (mediaRelationshipType) + { + case "media": + { + var mrr = newContentPart switch + { + SlidePart part => part.AddMediaReferenceRelationship(newPart), + SlideLayoutPart part => part.AddMediaReferenceRelationship(newPart), + SlideMasterPart part => part.AddMediaReferenceRelationship(newPart), + _ => null, + }; + + id = mrr?.Id; + relationshipType = "http://schemas.microsoft.com/office/2007/relationships/media"; + break; + } + case "video": + { + var vrr = newContentPart switch + { + SlidePart part => part.AddVideoReferenceRelationship(newPart), + HandoutMasterPart part => part.AddVideoReferenceRelationship(newPart), + NotesMasterPart part => part.AddVideoReferenceRelationship(newPart), + NotesSlidePart part => part.AddVideoReferenceRelationship(newPart), + SlideLayoutPart part => part.AddVideoReferenceRelationship(newPart), + SlideMasterPart part => part.AddVideoReferenceRelationship(newPart), + _ => null, + }; + + id = vrr?.Id; + relationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"; + break; + } + } + temp.DataPart = newPart; + temp.AddContentPartRelTypeResourceIdTupple(newContentPart, relationshipType, id); + imageReference.Attribute(attributeName).Set(id); + } + else + { + var desiredRelType = mediaRelationshipType switch + { + "media" => "http://schemas.microsoft.com/office/2007/relationships/media", + "video" => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video", + _ => null, + }; + + var existingRel = temp.ContentPartRelTypeIdList.FirstOrDefault(cp => + cp.ContentPart == newContentPart && cp.RelationshipType == desiredRelType + ); + if (existingRel is not null) + { + imageReference.Attribute(attributeName).Set(existingRel.RelationshipId); + } + else + { + var newPart = (MediaDataPart)temp.DataPart; + string id = null; + string relationshipType = null; + switch (mediaRelationshipType) + { + case "media": + { + var mrr = newContentPart switch + { + SlidePart part => part.AddMediaReferenceRelationship(newPart), + SlideLayoutPart part => part.AddMediaReferenceRelationship(newPart), + SlideMasterPart part => part.AddMediaReferenceRelationship(newPart), + _ => null, + }; + + id = mrr?.Id; + relationshipType = mrr?.RelationshipType; + break; + } + case "video": + { + var vrr = newContentPart switch + { + SlidePart part => part.AddVideoReferenceRelationship(newPart), + HandoutMasterPart part => part.AddVideoReferenceRelationship(newPart), + NotesMasterPart part => part.AddVideoReferenceRelationship(newPart), + NotesSlidePart part => part.AddVideoReferenceRelationship(newPart), + SlideLayoutPart part => part.AddVideoReferenceRelationship(newPart), + SlideMasterPart part => part.AddVideoReferenceRelationship(newPart), + _ => null, + }; + + id = vrr?.Id; + relationshipType = vrr?.RelationshipType; + break; + } + } + temp.AddContentPartRelTypeResourceIdTupple(newContentPart, relationshipType, id); + imageReference.Attribute(attributeName).Set(id); + } + } + } + + private ThemePart CopyThemePart(SlideMasterPart slideMasterPart, ThemePart oldThemePart, double scaleFactor) + { + var newThemePart = slideMasterPart.AddNewPart(); + var newThemeDoc = new XDocument(oldThemePart.GetXDocument()); + SlideLayoutData.ScaleShapes(newThemeDoc, scaleFactor); + newThemePart.PutXDocument(newThemeDoc); + + CopyRelatedPartsForContentParts(oldThemePart, newThemePart, [newThemePart.GetXDocument().Root]); + + if (_newDocument.PresentationPart.ThemePart is null) + newThemePart = _newDocument.PresentationPart.AddPart(newThemePart); + + return newThemePart; + } + + private SlideMasterPart CopySlideMasterPart(SlideMasterPart oldMasterPart, double scaleFactor) + { + var newMaster = _newDocument.PresentationPart.AddNewPart(); + + // Add to presentation slide master list, need newID for layout IDs also + var presentationPartDoc = _newDocument.PresentationPart.GetXDocument(); + presentationPartDoc + .Root.Element(P.sldMasterIdLst) + .Add( + new XElement( + P.sldMasterId, + new XAttribute(NoNamespace.id, GetNextFreeId().ToString()), + new XAttribute(R.id, _newDocument.PresentationPart.GetIdOfPart(newMaster)) + ) + ); + + // Ensure that master does not keep ids of old layouts + var newMasterDoc = new XDocument(oldMasterPart.GetXDocument()); + var sldLayoutIdLst = newMasterDoc.Root.Element(P.sldLayoutIdLst); + if (sldLayoutIdLst is null) + { + newMasterDoc.Root.Add(new XElement(P.sldLayoutIdLst)); + } + else + { + sldLayoutIdLst.Descendants(P.sldLayoutId).ToList().ForEach(e => e.Remove()); + } + + SlideLayoutData.ScaleShapes(newMasterDoc, scaleFactor); + newMaster.PutXDocument(newMasterDoc); + + PBT.AddRelationships(oldMasterPart, newMaster, [newMaster.GetXDocument().Root]); + CopyRelatedPartsForContentParts(oldMasterPart, newMaster, [newMaster.GetXDocument().Root]); + + _ = CopyThemePart(newMaster, oldMasterPart.ThemePart, scaleFactor); + + return newMaster; + } + + private SlideLayoutPart CopySlideLayoutPart( + SlideMasterPart newSlideMasterPart, + SlideLayoutPart oldSlideLayoutPart, + double scaleFactor + ) + { + var newLayout = newSlideMasterPart.AddNewPart(); + newLayout.AddPart(newSlideMasterPart); + + var newLayoutDoc = new XDocument(oldSlideLayoutPart.GetXDocument()); + SlideLayoutData.ScaleShapes(newLayoutDoc, scaleFactor); + newLayout.PutXDocument(newLayoutDoc); + + PBT.AddRelationships(oldSlideLayoutPart, newLayout, [newLayout.GetXDocument().Root]); + CopyRelatedPartsForContentParts(oldSlideLayoutPart, newLayout, [newLayout.GetXDocument().Root]); + + var newMasterDoc = newSlideMasterPart.GetXDocument(); + newMasterDoc + .Root.Element(P.sldLayoutIdLst) + .Add( + new XElement( + P.sldLayoutId, + new XAttribute(NoNamespace.id, GetNextFreeId().ToString()), + new XAttribute(R.id, newSlideMasterPart.GetIdOfPart(newLayout)) + ) + ); + + return newLayout; + } +} diff --git a/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Deduplication.cs b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Deduplication.cs new file mode 100644 index 00000000..9038bb62 --- /dev/null +++ b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Deduplication.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Presentation; + +namespace Clippit.PowerPoint.Fluent; + +internal partial class FluentPresentationBuilder +{ + private readonly List _mediaCache = []; + private readonly Dictionary _slideMasters = []; + private SlideSize _slideSize; + + private void InitializeCaches() + { + if (_newDocument.PresentationPart is not { } presentation) + return; + + foreach (var slideMasterPart in presentation.SlideMasterParts) + { + foreach (var slideLayoutPart in slideMasterPart.SlideLayoutParts) + { + _ = GetOrAddSlideLayoutPart(_newDocument, slideLayoutPart, 1.0f); + } + } + + // TODO: enumerate all images, media, master and layouts + _slideSize = presentation.Presentation.SlideSize; + } + + private double GetScaleFactor(PresentationDocument sourceDocument) + { + var slideSize = sourceDocument.PresentationPart.Presentation.SlideSize; + var scaleFactorX = (double)_slideSize.Cx / slideSize.Cx; + var scaleFactorY = (double)_slideSize.Cy / slideSize.Cy; + return Math.Min(scaleFactorX, scaleFactorY); + } + + // General function for handling images that tries to use an existing image if they are the same + private ImageData GetOrAddImageCopy(ImagePart oldImage) + { + return GetOrAddCachedMedia(new ImageData(oldImage)); + } + + // General function for handling media that tries to use an existing media item if they are the same + private MediaData GetOrAddMediaCopy(DataPart oldMedia) + { + return GetOrAddCachedMedia(new MediaData(oldMedia)); + } + + private T GetOrAddCachedMedia(T contentData) + where T : ContentData + { + var duplicateItem = _mediaCache.FirstOrDefault(x => x.Compare(contentData)); + if (duplicateItem != null) + { + return (T)duplicateItem; + } + + _mediaCache.Add(contentData); + return contentData; + } + + // General function for handling SlideMasterPart that tries to use an existing SlideMasterPart if they are the same + private SlideMasterData GetOrAddSlideMasterPart( + PresentationDocument presentationDocument, + SlideMasterPart slideMasterPart, + double scaleFactor + ) + { + if (_slideMasters.TryGetValue(slideMasterPart, out var slideMasterData)) + { + return slideMasterData; + } + + slideMasterData = new SlideMasterData(slideMasterPart, scaleFactor); + foreach (var item in _slideMasters.Values) + { + if (item.CompareTo(slideMasterData) == 0) + return item; + } + + if (!ReferenceEquals(presentationDocument, _newDocument)) + { + var newSlideMasterPart = CopySlideMasterPart(slideMasterPart, scaleFactor); + slideMasterData = new SlideMasterData(newSlideMasterPart, 1.0); + } + + _slideMasters.Add(slideMasterPart, slideMasterData); + return slideMasterData; + } + + // General function for handling SlideMasterPart that tries to use an existing SlideMasterPart if they are the same + private SlideLayoutData GetOrAddSlideLayoutPart( + PresentationDocument presentationDocument, + SlideLayoutPart slideLayoutPart, + double scaleFactor + ) + { + var slideMasterData = GetOrAddSlideMasterPart( + presentationDocument, + slideLayoutPart.SlideMasterPart, + scaleFactor + ); + + if (slideMasterData.SlideLayouts.TryGetValue(slideLayoutPart, out var slideLayoutData)) + { + return slideLayoutData; + } + + slideLayoutData = new SlideLayoutData(slideLayoutPart, scaleFactor); + foreach (var item in slideMasterData.SlideLayouts.Values) + { + if (item.CompareTo(slideLayoutData) == 0) + return item; + } + + if (!ReferenceEquals(presentationDocument, _newDocument)) + { + var newSlideLayoutPart = CopySlideLayoutPart(slideMasterData.Part, slideLayoutPart, scaleFactor); + slideLayoutData = new SlideLayoutData(newSlideLayoutPart, 1.0); + } + + slideMasterData.SlideLayouts.Add(slideLayoutPart, slideLayoutData); + return slideLayoutData; + } +} diff --git a/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Id.cs b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Id.cs new file mode 100644 index 00000000..373748df --- /dev/null +++ b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Id.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; + +namespace Clippit.PowerPoint.Fluent; + +internal sealed partial class FluentPresentationBuilder +{ + private uint GetNextFreeId() + { + uint newId = 0; + + var presentationPartDoc = _newDocument.PresentationPart.GetXDocument(); + var masterIds = presentationPartDoc + .Root.Descendants(P.sldMasterId) + .Select(f => (uint)f.Attribute(NoNamespace.id)) + .ToList(); + if (masterIds.Count != 0) + newId = Math.Max(newId, masterIds.Max()); + + foreach (var slideMasterData in _slideMasters.Values) + { + var masterPartDoc = slideMasterData.Part.GetXDocument(); + var layoutIds = masterPartDoc + .Root.Descendants(P.sldLayoutId) + .Select(f => (uint)f.Attribute(NoNamespace.id)) + .ToList(); + if (layoutIds.Count != 0) + newId = Math.Max(newId, layoutIds.Max()); + } + + return newId == 0 ? 2147483648 : newId + 1; + } +} diff --git a/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.cs b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.cs new file mode 100644 index 00000000..d5e49c68 --- /dev/null +++ b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.cs @@ -0,0 +1,192 @@ +using System.Xml.Linq; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Presentation; +using PBT = Clippit.PowerPoint.Fluent.PresentationBuilderTools; + +namespace Clippit.PowerPoint.Fluent; + +internal sealed partial class FluentPresentationBuilder : IFluentPresentationBuilder +{ + private readonly PresentationDocument _newDocument; + private bool _isDocumentInitialized; + + internal FluentPresentationBuilder(PresentationDocument presentationDocument) + { + _newDocument = presentationDocument ?? throw new NullReferenceException(nameof(presentationDocument)); + + if (_newDocument.PresentationPart is null) + { + _newDocument.AddPresentationPart(); + var xDocument = GetPresentationXDocument(); + _newDocument.PresentationPart.PutXDocument(xDocument); + } + + _isDocumentInitialized = false; + InitializeCaches(); + } + + private static XDocument GetPresentationXDocument() + { + XNamespace ns = "http://schemas.openxmlformats.org/presentationml/2006/main"; + XNamespace relationshipsns = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; + XNamespace drawingns = "http://schemas.openxmlformats.org/drawingml/2006/main"; + + return new XDocument( + new XElement( + ns + "presentation", + new XAttribute(XNamespace.Xmlns + "a", drawingns), + new XAttribute(XNamespace.Xmlns + "r", relationshipsns), + new XAttribute(XNamespace.Xmlns + "p", ns), + new XElement(ns + "sldMasterIdLst"), + new XElement(ns + "sldIdLst"), + new XElement(ns + "notesSz", new XAttribute("cx", "6858000"), new XAttribute("cy", "9144000")) + ) + ) + { + Declaration = new XDeclaration("1.0", "UTF-8", "yes"), + }; + } + + public void Dispose() => SaveAndCleanup(); + + private void SaveAndCleanup() + { + // Remove sections list (all slides added to default section) + var presentationDocument = _newDocument.PresentationPart.GetXDocument(); + var sectionLists = presentationDocument.Descendants(P14.sectionLst).ToList(); + foreach (var sectionList in sectionLists) + { + sectionList.Parent?.Remove(); // element + } + + // Remove custom properties (source doc metadata irrelevant for generated document) + var customPropsDocument = _newDocument.CustomFilePropertiesPart?.GetXDocument(); + if (customPropsDocument?.Root?.HasElements == true) + { + customPropsDocument.Root?.RemoveNodes(); + } + + foreach (var part in _newDocument.GetAllParts()) + { + if (part.ContentType.EndsWith("+xml")) + { + var xd = part.GetXDocument(); + xd.Descendants().Attributes("smtClean").Remove(); + part.PutXDocument(); + } + else if (part.Annotation() is not null) + part.PutXDocument(); + } + } + + public SlideMasterPart AddSlideMasterPart(SlideMasterPart slideMasterPart) + { + var sourceDocument = (PresentationDocument)slideMasterPart.OpenXmlPackage; + EnsureDocumentInitialized(sourceDocument); + + var scaleFactor = GetScaleFactor(sourceDocument); + var slideMasterData = GetOrAddSlideMasterPart(sourceDocument, slideMasterPart, scaleFactor); + + foreach (var slideLayoutPart in slideMasterPart.SlideLayoutParts) + { + _ = GetOrAddSlideLayoutPart(sourceDocument, slideLayoutPart, scaleFactor); + } + + return slideMasterData.Part; + } + + private void EnsureDocumentInitialized(PresentationDocument sourceDocument) + { + if (_isDocumentInitialized) + return; + + CopyStartingParts(sourceDocument); + CopyPresentationParts(sourceDocument); + + _slideSize = sourceDocument.PresentationPart.Presentation.SlideSize.CloneNode(true) as SlideSize; + + var newPresentation = _newDocument.PresentationPart.GetXDocument(); + if (newPresentation.Root.Element(P.sldIdLst) is null) + { + newPresentation.Root.Add(new XElement(P.sldIdLst)); + } + + _isDocumentInitialized = true; + } + + public SlidePart AddSlidePart(SlidePart slidePart) + { + var sourceDocument = (PresentationDocument)slidePart.OpenXmlPackage; + ArgumentNullException.ThrowIfNull(sourceDocument); + + try + { + return AddSlidePartImpl(sourceDocument, slidePart); + } + catch (PresentationBuilderInternalException dbie) + { + if (dbie.Message.Contains("{0}")) + throw new PresentationBuilderException(string.Format(dbie.Message, slidePart.Uri)); + throw; + } + } + + private SlidePart AddSlidePartImpl(PresentationDocument sourceDocument, SlidePart slidePart) + { + EnsureDocumentInitialized(sourceDocument); + + var scaleFactor = GetScaleFactor(sourceDocument); + + // TODO: Maintain it globally on the builder level, instead of calculating it for each slide add operation + var newPresentation = _newDocument.PresentationPart.GetXDocument(); + uint newId = 256; + var ids = newPresentation.Root.Descendants(P.sldId).Select(f => (uint)f.Attribute(NoNamespace.id)).ToList(); + if (ids.Count != 0) + newId = ids.Max() + 1; + + var newSlide = _newDocument.PresentationPart.AddNewPart(); + using (var sourceStream = slidePart.GetStream()) + { + newSlide.FeedData(sourceStream); + } + + var slideDocument = newSlide.GetXDocument(); + SlideLayoutData.ScaleShapes(slideDocument, scaleFactor); + + PBT.AddRelationships(slidePart, newSlide, [newSlide.GetXDocument().Root]); + CopyRelatedPartsForContentParts(slidePart, newSlide, [newSlide.GetXDocument().Root]); + CopyTableStyles(sourceDocument, newSlide); + + if (slidePart.NotesSlidePart is { } notesSlide) + { + if (_newDocument.PresentationPart.NotesMasterPart is null) + CopyNotesMaster(sourceDocument); + var newPart = newSlide.AddNewPart(); + newPart.PutXDocument(notesSlide.GetXDocument()); + newPart.AddPart(newSlide); + if (_newDocument.PresentationPart.NotesMasterPart is not null) + newPart.AddPart(_newDocument.PresentationPart.NotesMasterPart); + PBT.AddRelationships(notesSlide, newPart, [newPart.GetXDocument().Root]); + CopyRelatedPartsForContentParts(slidePart.NotesSlidePart, newPart, [newPart.GetXDocument().Root]); + } + + var slideLayoutData = GetOrAddSlideLayoutPart(sourceDocument, slidePart.SlideLayoutPart, scaleFactor); + newSlide.AddPart(slideLayoutData.Part); + + if (slidePart.SlideCommentsPart is not null) + CopyComments(sourceDocument, slidePart, newSlide); + + newPresentation = _newDocument.PresentationPart.GetXDocument(); + newPresentation + .Root.Element(P.sldIdLst) + .Add( + new XElement( + P.sldId, + new XAttribute(NoNamespace.id, newId.ToString()), + new XAttribute(R.id, _newDocument.PresentationPart.GetIdOfPart(newSlide)) + ) + ); + + return newSlide; + } +} diff --git a/Clippit/PowerPoint/PresentationBuilderTools.cs b/Clippit/PowerPoint/Fluent/PresentationBuilderTools.cs similarity index 98% rename from Clippit/PowerPoint/PresentationBuilderTools.cs rename to Clippit/PowerPoint/Fluent/PresentationBuilderTools.cs index 3ffd4dc5..cd662fe7 100644 --- a/Clippit/PowerPoint/PresentationBuilderTools.cs +++ b/Clippit/PowerPoint/Fluent/PresentationBuilderTools.cs @@ -1,10 +1,14 @@ -using System.Text; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; using System.Xml.Linq; using Clippit.Internal; using DocumentFormat.OpenXml.Experimental; using DocumentFormat.OpenXml.Packaging; -namespace Clippit.PowerPoint +namespace Clippit.PowerPoint.Fluent { internal static class PresentationBuilderTools { @@ -40,6 +44,15 @@ internal static string GetSlideTitle(XElement slide) return paragraphText.ToString().Trim(); } + internal static List GetSlideIdsInOrder(PresentationDocument srcDoc) + { + return srcDoc + .PresentationPart.GetXElement() + .Descendants(P.sldId) + .Select(x => x.Attribute(R.id)!.Value) + .ToList(); + } + internal static readonly Dictionary OrderPresentation = new() { { P.sldMasterIdLst, 10 }, diff --git a/Clippit/PowerPoint/Fluent/SlidePartData.cs b/Clippit/PowerPoint/Fluent/SlidePartData.cs new file mode 100644 index 00000000..b48b4c1c --- /dev/null +++ b/Clippit/PowerPoint/Fluent/SlidePartData.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Linq; +using DocumentFormat.OpenXml.Packaging; + +namespace Clippit.PowerPoint.Fluent; + +internal abstract class SlidePartData : IComparable> +{ + public T Part { get; } + private double ScaleFactor { get; } + private string ShapeXml { get; } + + protected SlidePartData(T part, double scaleFactor) + { + Part = part; + ScaleFactor = scaleFactor; + ShapeXml = GetShapeDescriptor(part); + } + + protected abstract string GetShapeDescriptor(T part); + + public virtual int CompareTo(SlidePartData other) + { + if (ReferenceEquals(this, other)) + return 0; + if (other is null) + return 1; + return string.Compare(ShapeXml, other.ShapeXml, StringComparison.Ordinal); + } + + protected string NormalizeXml(string xml) + { + var doc = XDocument.Parse(xml); + CleanUpAttributes(doc.Root); + ScaleShapes(doc, ScaleFactor); + return doc.ToString(); + } + + private static readonly XNamespace s_relNs = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; + private static readonly XName[] s_noiseAttNames = + [ + "smtClean", + "dirty", + "userDrawn", + s_relNs + "id", + s_relNs + "embed", + ]; + + /// + /// Remove OpenXml attributes that may occur on Layout/Master elements but does not affect rendering + /// + private static void CleanUpAttributes(XElement element) + { + foreach (var attName in s_noiseAttNames) + { + element.Attribute(attName)?.Remove(); + } + + foreach (var descendant in element.Descendants()) + { + CleanUpAttributes(descendant); + } + } + + private static readonly Dictionary s_resizableAttributes = new() + { + { A.off, ["x", "y"] }, // + { A.ext, ["cx", "cy"] }, // + { A.chOff, ["x", "y"] }, // + { A.chExt, ["cx", "cy"] }, // + { A.rPr, ["sz"] }, // + { A.defRPr, ["sz"] }, // + { A.pPr, ["defTabSz"] }, // + { A.endParaRPr, ["sz"] }, // + { A.gridCol, ["w"] }, // + { A.tr, ["h"] }, // + }; + + public static void ScaleShapes(XDocument openXmlPart, double scale) + { + if (Math.Abs(scale - 1.0) < 1.0e-5) + return; + + foreach (var element in openXmlPart.Descendants()) + { + if (!s_resizableAttributes.TryGetValue(element.Name, out var attrNames)) + continue; + + foreach (var attrName in attrNames) + { + var attr = element.Attribute(attrName); + if (attr is null) + continue; + if (!long.TryParse(attr.Value, out var num)) + continue; + + var newNum = (long)(num * scale); + attr.SetValue(newNum); + } + } + } +} + +// This class is used to prevent duplication of layouts and handle content modification +internal class SlideLayoutData(SlideLayoutPart slideLayout, double scaleFactor) + : SlidePartData(slideLayout, scaleFactor) +{ + protected override string GetShapeDescriptor(SlideLayoutPart slideLayout) + { + var sb = new StringBuilder(); + var cSld = slideLayout.SlideLayout.CommonSlideData; + sb.Append(NormalizeXml(cSld.ShapeTree.OuterXml)); + if (cSld.Background is not null) + sb.Append(NormalizeXml(cSld.Background.OuterXml)); + return sb.ToString(); + } +} + +// This class is used to prevent duplication of themes and handle content modification +internal class ThemeData(ThemePart themePart, double scaleFactor) : SlidePartData(themePart, scaleFactor) +{ + protected override string GetShapeDescriptor(ThemePart themePart) => + NormalizeXml(themePart.Theme.ThemeElements.OuterXml); +} + +// This class is used to prevent duplication of masters and handle content modification +internal class SlideMasterData(SlideMasterPart slideMaster, double scaleFactor) + : SlidePartData(slideMaster, scaleFactor) +{ + public ThemeData ThemeData { get; } = new(slideMaster.ThemePart, scaleFactor); + public Dictionary SlideLayouts { get; } = []; + + protected override string GetShapeDescriptor(SlideMasterPart slideMaster) + { + var sb = new StringBuilder(); + var cSld = slideMaster.SlideMaster.CommonSlideData; + sb.Append(NormalizeXml(cSld.ShapeTree.OuterXml)); + if (cSld.Background is not null) + sb.Append(NormalizeXml(cSld.Background.OuterXml)); + + sb.Append(NormalizeXml(slideMaster.SlideMaster.ColorMap.OuterXml)); + return sb.ToString(); + } + + public override int CompareTo(SlidePartData other) + { + var res = base.CompareTo(other); + if (res == 0 && other is SlideMasterData otherData) + res = ThemeData.CompareTo(otherData.ThemeData); + return res; + } +} diff --git a/Clippit/PowerPoint/FluentPresentationBuilder.cs b/Clippit/PowerPoint/FluentPresentationBuilder.cs deleted file mode 100644 index 267b2cdd..00000000 --- a/Clippit/PowerPoint/FluentPresentationBuilder.cs +++ /dev/null @@ -1,1394 +0,0 @@ -using System.Xml; -using System.Xml.Linq; -using Clippit.Internal; -using DocumentFormat.OpenXml.Experimental; -using DocumentFormat.OpenXml.Features; -using DocumentFormat.OpenXml.Packaging; -using DocumentFormat.OpenXml.Presentation; -using Path = System.IO.Path; -using PBT = Clippit.PowerPoint.PresentationBuilderTools; - -namespace Clippit.PowerPoint -{ - internal sealed class FluentPresentationBuilder : IDisposable - { - private readonly PresentationDocument _newDocument; - private SlideSize _slideSize; - private bool _isDocumentInitialized; - - private readonly List _mediaCache = []; - private readonly List _slideMasterList = []; - - internal FluentPresentationBuilder(PresentationDocument presentationDocument) - { - _newDocument = presentationDocument ?? throw new NullReferenceException(nameof(presentationDocument)); - - var mainPart = _newDocument.PresentationPart.GetXDocument(); - mainPart.Declaration.Standalone = "yes"; - mainPart.Declaration.Encoding = "UTF-8"; - - _isDocumentInitialized = false; - if (presentationDocument.PresentationPart is { } presentation) - { - foreach (var slideMasterPart in presentation.SlideMasterParts) - { - foreach (var slideLayoutPart in slideMasterPart.SlideLayoutParts) - { - _ = ManageSlideLayoutPart(presentationDocument, slideLayoutPart, 1.0f); - } - } - - // TODO: enumerate all images, media, master and layouts - _slideSize = presentation.Presentation.SlideSize; - } - } - - public void Dispose() => SaveAndCleanup(); - - private void SaveAndCleanup() - { - // Remove sections list (all slides added to default section) - var presentationDocument = _newDocument.PresentationPart.GetXDocument(); - var sectionLists = presentationDocument.Descendants(P14.sectionLst).ToList(); - foreach (var sectionList in sectionLists) - { - sectionList.Parent?.Remove(); // element - } - - // Remove custom properties (source doc metadata irrelevant for generated document) - var customPropsDocument = _newDocument.CustomFilePropertiesPart?.GetXDocument(); - if (customPropsDocument?.Root?.HasElements == true) - { - customPropsDocument.Root?.RemoveNodes(); - } - - foreach (var part in _newDocument.GetAllParts()) - { - if (part.ContentType.EndsWith("+xml")) - { - var xd = part.GetXDocument(); - xd.Descendants().Attributes("smtClean").Remove(); - part.PutXDocument(); - } - else if (part.Annotation() is not null) - part.PutXDocument(); - } - } - - private void CopyStartingParts(PresentationDocument sourceDocument) - { - // A Core File Properties part does not have implicit or explicit relationships to other parts. - var corePart = sourceDocument.CoreFilePropertiesPart; - if (corePart?.GetXDocument().Root is not null) - { - _newDocument.AddCoreFilePropertiesPart(); - var newXDoc = _newDocument.CoreFilePropertiesPart.GetXDocument(); - newXDoc.Declaration.Standalone = "yes"; - newXDoc.Declaration.Encoding = "UTF-8"; - var sourceXDoc = corePart.GetXDocument(); - newXDoc.Add(sourceXDoc.Root); - } - - // An application attributes part does not have implicit or explicit relationships to other parts. - if (sourceDocument.ExtendedFilePropertiesPart is { } extPart) - { - _newDocument.AddExtendedFilePropertiesPart(); - var newXDoc = _newDocument.ExtendedFilePropertiesPart.GetXDocument(); - newXDoc.Declaration.Standalone = "yes"; - newXDoc.Declaration.Encoding = "UTF-8"; - newXDoc.Add(extPart.GetXDocument().Root); - } - - // An custom file properties part does not have implicit or explicit relationships to other parts. - if (sourceDocument.CustomFilePropertiesPart is { } customPart) - { - _newDocument.AddCustomFilePropertiesPart(); - var newXDoc = _newDocument.CustomFilePropertiesPart.GetXDocument(); - newXDoc.Declaration.Standalone = "yes"; - newXDoc.Declaration.Encoding = "UTF-8"; - newXDoc.Add(customPart.GetXDocument().Root); - } - } - -#if false - // TODO need to handle the following - - { P.custShowLst, 80 }, - { P.photoAlbum, 90 }, - { P.custDataLst, 100 }, - { P.kinsoku, 120 }, - { P.modifyVerifier, 150 }, -#endif - // Copy handout master, notes master, presentation properties and view properties, if they exist - private void CopyPresentationParts(PresentationDocument sourceDocument) - { - var newPresentation = _newDocument.PresentationPart.GetXDocument(); - - // Copy slide and note slide sizes - var oldPresentationDoc = sourceDocument.PresentationPart.GetXDocument(); - - foreach (var att in oldPresentationDoc.Root.Attributes()) - { - if (!att.IsNamespaceDeclaration && newPresentation.Root.Attribute(att.Name) is null) - newPresentation.Root.Add(oldPresentationDoc.Root.Attribute(att.Name)); - } - - if (oldPresentationDoc.Root.Elements(P.sldSz).FirstOrDefault() is { } oldElement) - newPresentation.Root.Add(oldElement); - - // Copy Font Parts - if (oldPresentationDoc.Root.Element(P.embeddedFontLst) is not null) - { - var newFontLst = new XElement(P.embeddedFontLst); - foreach (var font in oldPresentationDoc.Root.Element(P.embeddedFontLst).Elements(P.embeddedFont)) - { - var newEmbeddedFont = new XElement(P.embeddedFont, font.Elements(P.font)); - - if (font.Element(P.regular) is not null) - newEmbeddedFont.Add(CreateEmbeddedFontPart(sourceDocument, font, P.regular)); - if (font.Element(P.bold) is not null) - newEmbeddedFont.Add(CreateEmbeddedFontPart(sourceDocument, font, P.bold)); - if (font.Element(P.italic) is not null) - newEmbeddedFont.Add(CreateEmbeddedFontPart(sourceDocument, font, P.italic)); - if (font.Element(P.boldItalic) is not null) - newEmbeddedFont.Add(CreateEmbeddedFontPart(sourceDocument, font, P.boldItalic)); - - newFontLst.Add(newEmbeddedFont); - } - newPresentation.Root.Add(newFontLst); - } - - newPresentation.Root.Add(oldPresentationDoc.Root.Element(P.defaultTextStyle)); - newPresentation.Root.Add(SanitizeExtLst(oldPresentationDoc.Root.Elements(P.extLst))); - - // - // - // - // - // - // - // - - // Copy Handout Master - if (sourceDocument.PresentationPart.HandoutMasterPart is { } oldMaster) - { - var newMaster = _newDocument.PresentationPart.AddNewPart(); - - // Copy theme for master - var newThemePart = newMaster.AddNewPart(); - newThemePart.PutXDocument(new XDocument(oldMaster.ThemePart.GetXDocument())); - CopyRelatedPartsForContentParts(oldMaster.ThemePart, newThemePart, [newThemePart.GetXDocument().Root]); - - // Copy master - newMaster.PutXDocument(new XDocument(oldMaster.GetXDocument())); - PBT.AddRelationships(oldMaster, newMaster, [newMaster.GetXDocument().Root]); - CopyRelatedPartsForContentParts(oldMaster, newMaster, [newMaster.GetXDocument().Root]); - - newPresentation.Root.Add( - new XElement( - P.handoutMasterIdLst, - new XElement( - P.handoutMasterId, - new XAttribute(R.id, _newDocument.PresentationPart.GetIdOfPart(newMaster)) - ) - ) - ); - } - - // Copy Notes Master - CopyNotesMaster(sourceDocument); - - // Copy Presentation Properties - if (sourceDocument.PresentationPart.PresentationPropertiesPart is { } presentationPropertiesPart) - { - var newPart = _newDocument.PresentationPart.AddNewPart(); - var xd1 = presentationPropertiesPart.GetXDocument(); - xd1.Descendants(P.custShow).Remove(); - newPart.PutXDocument(xd1); - } - - // Copy View Properties - if (sourceDocument.PresentationPart.ViewPropertiesPart is { } viewPropertiesPart) - { - var newPart = _newDocument.PresentationPart.AddNewPart(); - var xd = viewPropertiesPart.GetXDocument(); - xd.Descendants(P.outlineViewPr).Elements(P.sldLst).Remove(); - newPart.PutXDocument(xd); - } - - foreach ( - var legacyDocTextInfo in sourceDocument.PresentationPart.Parts.Where(p => - p.OpenXmlPart.RelationshipType - == "http://schemas.microsoft.com/office/2006/relationships/legacyDocTextInfo" - ) - ) - { - var newPart = _newDocument.PresentationPart.AddNewPart(); - using var stream = legacyDocTextInfo.OpenXmlPart.GetStream(); - newPart.FeedData(stream); - } - - var listOfRootChildren = newPresentation.Root.Elements().ToList(); - foreach (var rc in listOfRootChildren) - rc.Remove(); - newPresentation.Root.Add( - listOfRootChildren.OrderBy(e => PBT.OrderPresentation.TryGetValue(e.Name, out var value) ? value : 999) - ); - } - - /// - /// This method remove custom p:ext elements from the extLst element, especially ones that reference custom metadata - /// Example: - /// - /// - /// - /// - /// List of all from source presentation.xml - /// Modified copy of all elements - private static IEnumerable SanitizeExtLst(IEnumerable extLstList) - { - foreach (var srcExtLst in extLstList) - { - // Deep clone the element - var extLst = new XElement(srcExtLst); - - // Sanitize all p:ext elements with r:Id attributes on any child element - foreach (var ext in extLst.Elements(P.ext).ToList()) - { - var hasRid = ext.Descendants().Any(e => e.Attributes().Any(a => a.Name == R.id)); - if (hasRid) - ext.Remove(); - } - - yield return extLst; - } - } - - private XElement CreateEmbeddedFontPart(PresentationDocument sourceDocument, XElement font, XName fontXName) - { - var oldFontPartId = (string)font.Element(fontXName).Attributes(R.id).FirstOrDefault(); - if (!sourceDocument.PresentationPart.TryGetPartById(oldFontPartId, out var oldFontPart)) - return null; - if (oldFontPart is not FontPart) - throw new FormatException($"Part {oldFontPartId} is not {nameof(FontPart)}"); - - var fontPartType = oldFontPart.ContentType switch - { - "application/x-fontdata" => FontPartType.FontData, - "application/x-font-ttf" => FontPartType.FontTtf, - _ => FontPartType.FontOdttf, - }; - - var newFontPartId = Relationships.GetNewRelationshipId(); - var newFontPart = _newDocument.PresentationPart.AddFontPart(fontPartType, newFontPartId); - using (var stream = oldFontPart.GetStream()) - newFontPart.FeedData(stream); - return new XElement(fontXName, new XAttribute(R.id, newFontPartId)); - } - - public void AppendMaster(PresentationDocument sourceDocument, SlideMasterPart slideMasterPart) - { - EnsureDocumentInitialized(sourceDocument); - - var scaleFactor = GetScaleFactor(sourceDocument); - foreach (var slideLayoutPart in slideMasterPart.SlideLayoutParts) - { - _ = ManageSlideLayoutPart(sourceDocument, slideLayoutPart, scaleFactor); - } - } - - private void EnsureDocumentInitialized(PresentationDocument sourceDocument) - { - if (_isDocumentInitialized) - return; - - CopyStartingParts(sourceDocument); - CopyPresentationParts(sourceDocument); - - _slideSize = sourceDocument.PresentationPart.Presentation.SlideSize.CloneNode(true) as SlideSize; - - var newPresentation = _newDocument.PresentationPart.GetXDocument(); - if (newPresentation.Root.Element(P.sldIdLst) is null) - { - newPresentation.Root.Add(new XElement(P.sldIdLst)); - } - - _isDocumentInitialized = true; - } - - public void AppendSlides(PresentationDocument sourceDocument, int start, int count) => - AppendSlides(sourceDocument, start, count, false); - - internal void AppendSlides(PresentationDocument sourceDocument, int start, int count, bool unHideSlides) - { - EnsureDocumentInitialized(sourceDocument); - - var newPresentation = _newDocument.PresentationPart.GetXDocument(); - var scaleFactor = GetScaleFactor(sourceDocument); - - uint newId = 256; - var ids = newPresentation.Root.Descendants(P.sldId).Select(f => (uint)f.Attribute(NoNamespace.id)).ToList(); - if (ids.Count != 0) - newId = ids.Max() + 1; - - var slideList = sourceDocument.PresentationPart.GetXDocument().Root.Descendants(P.sldId).ToList(); - while (count > 0 && start < slideList.Count) - { - var slide = (SlidePart) - sourceDocument.PresentationPart.GetPartById(slideList.ElementAt(start).Attribute(R.id).Value); - var newSlide = _newDocument.PresentationPart.AddNewPart(); - - using (var sourceStream = slide.GetStream()) - { - newSlide.FeedData(sourceStream); - } - - var slideDocument = newSlide.GetXDocument(); - if (unHideSlides) - { - slideDocument.Root?.Attribute(NoNamespace.show)?.Remove(); - } - - SlideLayoutData.ScaleShapes(slideDocument, scaleFactor); - - PBT.AddRelationships(slide, newSlide, [newSlide.GetXDocument().Root]); - CopyRelatedPartsForContentParts(slide, newSlide, [newSlide.GetXDocument().Root]); - CopyTableStyles(sourceDocument, newSlide); - - if (slide.NotesSlidePart is { } notesSlide) - { - if (_newDocument.PresentationPart.NotesMasterPart is null) - CopyNotesMaster(sourceDocument); - var newPart = newSlide.AddNewPart(); - newPart.PutXDocument(notesSlide.GetXDocument()); - newPart.AddPart(newSlide); - if (_newDocument.PresentationPart.NotesMasterPart is not null) - newPart.AddPart(_newDocument.PresentationPart.NotesMasterPart); - PBT.AddRelationships(notesSlide, newPart, [newPart.GetXDocument().Root]); - CopyRelatedPartsForContentParts(slide.NotesSlidePart, newPart, [newPart.GetXDocument().Root]); - - notesSlide.RemoveAnnotations(); - notesSlide.UnloadRootElement(); - } - - var slideLayoutData = ManageSlideLayoutPart(sourceDocument, slide.SlideLayoutPart, scaleFactor); - newSlide.AddPart(slideLayoutData.Part); - - if (slide.SlideCommentsPart is not null) - CopyComments(sourceDocument, slide, newSlide); - - newPresentation = _newDocument.PresentationPart.GetXDocument(); - newPresentation - .Root.Element(P.sldIdLst) - .Add( - new XElement( - P.sldId, - new XAttribute(NoNamespace.id, newId.ToString()), - new XAttribute(R.id, _newDocument.PresentationPart.GetIdOfPart(newSlide)) - ) - ); - - slide.RemoveAnnotations(); - slide.UnloadRootElement(); - - newId++; - start++; - count--; - } - } - - private double GetScaleFactor(PresentationDocument sourceDocument) - { - var slideSize = sourceDocument.PresentationPart.Presentation.SlideSize; - var scaleFactorX = (double)_slideSize.Cx / slideSize.Cx; - var scaleFactorY = (double)_slideSize.Cy / slideSize.Cy; - return Math.Min(scaleFactorX, scaleFactorY); - } - - // Copies notes master and notesSz element from presentation - private void CopyNotesMaster(PresentationDocument sourceDocument) - { - // Copy notesSz element from presentation - var newPresentation = _newDocument.PresentationPart.GetXDocument(); - var oldPresentationDoc = sourceDocument.PresentationPart.GetXDocument(); - var oldElement = oldPresentationDoc.Root.Element(P.notesSz); - newPresentation.Root.Element(P.notesSz).ReplaceWith(oldElement); - - // Copy Notes Master - if (sourceDocument.PresentationPart.NotesMasterPart is { } oldMaster) - { - var newMaster = _newDocument.PresentationPart.AddNewPart(); - - // Copy theme for master - if (oldMaster.ThemePart is { } themePart) - { - var newThemePart = newMaster.AddNewPart(); - newThemePart.PutXDocument(new XDocument(themePart.GetXDocument())); - CopyRelatedPartsForContentParts(themePart, newThemePart, [newThemePart.GetXDocument().Root]); - } - - // Copy master - newMaster.PutXDocument(new XDocument(oldMaster.GetXDocument())); - PBT.AddRelationships(oldMaster, newMaster, [newMaster.GetXDocument().Root]); - CopyRelatedPartsForContentParts(oldMaster, newMaster, [newMaster.GetXDocument().Root]); - - newPresentation.Root.Add( - new XElement( - P.notesMasterIdLst, - new XElement( - P.notesMasterId, - new XAttribute(R.id, _newDocument.PresentationPart.GetIdOfPart(newMaster)) - ) - ) - ); - } - } - - private void CopyComments(PresentationDocument oldDocument, SlidePart oldSlide, SlidePart newSlide) - { - newSlide.AddNewPart(); - newSlide.SlideCommentsPart.PutXDocument(new XDocument(oldSlide.SlideCommentsPart.GetXDocument())); - var newSlideComments = newSlide.SlideCommentsPart.GetXDocument(); - var oldAuthors = oldDocument.PresentationPart.CommentAuthorsPart.GetXDocument(); - foreach (var comment in newSlideComments.Root.Elements(P.cm)) - { - var newAuthor = FindCommentsAuthor(comment, oldAuthors); - // Update last index value for new comment - comment.Attribute(NoNamespace.authorId).SetValue(newAuthor.Attribute(NoNamespace.id).Value); - var lastIndex = Convert.ToUInt32(newAuthor.Attribute(NoNamespace.lastIdx).Value); - comment.Attribute(NoNamespace.idx).SetValue(lastIndex.ToString()); - newAuthor.Attribute(NoNamespace.lastIdx).SetValue(Convert.ToString(lastIndex + 1)); - } - } - - private XElement FindCommentsAuthor(XElement comment, XDocument oldAuthors) - { - var oldAuthor = oldAuthors - .Root.Elements(P.cmAuthor) - .FirstOrDefault(f => - f.Attribute(NoNamespace.id).Value == comment.Attribute(NoNamespace.authorId).Value - ); - XElement newAuthor = null; - if (_newDocument.PresentationPart.CommentAuthorsPart is null) - { - _newDocument.PresentationPart.AddNewPart(); - _newDocument.PresentationPart.CommentAuthorsPart.PutXDocument( - new XDocument( - new XElement( - P.cmAuthorLst, - new XAttribute(XNamespace.Xmlns + "a", A.a), - new XAttribute(XNamespace.Xmlns + "r", R.r), - new XAttribute(XNamespace.Xmlns + "p", P.p) - ) - ) - ); - } - var authors = _newDocument.PresentationPart.CommentAuthorsPart.GetXDocument(); - newAuthor = authors - .Root.Elements(P.cmAuthor) - .FirstOrDefault(f => - f.Attribute(NoNamespace.initials).Value == oldAuthor.Attribute(NoNamespace.initials).Value - ); - if (newAuthor is null) - { - uint newId = 0; - var ids = authors.Root.Descendants(P.cmAuthor).Select(f => (uint)f.Attribute(NoNamespace.id)).ToList(); - if (ids.Count != 0) - newId = ids.Max() + 1; - - newAuthor = new XElement( - P.cmAuthor, - new XAttribute(NoNamespace.id, newId.ToString()), - new XAttribute(NoNamespace.name, oldAuthor.Attribute(NoNamespace.name).Value), - new XAttribute(NoNamespace.initials, oldAuthor.Attribute(NoNamespace.initials).Value), - new XAttribute(NoNamespace.lastIdx, "1"), - new XAttribute(NoNamespace.clrIdx, newId.ToString()) - ); - authors.Root.Add(newAuthor); - } - - return newAuthor; - } - - private void CopyTableStyles(PresentationDocument oldDocument, OpenXmlPart newContentPart) - { - if (oldDocument.PresentationPart.TableStylesPart is null) - return; - - var oldTableStylesDocument = oldDocument.PresentationPart.TableStylesPart.GetXDocument(); - var oldTableStyles = oldTableStylesDocument.Root.Elements(A.tblStyle).ToList(); - - foreach (var table in newContentPart.GetXDocument().Descendants(A.tableStyleId)) - { - var styleId = table.Value; - if (string.IsNullOrEmpty(styleId)) - continue; - - // Find old style - var oldStyle = oldTableStyles.FirstOrDefault(f => f.Attribute(NoNamespace.styleId).Value == styleId); - if (oldStyle is null) - continue; - - // Create new TableStylesPart, if needed - XDocument tableStyles; - if (_newDocument.PresentationPart.TableStylesPart is null) - { - var newStylesPart = _newDocument.PresentationPart.AddNewPart(); - tableStyles = new XDocument( - new XElement( - A.tblStyleLst, - new XAttribute(XNamespace.Xmlns + "a", A.a), - new XAttribute(NoNamespace.def, styleId) - ) - ); - newStylesPart.PutXDocument(tableStyles); - } - else - tableStyles = _newDocument.PresentationPart.TableStylesPart.GetXDocument(); - - // Search new TableStylesPart to see if it contains the ID - if ( - tableStyles - .Root.Elements(A.tblStyle) - .FirstOrDefault(f => f.Attribute(NoNamespace.styleId).Value == styleId) - is not null - ) - continue; - - // Copy style to new part - tableStyles.Root.Add(oldStyle); - } - } - - private void CopyRelatedPartsForContentParts( - OpenXmlPart oldContentPart, - OpenXmlPart newContentPart, - IEnumerable newContent - ) - { - var relevantElements = newContent - .DescendantsAndSelf() - .Where(d => - d.Name == VML.imagedata - || d.Name == VML.fill - || d.Name == VML.stroke - || d.Name == A.blip - || d.Name == SVG.svgBlip - ) - .ToList(); - foreach (var imageReference in relevantElements) - { - CopyRelatedImage(oldContentPart, newContentPart, imageReference, R.embed); - CopyRelatedImage(oldContentPart, newContentPart, imageReference, R.pict); - CopyRelatedImage(oldContentPart, newContentPart, imageReference, R.id); - CopyRelatedImage(oldContentPart, newContentPart, imageReference, O.relid); - } - - relevantElements = newContent - .DescendantsAndSelf() - .Where(d => d.Name == A.videoFile || d.Name == A.quickTimeFile) - .ToList(); - foreach (var imageReference in relevantElements) - { - CopyRelatedMedia(oldContentPart, newContentPart, imageReference, R.link, "video"); - } - - relevantElements = newContent - .DescendantsAndSelf() - .Where(d => d.Name == P14.media || d.Name == PAV.srcMedia) - .ToList(); - foreach (var imageReference in relevantElements) - { - CopyRelatedMedia(oldContentPart, newContentPart, imageReference, R.embed, "media"); - PBT.CopyRelatedMediaExternalRelationship(oldContentPart, newContentPart, imageReference, R.link); - } - - foreach (var extendedReference in newContent.DescendantsAndSelf(A14.imgLayer)) - { - PBT.CopyExtendedPart(oldContentPart, newContentPart, extendedReference, R.embed); - } - - foreach (var contentPartReference in newContent.DescendantsAndSelf(P.contentPart)) - { - PBT.CopyInkPart(oldContentPart, newContentPart, contentPartReference, R.id); - } - - foreach (var contentPartReference in newContent.DescendantsAndSelf(P.control)) - { - PBT.CopyActiveXPart(oldContentPart, newContentPart, contentPartReference, R.id); - } - - foreach (var contentPartReference in newContent.DescendantsAndSelf(Plegacy.textdata)) - { - PBT.CopyLegacyDiagramText(oldContentPart, newContentPart, contentPartReference, "id"); - } - - foreach ( - var diagramReference in newContent - .DescendantsAndSelf() - .Where(d => d.Name == DGM.relIds || d.Name == A.relIds) - ) - { - // dm attribute - var relId = diagramReference.Attribute(R.dm).Value; - if (newContentPart.HasRelationship(relId)) - continue; - - var oldPart = oldContentPart.GetPartById(relId); - OpenXmlPart newPart = newContentPart.AddNewPart(); - newPart.GetXDocument().Add(oldPart.GetXDocument().Root); - diagramReference.Attribute(R.dm).Value = newContentPart.GetIdOfPart(newPart); - PBT.AddRelationships(oldPart, newPart, [newPart.GetXDocument().Root]); - CopyRelatedPartsForContentParts(oldPart, newPart, [newPart.GetXDocument().Root]); - - // lo attribute - relId = diagramReference.Attribute(R.lo).Value; - if (newContentPart.HasRelationship(relId)) - continue; - - oldPart = oldContentPart.GetPartById(relId); - newPart = newContentPart.AddNewPart(); - newPart.GetXDocument().Add(oldPart.GetXDocument().Root); - diagramReference.Attribute(R.lo).Value = newContentPart.GetIdOfPart(newPart); - PBT.AddRelationships(oldPart, newPart, [newPart.GetXDocument().Root]); - CopyRelatedPartsForContentParts(oldPart, newPart, [newPart.GetXDocument().Root]); - - // qs attribute - relId = diagramReference.Attribute(R.qs).Value; - if (newContentPart.HasRelationship(relId)) - continue; - - oldPart = oldContentPart.GetPartById(relId); - newPart = newContentPart.AddNewPart(); - newPart.GetXDocument().Add(oldPart.GetXDocument().Root); - diagramReference.Attribute(R.qs).Value = newContentPart.GetIdOfPart(newPart); - PBT.AddRelationships(oldPart, newPart, [newPart.GetXDocument().Root]); - CopyRelatedPartsForContentParts(oldPart, newPart, [newPart.GetXDocument().Root]); - - // cs attribute - relId = diagramReference.Attribute(R.cs).Value; - if (newContentPart.HasRelationship(relId)) - continue; - - oldPart = oldContentPart.GetPartById(relId); - newPart = newContentPart.AddNewPart(); - newPart.GetXDocument().Add(oldPart.GetXDocument().Root); - diagramReference.Attribute(R.cs).Value = newContentPart.GetIdOfPart(newPart); - PBT.AddRelationships(oldPart, newPart, [newPart.GetXDocument().Root]); - CopyRelatedPartsForContentParts(oldPart, newPart, [newPart.GetXDocument().Root]); - } - - foreach ( - var oleReference in newContent - .DescendantsAndSelf() - .Where(d => d.Name == P.oleObj || d.Name == P.externalData) - ) - { - var relId = oleReference.Attribute(R.id).Value; - - // First look to see if this relId has already been added to the new document. - // This is necessary for those parts that get processed with both old and new ids, such as the comments - // part. This is not necessary for parts such as the main document part, but this code won't malfunction - // in that case. - if (newContentPart.HasRelationship(relId)) - continue; - - var oldPartIdPair = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); - if (oldPartIdPair != default) - { - var oldPart = oldPartIdPair.OpenXmlPart; - OpenXmlPart newPart = null; - newPart = oldPart switch - { - EmbeddedObjectPart => newContentPart switch - { - DialogsheetPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), - HandoutMasterPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), - NotesMasterPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), - NotesSlidePart part => part.AddEmbeddedObjectPart(oldPart.ContentType), - SlideLayoutPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), - SlideMasterPart part => part.AddEmbeddedObjectPart(oldPart.ContentType), - SlidePart part => part.AddEmbeddedObjectPart(oldPart.ContentType), - _ => newPart, - }, - EmbeddedPackagePart => newContentPart switch - { - ChartPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), - HandoutMasterPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), - NotesMasterPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), - NotesSlidePart part => part.AddEmbeddedPackagePart(oldPart.ContentType), - SlideLayoutPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), - SlideMasterPart part => part.AddEmbeddedPackagePart(oldPart.ContentType), - SlidePart part => part.AddEmbeddedPackagePart(oldPart.ContentType), - _ => newPart, - }, - _ => newPart, - }; - using (var oldObject = oldPart.GetStream(FileMode.Open, FileAccess.Read)) - { - newPart.FeedData(oldObject); - } - oleReference.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); - } - else - { - var er = oldContentPart.GetExternalRelationship(relId); - var newEr = newContentPart.AddExternalRelationship(er.RelationshipType, er.Uri); - oleReference.Attribute(R.id).Set(newEr.Id); - } - } - - foreach (var chartReference in newContent.DescendantsAndSelf(C.chart)) - { - var relId = (string)chartReference.Attribute(R.id); - if (newContentPart.HasRelationship(relId)) - continue; - - var oldPartIdPair2 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); - if (oldPartIdPair2.OpenXmlPart is ChartPart oldPart) - { - var oldChart = oldPart.GetXDocument(); - var newPart = newContentPart.AddNewPart(); - var newChart = newPart.GetXDocument(); - newChart.Add(oldChart.Root); - chartReference.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); - PBT.CopyChartObjects(oldPart, newPart); - CopyRelatedPartsForContentParts(oldPart, newPart, [newChart.Root]); - } - } - - foreach (var chartReference in newContent.DescendantsAndSelf(Cx.chart)) - { - var relId = (string)chartReference.Attribute(R.id); - if (newContentPart.HasRelationship(relId)) - continue; - - var oldPartIdPair2 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); - if (oldPartIdPair2.OpenXmlPart is ExtendedChartPart oldPart) - { - var oldChart = oldPart.GetXDocument(); - var newPart = newContentPart.AddNewPart(); - var newChart = newPart.GetXDocument(); - newChart.Add(oldChart.Root); - chartReference.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); - PBT.CopyExtendedChartObjects(oldPart, newPart); - CopyRelatedPartsForContentParts(oldPart, newPart, [newChart.Root]); - } - } - - foreach (var userShape in newContent.DescendantsAndSelf(C.userShapes)) - { - var relId = (string)userShape.Attribute(R.id); - if (newContentPart.HasRelationship(relId)) - continue; - - var oldPartIdPair3 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); - if (oldPartIdPair3.OpenXmlPart is ChartDrawingPart oldPart) - { - var oldXDoc = oldPart.GetXDocument(); - var newPart = newContentPart.AddNewPart(); - var newXDoc = newPart.GetXDocument(); - newXDoc.Add(oldXDoc.Root); - userShape.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); - PBT.AddRelationships(oldPart, newPart, newContent); - CopyRelatedPartsForContentParts(oldPart, newPart, [newXDoc.Root]); - } - } - - foreach (var tags in newContent.DescendantsAndSelf(P.tags)) - { - var relId = (string)tags.Attribute(R.id); - if (newContentPart.HasRelationship(relId)) - continue; - - var oldPartIdPair4 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); - if (oldPartIdPair4.OpenXmlPart is UserDefinedTagsPart oldPart) - { - var oldXDoc = oldPart.GetXDocument(); - var newPart = newContentPart.AddNewPart(); - var newXDoc = newPart.GetXDocument(); - newXDoc.Add(oldXDoc.Root); - tags.Attribute(R.id).Value = newContentPart.GetIdOfPart(newPart); - } - } - - foreach (var custData in newContent.DescendantsAndSelf(P.custData)) - { - var relId = (string)custData.Attribute(R.id); - if (string.IsNullOrEmpty(relId) || newContentPart.Parts.Any(p => p.RelationshipId == relId)) - continue; - - var oldPartIdPair9 = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); - if (oldPartIdPair9 != default) - { - var newPart = _newDocument.PresentationPart.AddCustomXmlPart(CustomXmlPartType.CustomXml); - using (var stream = oldPartIdPair9.OpenXmlPart.GetStream()) - newPart.FeedData(stream); - foreach ( - var itemProps in oldPartIdPair9.OpenXmlPart.Parts.Where(p => - p.OpenXmlPart.ContentType - == "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" - ) - ) - { - var newId2 = Relationships.GetNewRelationshipId(); - var cxpp = newPart.AddNewPart( - "application/vnd.openxmlformats-officedocument.customXmlProperties+xml", - newId2 - ); - using var stream = itemProps.OpenXmlPart.GetStream(); - cxpp.FeedData(stream); - } - var newId = Relationships.GetNewRelationshipId(); - newContentPart.CreateRelationshipToPart(newPart, newId); - custData.Attribute(R.id).Value = newId; - } - } - - foreach (var soundReference in newContent.DescendantsAndSelf().Where(d => d.Name == A.audioFile)) - PBT.CopyRelatedSound(_newDocument, oldContentPart, newContentPart, soundReference, R.link); - - if ( - (oldContentPart is ChartsheetPart && newContentPart is ChartsheetPart) - || (oldContentPart is DialogsheetPart && newContentPart is DialogsheetPart) - || (oldContentPart is HandoutMasterPart && newContentPart is HandoutMasterPart) - || (oldContentPart is InternationalMacroSheetPart && newContentPart is InternationalMacroSheetPart) - || (oldContentPart is MacroSheetPart && newContentPart is MacroSheetPart) - || (oldContentPart is NotesMasterPart && newContentPart is NotesMasterPart) - || (oldContentPart is NotesSlidePart && newContentPart is NotesSlidePart) - || (oldContentPart is SlideLayoutPart && newContentPart is SlideLayoutPart) - || (oldContentPart is SlideMasterPart && newContentPart is SlideMasterPart) - || (oldContentPart is SlidePart && newContentPart is SlidePart) - || (oldContentPart is WorksheetPart && newContentPart is WorksheetPart) - ) - { - foreach ( - var soundReference in newContent - .DescendantsAndSelf() - .Where(d => - d.Name == P.snd - || d.Name == P.sndTgt - || d.Name == A.wavAudioFile - || d.Name == A.snd - || d.Name == PAV.srcMedia - ) - ) - PBT.CopyRelatedSound(_newDocument, oldContentPart, newContentPart, soundReference, R.embed); - - var vmlDrawingParts = oldContentPart switch - { - ChartsheetPart part => part.VmlDrawingParts, - DialogsheetPart part => part.VmlDrawingParts, - HandoutMasterPart part => part.VmlDrawingParts, - InternationalMacroSheetPart part => part.VmlDrawingParts, - MacroSheetPart part => part.VmlDrawingParts, - NotesMasterPart part => part.VmlDrawingParts, - NotesSlidePart part => part.VmlDrawingParts, - SlideLayoutPart part => part.VmlDrawingParts, - SlideMasterPart part => part.VmlDrawingParts, - SlidePart part => part.VmlDrawingParts, - WorksheetPart part => part.VmlDrawingParts, - _ => null, - }; - - if (vmlDrawingParts is not null) - { - // Transitional: Copy VML Drawing parts, implicit relationship - foreach (var vmlPart in vmlDrawingParts) - { - var newVmlPart = newContentPart switch - { - ChartsheetPart part => part.AddNewPart(), - DialogsheetPart part => part.AddNewPart(), - HandoutMasterPart part => part.AddNewPart(), - InternationalMacroSheetPart part => part.AddNewPart(), - MacroSheetPart part => part.AddNewPart(), - NotesMasterPart part => part.AddNewPart(), - NotesSlidePart part => part.AddNewPart(), - SlideLayoutPart part => part.AddNewPart(), - SlideMasterPart part => part.AddNewPart(), - SlidePart part => part.AddNewPart(), - WorksheetPart part => part.AddNewPart(), - _ => null, - }; - - try - { - var xd = new XDocument(vmlPart.GetXDocument()); - foreach (var item in xd.Descendants(O.ink)) - { - if (item.Attribute("i") is { } attr) - { - var i = attr.Value; - i = i.Replace(" ", "\r\n"); - attr.Value = i; - } - } - newVmlPart.PutXDocument(xd); - - PBT.AddRelationships(vmlPart, newVmlPart, [newVmlPart.GetXDocument().Root]); - CopyRelatedPartsForContentParts(vmlPart, newVmlPart, [newVmlPart.GetXDocument().Root]); - } - catch (XmlException) - { - using var srcStream = vmlPart.GetStream(); - using var dstStream = newVmlPart.GetStream(FileMode.Create, FileAccess.Write); - srcStream.CopyTo(dstStream); - } - } - } - } - } - - private void CopyRelatedImage( - OpenXmlPart oldContentPart, - OpenXmlPart newContentPart, - XElement imageReference, - XName attributeName - ) - { - // First look to see if this relId has already been added to the new document. - // This is necessary for those parts that get processed with both old and new ids, such as the comments - // part. This is not necessary for parts such as the main document part, but this code won't malfunction - // in that case. - - var relId = (string)imageReference.Attribute(attributeName); - if (newContentPart.HasRelationship(relId)) - return; - - var oldPartIdPair = oldContentPart.Parts.FirstOrDefault(p => p.RelationshipId == relId); - if (oldPartIdPair != default) - { - var oldPart = oldPartIdPair.OpenXmlPart as ImagePart; - var temp = ManageImageCopy(oldPart); - if (temp.ImagePart is null) - { - var contentType = oldPart?.ContentType; - var targetExtension = contentType switch - { - "image/bmp" => ".bmp", - "image/gif" => ".gif", - "image/png" => ".png", - "image/tiff" => ".tiff", - "image/x-icon" => ".ico", - "image/x-pcx" => ".pcx", - "image/jpeg" => ".jpg", - "image/x-emf" => ".emf", - "image/x-wmf" => ".wmf", - "image/svg+xml" => ".svg", - _ => ".image", - }; - newContentPart.Features.GetRequired().Register(contentType, targetExtension); - - var newPart = newContentPart switch - { - ChartDrawingPart part => part.AddImagePart(contentType), - ChartPart part => part.AddImagePart(contentType), - ChartsheetPart part => part.AddImagePart(contentType), - DiagramDataPart part => part.AddImagePart(contentType), - DiagramLayoutDefinitionPart part => part.AddImagePart(contentType), - DiagramPersistLayoutPart part => part.AddImagePart(contentType), - DrawingsPart part => part.AddImagePart(contentType), - HandoutMasterPart part => part.AddImagePart(contentType), - NotesMasterPart part => part.AddImagePart(contentType), - NotesSlidePart part => part.AddImagePart(contentType), - RibbonAndBackstageCustomizationsPart part => part.AddImagePart(contentType), - RibbonExtensibilityPart part => part.AddImagePart(contentType), - SlideLayoutPart part => part.AddImagePart(contentType), - SlideMasterPart part => part.AddImagePart(contentType), - SlidePart part => part.AddImagePart(contentType), - ThemeOverridePart part => part.AddImagePart(contentType), - ThemePart part => part.AddImagePart(contentType), - VmlDrawingPart part => part.AddImagePart(contentType), - WorksheetPart part => part.AddImagePart(contentType), - _ => null, - }; - - temp.ImagePart = newPart; - var id = newContentPart.GetIdOfPart(newPart); - temp.AddContentPartRelTypeResourceIdTupple(newContentPart, newPart.RelationshipType, id); - - using (var stream = oldPart.GetStream()) - newPart.FeedData(stream); - imageReference.SetAttributeValue(attributeName, id); - } - else - { - var refRel = newContentPart.DataPartReferenceRelationships.FirstOrDefault(rr => - temp.ContentPartRelTypeIdList.Exists(cpr => - cpr.ContentPart == newContentPart && cpr.RelationshipId == rr.Id - ) - ); - if (refRel is not null) - { - var relationshipId = temp - .ContentPartRelTypeIdList.First(cpr => - cpr.ContentPart == newContentPart && cpr.RelationshipId == refRel.Id - ) - .RelationshipId; - imageReference.SetAttributeValue(attributeName, relationshipId); - return; - } - - var cpr2 = temp.ContentPartRelTypeIdList.FirstOrDefault(c => c.ContentPart == newContentPart); - if (cpr2 is not null) - { - imageReference.SetAttributeValue(attributeName, cpr2.RelationshipId); - } - else - { - var imagePart = (ImagePart)temp.ImagePart; - var existingImagePart = newContentPart.AddPart(imagePart); - var newId = newContentPart.GetIdOfPart(existingImagePart); - temp.AddContentPartRelTypeResourceIdTupple(newContentPart, imagePart.RelationshipType, newId); - imageReference.SetAttributeValue(attributeName, newId); - } - } - } - else - { - var er = oldContentPart.ExternalRelationships.FirstOrDefault(r => r.Id == relId); - if (er is not null) - { - var newEr = newContentPart.AddExternalRelationship(er.RelationshipType, er.Uri); - imageReference.SetAttributeValue(R.id, newEr.Id); - } - else - { - var newPart = newContentPart - .OpenXmlPackage.GetPackage() - .GetParts() - .FirstOrDefault(p => p.Uri == newContentPart.Uri); - if (newPart is not null && !newPart.Relationships.Contains(relId)) - { - newPart.Relationships.Create( - new Uri("NULL", UriKind.RelativeOrAbsolute), - System.IO.Packaging.TargetMode.Internal, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - relId - ); - } - } - } - } - - private void CopyRelatedMedia( - OpenXmlPart oldContentPart, - OpenXmlPart newContentPart, - XElement imageReference, - XName attributeName, - string mediaRelationshipType - ) - { - var relId = (string)imageReference.Attribute(attributeName); - if ( - string.IsNullOrEmpty(relId) || newContentPart.DataPartReferenceRelationships.Any(dpr => dpr.Id == relId) - ) // First look to see if this relId has already been added to the new document. - return; - - var oldRel = oldContentPart.DataPartReferenceRelationships.FirstOrDefault(dpr => dpr.Id == relId); - if (oldRel is null) - return; - - var oldPart = oldRel.DataPart; - var temp = ManageMediaCopy(oldPart); - if (temp.DataPart is null) - { - var ct = oldPart.ContentType; - var ext = Path.GetExtension(oldPart.Uri.OriginalString); - var newPart = newContentPart.OpenXmlPackage.CreateMediaDataPart(ct, ext); - using (var stream = oldPart.GetStream()) - newPart.FeedData(stream); - string id = null; - string relationshipType = null; - - switch (mediaRelationshipType) - { - case "media": - { - var mrr = newContentPart switch - { - SlidePart part => part.AddMediaReferenceRelationship(newPart), - SlideLayoutPart part => part.AddMediaReferenceRelationship(newPart), - SlideMasterPart part => part.AddMediaReferenceRelationship(newPart), - _ => null, - }; - - id = mrr?.Id; - relationshipType = "http://schemas.microsoft.com/office/2007/relationships/media"; - break; - } - case "video": - { - var vrr = newContentPart switch - { - SlidePart part => part.AddVideoReferenceRelationship(newPart), - HandoutMasterPart part => part.AddVideoReferenceRelationship(newPart), - NotesMasterPart part => part.AddVideoReferenceRelationship(newPart), - NotesSlidePart part => part.AddVideoReferenceRelationship(newPart), - SlideLayoutPart part => part.AddVideoReferenceRelationship(newPart), - SlideMasterPart part => part.AddVideoReferenceRelationship(newPart), - _ => null, - }; - - id = vrr?.Id; - relationshipType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"; - break; - } - } - temp.DataPart = newPart; - temp.AddContentPartRelTypeResourceIdTupple(newContentPart, relationshipType, id); - imageReference.Attribute(attributeName).Set(id); - } - else - { - var desiredRelType = mediaRelationshipType switch - { - "media" => "http://schemas.microsoft.com/office/2007/relationships/media", - "video" => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video", - _ => null, - }; - - var existingRel = temp.ContentPartRelTypeIdList.FirstOrDefault(cp => - cp.ContentPart == newContentPart && cp.RelationshipType == desiredRelType - ); - if (existingRel is not null) - { - imageReference.Attribute(attributeName).Set(existingRel.RelationshipId); - } - else - { - var newPart = (MediaDataPart)temp.DataPart; - string id = null; - string relationshipType = null; - switch (mediaRelationshipType) - { - case "media": - { - var mrr = newContentPart switch - { - SlidePart part => part.AddMediaReferenceRelationship(newPart), - SlideLayoutPart part => part.AddMediaReferenceRelationship(newPart), - SlideMasterPart part => part.AddMediaReferenceRelationship(newPart), - _ => null, - }; - - id = mrr?.Id; - relationshipType = mrr?.RelationshipType; - break; - } - case "video": - { - var vrr = newContentPart switch - { - SlidePart part => part.AddVideoReferenceRelationship(newPart), - HandoutMasterPart part => part.AddVideoReferenceRelationship(newPart), - NotesMasterPart part => part.AddVideoReferenceRelationship(newPart), - NotesSlidePart part => part.AddVideoReferenceRelationship(newPart), - SlideLayoutPart part => part.AddVideoReferenceRelationship(newPart), - SlideMasterPart part => part.AddVideoReferenceRelationship(newPart), - _ => null, - }; - - id = vrr?.Id; - relationshipType = vrr?.RelationshipType; - break; - } - } - temp.AddContentPartRelTypeResourceIdTupple(newContentPart, relationshipType, id); - imageReference.Attribute(attributeName).Set(id); - } - } - } - - // General function for handling images that tries to use an existing image if they are the same - private ImageData ManageImageCopy(ImagePart oldImage) - { - return GetOrAddCachedMedia(new ImageData(oldImage)); - } - - // General function for handling media that tries to use an existing media item if they are the same - private MediaData ManageMediaCopy(DataPart oldMedia) - { - return GetOrAddCachedMedia(new MediaData(oldMedia)); - } - - private T GetOrAddCachedMedia(T contentData) - where T : ContentData - { - var duplicateItem = _mediaCache.FirstOrDefault(x => x.Compare(contentData)); - if (duplicateItem != null) - { - return (T)duplicateItem; - } - - _mediaCache.Add(contentData); - return contentData; - } - - private ThemePart CopyThemePart(SlideMasterPart slideMasterPart, ThemePart oldThemePart, double scaleFactor) - { - var newThemePart = slideMasterPart.AddNewPart(); - var newThemeDoc = new XDocument(oldThemePart.GetXDocument()); - SlideLayoutData.ScaleShapes(newThemeDoc, scaleFactor); - newThemePart.PutXDocument(newThemeDoc); - - CopyRelatedPartsForContentParts(oldThemePart, newThemePart, [newThemePart.GetXDocument().Root]); - - if (_newDocument.PresentationPart.ThemePart is null) - newThemePart = _newDocument.PresentationPart.AddPart(newThemePart); - - return newThemePart; - } - - // General function for handling SlideMasterPart that tries to use an existing SlideMasterPart if they are the same - private SlideMasterData ManageSlideMasterPart( - PresentationDocument presentationDocument, - SlideMasterPart slideMasterPart, - double scaleFactor - ) - { - var slideMasterData = new SlideMasterData(slideMasterPart, scaleFactor); - foreach (var item in _slideMasterList) - { - if (item.CompareTo(slideMasterData) == 0) - return item; - } - - if (!ReferenceEquals(presentationDocument, _newDocument)) - { - var newSlideMasterPart = CopySlideMasterPart(slideMasterPart, scaleFactor); - slideMasterData = new SlideMasterData(newSlideMasterPart, 1.0); - } - - _slideMasterList.Add(slideMasterData); - return slideMasterData; - } - - private SlideMasterPart CopySlideMasterPart(SlideMasterPart oldMasterPart, double scaleFactor) - { - var newMaster = _newDocument.PresentationPart.AddNewPart(); - - // Add to presentation slide master list, need newID for layout IDs also - var presentationPartDoc = _newDocument.PresentationPart.GetXDocument(); - presentationPartDoc - .Root.Element(P.sldMasterIdLst) - .Add( - new XElement( - P.sldMasterId, - new XAttribute(NoNamespace.id, GetNextFreeId().ToString()), - new XAttribute(R.id, _newDocument.PresentationPart.GetIdOfPart(newMaster)) - ) - ); - - // Ensure that master does not keep ids of old layouts - var newMasterDoc = new XDocument(oldMasterPart.GetXDocument()); - var sldLayoutIdLst = newMasterDoc.Root.Element(P.sldLayoutIdLst); - if (sldLayoutIdLst is null) - { - newMasterDoc.Root.Add(new XElement(P.sldLayoutIdLst)); - } - else - { - sldLayoutIdLst.Descendants(P.sldLayoutId).ToList().ForEach(e => e.Remove()); - } - - SlideLayoutData.ScaleShapes(newMasterDoc, scaleFactor); - newMaster.PutXDocument(newMasterDoc); - - PBT.AddRelationships(oldMasterPart, newMaster, [newMaster.GetXDocument().Root]); - CopyRelatedPartsForContentParts(oldMasterPart, newMaster, [newMaster.GetXDocument().Root]); - - _ = CopyThemePart(newMaster, oldMasterPart.ThemePart, scaleFactor); - - return newMaster; - } - - // General function for handling SlideMasterPart that tries to use an existing SlideMasterPart if they are the same - private SlideLayoutData ManageSlideLayoutPart( - PresentationDocument presentationDocument, - SlideLayoutPart slideLayoutPart, - double scaleFactor - ) - { - var slideMasterData = ManageSlideMasterPart( - presentationDocument, - slideLayoutPart.SlideMasterPart, - scaleFactor - ); - - var slideLayoutData = new SlideLayoutData(slideLayoutPart, scaleFactor); - foreach (var item in slideMasterData.SlideLayoutList) - { - if (item.CompareTo(slideLayoutData) == 0) - return item; - } - - if (!ReferenceEquals(presentationDocument, _newDocument)) - { - var newSlideLayoutPart = CopySlideLayoutPart(slideMasterData.Part, slideLayoutPart, scaleFactor); - slideLayoutData = new SlideLayoutData(newSlideLayoutPart, 1.0); - } - - slideMasterData.SlideLayoutList.Add(slideLayoutData); - return slideLayoutData; - } - - private SlideLayoutPart CopySlideLayoutPart( - SlideMasterPart newSlideMasterPart, - SlideLayoutPart oldSlideLayoutPart, - double scaleFactor - ) - { - var newLayout = newSlideMasterPart.AddNewPart(); - newLayout.AddPart(newSlideMasterPart); - - var newLayoutDoc = new XDocument(oldSlideLayoutPart.GetXDocument()); - SlideLayoutData.ScaleShapes(newLayoutDoc, scaleFactor); - newLayout.PutXDocument(newLayoutDoc); - - PBT.AddRelationships(oldSlideLayoutPart, newLayout, [newLayout.GetXDocument().Root]); - CopyRelatedPartsForContentParts(oldSlideLayoutPart, newLayout, [newLayout.GetXDocument().Root]); - - var newMasterDoc = newSlideMasterPart.GetXDocument(); - newMasterDoc - .Root.Element(P.sldLayoutIdLst) - .Add( - new XElement( - P.sldLayoutId, - new XAttribute(NoNamespace.id, GetNextFreeId().ToString()), - new XAttribute(R.id, newSlideMasterPart.GetIdOfPart(newLayout)) - ) - ); - - return newLayout; - } - - private uint GetNextFreeId() - { - uint newId = 0; - - var presentationPartDoc = _newDocument.PresentationPart.GetXDocument(); - var masterIds = presentationPartDoc - .Root.Descendants(P.sldMasterId) - .Select(f => (uint)f.Attribute(NoNamespace.id)) - .ToList(); - if (masterIds.Count != 0) - newId = Math.Max(newId, masterIds.Max()); - - foreach (var slideMasterData in _slideMasterList) - { - var masterPartDoc = slideMasterData.Part.GetXDocument(); - var layoutIds = masterPartDoc - .Root.Descendants(P.sldLayoutId) - .Select(f => (uint)f.Attribute(NoNamespace.id)) - .ToList(); - if (layoutIds.Count != 0) - newId = Math.Max(newId, layoutIds.Max()); - } - - return newId == 0 ? 2147483648 : newId + 1; - } - } -} diff --git a/Clippit/PowerPoint/IFluentPresentationBuilder.cs b/Clippit/PowerPoint/IFluentPresentationBuilder.cs new file mode 100644 index 00000000..bb4b2463 --- /dev/null +++ b/Clippit/PowerPoint/IFluentPresentationBuilder.cs @@ -0,0 +1,10 @@ +using System; +using DocumentFormat.OpenXml.Packaging; + +namespace Clippit.PowerPoint; + +public interface IFluentPresentationBuilder : IDisposable +{ + public SlideMasterPart AddSlideMasterPart(SlideMasterPart slideMasterPart); + public SlidePart AddSlidePart(SlidePart slidePart); +} diff --git a/Clippit/PowerPoint/PmlDocument.cs b/Clippit/PowerPoint/PmlDocument.cs index 73e8c0b1..35b41992 100644 --- a/Clippit/PowerPoint/PmlDocument.cs +++ b/Clippit/PowerPoint/PmlDocument.cs @@ -3,7 +3,7 @@ namespace Clippit.PowerPoint; -public partial class PmlDocument : OpenXmlPowerToolsDocument +public class PmlDocument : OpenXmlPowerToolsDocument { private const string NotPresentationExceptionMessage = "The document is not a PresentationML document."; diff --git a/Clippit/PowerPoint/PresentationBuilder.BuildPresentation.cs b/Clippit/PowerPoint/PresentationBuilder.BuildPresentation.cs new file mode 100644 index 00000000..1e016af0 --- /dev/null +++ b/Clippit/PowerPoint/PresentationBuilder.BuildPresentation.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Clippit.PowerPoint.Fluent; +using DocumentFormat.OpenXml.Packaging; +using PBT = Clippit.PowerPoint.Fluent.PresentationBuilderTools; + +namespace Clippit.PowerPoint; + +public static partial class PresentationBuilder +{ + public static PmlDocument BuildPresentation(List sources) + { + using var streamDoc = OpenXmlMemoryStreamDocument.CreatePresentationDocument(); + using (var output = streamDoc.GetPresentationDocument(new OpenSettings { AutoSave = false })) + { + BuildPresentation(sources, output); + output.PackageProperties.Modified = DateTime.Now; + } + return streamDoc.GetModifiedPmlDocument(); + } + + private static void BuildPresentation(List sources, PresentationDocument output) + { + using var builder = Create(output); + + var sourceNum = 0; + var openSettings = new OpenSettings { AutoSave = false }; + foreach (var source in sources) + { + using var streamDoc = new OpenXmlMemoryStreamDocument(source.PmlDocument); + using var doc = streamDoc.GetPresentationDocument(openSettings); + try + { + if (source.KeepMaster) + { + foreach (var slideMasterPart in doc.PresentationPart.SlideMasterParts) + { + builder.AddSlideMasterPart(slideMasterPart); + } + } + + var slideIds = PBT.GetSlideIdsInOrder(doc); + var (count, start) = (source.Count, source.Start); + while (count > 0 && start < slideIds.Count) + { + var slidePart = (SlidePart)doc.PresentationPart.GetPartById(slideIds[start]); + builder.AddSlidePart(slidePart); + + start++; + count--; + } + } + catch (PresentationBuilderInternalException dbie) + { + if (dbie.Message.Contains("{0}")) + throw new PresentationBuilderException(string.Format(dbie.Message, sourceNum)); + throw; + } + + sourceNum++; + } + } +} diff --git a/Clippit/PowerPoint/PresentationBuilder.Fluent.cs b/Clippit/PowerPoint/PresentationBuilder.Fluent.cs new file mode 100644 index 00000000..6a05393d --- /dev/null +++ b/Clippit/PowerPoint/PresentationBuilder.Fluent.cs @@ -0,0 +1,18 @@ +using System.IO; +using Clippit.PowerPoint.Fluent; +using DocumentFormat.OpenXml.Packaging; + +namespace Clippit.PowerPoint; + +public static partial class PresentationBuilder +{ + public static IFluentPresentationBuilder Create(PresentationDocument document) + { + return new FluentPresentationBuilder(document); + } + + public static PresentationDocument NewDocument(Stream stream) + { + return PresentationDocument.Create(stream, DocumentFormat.OpenXml.PresentationDocumentType.Presentation, false); + } +} diff --git a/Clippit/PowerPoint/PresentationBuilder.PublishSlides.cs b/Clippit/PowerPoint/PresentationBuilder.PublishSlides.cs new file mode 100644 index 00000000..e0bd7644 --- /dev/null +++ b/Clippit/PowerPoint/PresentationBuilder.PublishSlides.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using Clippit.PowerPoint.Fluent; +using DocumentFormat.OpenXml.Packaging; + +namespace Clippit.PowerPoint; + +public static partial class PresentationBuilder +{ + public static IList PublishSlides(PmlDocument src) + { + using var streamSrcDoc = new OpenXmlMemoryStreamDocument(src); + using var srcDoc = streamSrcDoc.GetPresentationDocument(new OpenSettings { AutoSave = false }); + return PublishSlides(srcDoc, src.FileName).ToList(); + } + + public static IEnumerable PublishSlides(PresentationDocument srcDoc, string fileName) + { + fileName ??= string.Empty; + + var slideNameRegex = SlideNameRegex(); + var slideNumber = 0; + foreach (var memoryStream in PublishSlides(srcDoc)) + { + try + { + var slideName = slideNameRegex.Replace(fileName, $"_{++slideNumber:000}.pptx"); + yield return new PmlDocument(slideName, memoryStream); + } + finally + { + memoryStream.Dispose(); + } + } + } + + private static IEnumerable PublishSlides(PresentationDocument srcDoc) + { + var slidesIds = PresentationBuilderTools.GetSlideIdsInOrder(srcDoc); + foreach (var slideId in slidesIds) + { + var srcSlidePart = (SlidePart)srcDoc.PresentationPart.GetPartById(slideId); + + var memoryStream = new MemoryStream(); + using (var output = NewDocument(memoryStream)) + { + using (var builder = Create(output)) + { + var newSlidePart = builder.AddSlidePart(srcSlidePart); + + // Remove the show attribute from the slide element (if it exists) + var slideDocument = newSlidePart.GetXDocument(); + slideDocument.Root?.Attribute(NoNamespace.show)?.Remove(); + } + + // Set the title of the new presentation to the title of the slide + var title = PresentationBuilderTools.GetSlideTitle(srcSlidePart.GetXElement()); + output.PackageProperties.Title = title; + } + + srcSlidePart.RemoveAnnotations(); + srcSlidePart.UnloadRootElement(); + + yield return memoryStream; + } + } + + [GeneratedRegex(".pptx", RegexOptions.IgnoreCase, "en-US")] + private static partial Regex SlideNameRegex(); +} diff --git a/Clippit/PowerPoint/PresentationBuilder.cs b/Clippit/PowerPoint/PresentationBuilder.cs deleted file mode 100644 index 054663d0..00000000 --- a/Clippit/PowerPoint/PresentationBuilder.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using DocumentFormat.OpenXml.Packaging; - -namespace Clippit.PowerPoint -{ - public class SlideSource(PmlDocument source, int start, int count, bool keepMaster) - { - public PmlDocument PmlDocument { get; set; } = source; - public int Start { get; set; } = start; - public int Count { get; set; } = count; - public bool KeepMaster { get; set; } = keepMaster; - - public SlideSource(PmlDocument source, bool keepMaster) - : this(source, 0, int.MaxValue, keepMaster) { } - - public SlideSource(string fileName, bool keepMaster) - : this(new PmlDocument(fileName), 0, int.MaxValue, keepMaster) { } - - public SlideSource(PmlDocument source, int start, bool keepMaster) - : this(source, start, int.MaxValue, keepMaster) { } - - public SlideSource(string fileName, int start, bool keepMaster) - : this(new PmlDocument(fileName), start, int.MaxValue, keepMaster) { } - - public SlideSource(string fileName, int start, int count, bool keepMaster) - : this(new PmlDocument(fileName), start, count, keepMaster) { } - } - - public static partial class PresentationBuilder - { - public static PmlDocument BuildPresentation(List sources) - { - using var streamDoc = OpenXmlMemoryStreamDocument.CreatePresentationDocument(); - using (var output = streamDoc.GetPresentationDocument(new OpenSettings { AutoSave = false })) - { - BuildPresentation(sources, output); - output.PackageProperties.Modified = DateTime.Now; - } - return streamDoc.GetModifiedPmlDocument(); - } - - public static IList PublishSlides(PmlDocument src) - { - using var streamSrcDoc = new OpenXmlMemoryStreamDocument(src); - using var srcDoc = streamSrcDoc.GetPresentationDocument(new OpenSettings { AutoSave = false }); - return PublishSlides(srcDoc, src.FileName).ToList(); - } - - public static IEnumerable PublishSlides(PresentationDocument srcDoc, string fileName) - { - var slidesCount = srcDoc.PresentationPart.GetXElement().Descendants(P.sldId).Count(); - var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); - for (var slideNumber = 0; slideNumber < slidesCount; slideNumber++) - { - using var streamDoc = OpenXmlMemoryStreamDocument.CreatePresentationDocument(); - using (var output = streamDoc.GetPresentationDocument(new OpenSettings { AutoSave = false })) - { - ExtractSlide(srcDoc, slideNumber, output); - - var slides = output.PresentationPart.GetXElement().Descendants(P.sldId); - var slidePartId = slides.Single().Attribute(R.id)?.Value; - var slidePart = (SlidePart)output.PresentationPart.GetPartById(slidePartId); - var title = PresentationBuilderTools.GetSlideTitle(slidePart.GetXElement()); - - output.PackageProperties.Title = title; - } - - var slideDoc = streamDoc.GetModifiedPmlDocument(); - if (!string.IsNullOrWhiteSpace(fileName)) - { - slideDoc.FileName = $"{fileNameWithoutExtension}_{slideNumber + 1:000}.pptx"; - } - - yield return slideDoc; - } - } - - private static void ExtractSlide(PresentationDocument srcDoc, int slideNumber, PresentationDocument output) - { - using var fluentBuilder = new FluentPresentationBuilder(output); - try - { - fluentBuilder.AppendSlides(srcDoc, slideNumber, 1, true); - } - catch (PresentationBuilderInternalException dbie) - { - if (dbie.Message.Contains("{0}")) - throw new PresentationBuilderException(string.Format(dbie.Message, slideNumber)); - throw; - } - } - - private static void BuildPresentation(List sources, PresentationDocument output) - { - using var fluentBuilder = new FluentPresentationBuilder(output); - - var sourceNum = 0; - var openSettings = new OpenSettings { AutoSave = false }; - foreach (var source in sources) - { - using var streamDoc = new OpenXmlMemoryStreamDocument(source.PmlDocument); - using var doc = streamDoc.GetPresentationDocument(openSettings); - try - { - if (source.KeepMaster) - { - foreach (var slideMasterPart in doc.PresentationPart.SlideMasterParts) - { - fluentBuilder.AppendMaster(doc, slideMasterPart); - } - } - fluentBuilder.AppendSlides(doc, source.Start, source.Count); - } - catch (PresentationBuilderInternalException dbie) - { - if (dbie.Message.Contains("{0}")) - throw new PresentationBuilderException(string.Format(dbie.Message, sourceNum)); - throw; - } - - sourceNum++; - } - } - } -} diff --git a/Clippit/PowerPoint/SlidePartData.cs b/Clippit/PowerPoint/SlidePartData.cs deleted file mode 100644 index 04c385f6..00000000 --- a/Clippit/PowerPoint/SlidePartData.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Text; -using System.Xml.Linq; -using DocumentFormat.OpenXml.Packaging; - -namespace Clippit.PowerPoint -{ - internal abstract class SlidePartData : IComparable> - { - public T Part { get; } - private double ScaleFactor { get; } - private string ShapeXml { get; } - - protected SlidePartData(T part, double scaleFactor) - { - Part = part; - ScaleFactor = scaleFactor; - ShapeXml = GetShapeDescriptor(part); - } - - protected abstract string GetShapeDescriptor(T part); - - public virtual int CompareTo(SlidePartData other) - { - if (ReferenceEquals(this, other)) - return 0; - if (other is null) - return 1; - return string.Compare(ShapeXml, other.ShapeXml, StringComparison.Ordinal); - } - - protected string NormalizeXml(string xml) - { - var doc = XDocument.Parse(xml); - CleanUpAttributes(doc.Root); - ScaleShapes(doc, ScaleFactor); - return doc.ToString(); - } - - private static readonly XNamespace s_relNs = - "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; - private static readonly XName[] s_noiseAttNames = - [ - "smtClean", - "dirty", - "userDrawn", - s_relNs + "id", - s_relNs + "embed", - ]; - - /// - /// Remove OpenXml attributes that may occur on Layout/Master elements but does not affect rendering - /// - private static void CleanUpAttributes(XElement element) - { - foreach (var attName in s_noiseAttNames) - { - element.Attribute(attName)?.Remove(); - } - - foreach (var descendant in element.Descendants()) - { - CleanUpAttributes(descendant); - } - } - - private static readonly Dictionary s_resizableAttributes = new() - { - { A.off, new[] { "x", "y" } }, // - { A.ext, new[] { "cx", "cy" } }, // - { A.chOff, new[] { "x", "y" } }, // - { A.chExt, new[] { "cx", "cy" } }, // - { A.rPr, new[] { "sz" } }, // - { A.defRPr, new[] { "sz" } }, // - { A.pPr, new[] { "defTabSz" } }, // - { A.endParaRPr, new[] { "sz" } }, // - { A.gridCol, new[] { "w" } }, // - { A.tr, new[] { "h" } }, // - }; - - public static void ScaleShapes(XDocument openXmlPart, double scale) - { - if (Math.Abs(scale - 1.0) < 1.0e-5) - return; - - foreach (var element in openXmlPart.Descendants()) - { - if (!s_resizableAttributes.TryGetValue(element.Name, out var attrNames)) - continue; - - foreach (var attrName in attrNames) - { - var attr = element.Attribute(attrName); - if (attr is null) - continue; - if (!long.TryParse(attr.Value, out var num)) - continue; - - var newNum = (long)(num * scale); - attr.SetValue(newNum); - } - } - } - } - - // This class is used to prevent duplication of layouts and handle content modification - internal class SlideLayoutData(SlideLayoutPart slideLayout, double scaleFactor) - : SlidePartData(slideLayout, scaleFactor) - { - protected override string GetShapeDescriptor(SlideLayoutPart slideLayout) - { - var sb = new StringBuilder(); - var cSld = slideLayout.SlideLayout.CommonSlideData; - sb.Append(NormalizeXml(cSld.ShapeTree.OuterXml)); - if (cSld.Background is not null) - sb.Append(NormalizeXml(cSld.Background.OuterXml)); - return sb.ToString(); - } - } - - // This class is used to prevent duplication of themes and handle content modification - internal class ThemeData(ThemePart themePart, double scaleFactor) : SlidePartData(themePart, scaleFactor) - { - protected override string GetShapeDescriptor(ThemePart themePart) => - NormalizeXml(themePart.Theme.ThemeElements.OuterXml); - } - - // This class is used to prevent duplication of masters and handle content modification - internal class SlideMasterData(SlideMasterPart slideMaster, double scaleFactor) - : SlidePartData(slideMaster, scaleFactor) - { - public ThemeData ThemeData { get; } = new ThemeData(slideMaster.ThemePart, scaleFactor); - public List SlideLayoutList { get; } = []; - - protected override string GetShapeDescriptor(SlideMasterPart slideMaster) - { - var sb = new StringBuilder(); - var cSld = slideMaster.SlideMaster.CommonSlideData; - sb.Append(NormalizeXml(cSld.ShapeTree.OuterXml)); - if (cSld.Background is not null) - sb.Append(NormalizeXml(cSld.Background.OuterXml)); - - sb.Append(NormalizeXml(slideMaster.SlideMaster.ColorMap.OuterXml)); - return sb.ToString(); - } - - public override int CompareTo(SlidePartData other) - { - var res = base.CompareTo(other); - if (res == 0 && other is SlideMasterData otherData) - res = ThemeData.CompareTo(otherData.ThemeData); - return res; - } - } -} diff --git a/Clippit/PowerPoint/SlideSource.cs b/Clippit/PowerPoint/SlideSource.cs new file mode 100644 index 00000000..ad26389c --- /dev/null +++ b/Clippit/PowerPoint/SlideSource.cs @@ -0,0 +1,24 @@ +namespace Clippit.PowerPoint; + +public class SlideSource(PmlDocument source, int start, int count, bool keepMaster) +{ + public PmlDocument PmlDocument { get; set; } = source; + public int Start { get; set; } = start; + public int Count { get; set; } = count; + public bool KeepMaster { get; set; } = keepMaster; + + public SlideSource(PmlDocument source, bool keepMaster) + : this(source, 0, int.MaxValue, keepMaster) { } + + public SlideSource(string fileName, bool keepMaster) + : this(new PmlDocument(fileName), 0, int.MaxValue, keepMaster) { } + + public SlideSource(PmlDocument source, int start, bool keepMaster) + : this(source, start, int.MaxValue, keepMaster) { } + + public SlideSource(string fileName, int start, bool keepMaster) + : this(new PmlDocument(fileName), start, int.MaxValue, keepMaster) { } + + public SlideSource(string fileName, int start, int count, bool keepMaster) + : this(new PmlDocument(fileName), start, count, keepMaster) { } +} diff --git a/Clippit/PtOpenXmlDocument.cs b/Clippit/PtOpenXmlDocument.cs index c9123d5b..2f9e9d41 100644 --- a/Clippit/PtOpenXmlDocument.cs +++ b/Clippit/PtOpenXmlDocument.cs @@ -552,31 +552,33 @@ public Type GetDocumentType() } #pragma warning disable IDISP003 - public OpenXmlPowerToolsDocument GetModifiedDocument() + private void ClosePackage() { _docPackage.Close(); _docPackage = null; + } + + public OpenXmlPowerToolsDocument GetModifiedDocument() + { + ClosePackage(); return new OpenXmlPowerToolsDocument(_document?.FileName, _docMemoryStream); } public WmlDocument GetModifiedWmlDocument() { - _docPackage.Close(); - _docPackage = null; + ClosePackage(); return new WmlDocument(_document?.FileName, _docMemoryStream); } public SmlDocument GetModifiedSmlDocument() { - _docPackage.Close(); - _docPackage = null; + ClosePackage(); return new SmlDocument(_document?.FileName, _docMemoryStream); } public PmlDocument GetModifiedPmlDocument() { - _docPackage.Close(); - _docPackage = null; + ClosePackage(); return new PmlDocument(_document?.FileName, _docMemoryStream); } diff --git a/build.fsx b/build.fsx index 758e5805..e7946492 100644 --- a/build.fsx +++ b/build.fsx @@ -37,7 +37,7 @@ pipeline "build" { stage "AssemblyInfo" { run (fun _ -> - let fileName = "Clippit/Properties/AssemblyInfo.Generated.cs" + let fileName = "Clippit/Properties/AssemblyInfo.g.cs" AssemblyInfoFile.createCSharp fileName