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