From 44bf4eb8aa41aeb049c50c2af18df2e5c5821373 Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Sun, 6 Oct 2024 10:36:16 +0200 Subject: [PATCH] feat: public API for fluent presentation builder --- ...FluentPresentationBuilder.Deduplication.cs | 118 ++ .../Fluent/FluentPresentationBuilder.cs | 1284 +++++++++++++++ Clippit/PowerPoint/Fluent/SlidePartData.cs | 155 ++ .../PowerPoint/FluentPresentationBuilder.cs | 1392 ----------------- .../PowerPoint/IFluentPresentationBuilder.cs | 10 + Clippit/PowerPoint/PresentationBuilder.cs | 17 +- Clippit/PowerPoint/SlidePartData.cs | 157 -- 7 files changed, 1579 insertions(+), 1554 deletions(-) create mode 100644 Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Deduplication.cs create mode 100644 Clippit/PowerPoint/Fluent/FluentPresentationBuilder.cs create mode 100644 Clippit/PowerPoint/Fluent/SlidePartData.cs delete mode 100644 Clippit/PowerPoint/FluentPresentationBuilder.cs create mode 100644 Clippit/PowerPoint/IFluentPresentationBuilder.cs delete mode 100644 Clippit/PowerPoint/SlidePartData.cs diff --git a/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Deduplication.cs b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Deduplication.cs new file mode 100644 index 00000000..7f5d99af --- /dev/null +++ b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.Deduplication.cs @@ -0,0 +1,118 @@ +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 List _slideMasterList = []; + 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 + ) + { + 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; + } + + // 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 + ); + + 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; + } +} diff --git a/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.cs b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.cs new file mode 100644 index 00000000..f44d3367 --- /dev/null +++ b/Clippit/PowerPoint/Fluent/FluentPresentationBuilder.cs @@ -0,0 +1,1284 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +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.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)); + + var mainPart = _newDocument.PresentationPart.GetXDocument(); + mainPart.Declaration.Standalone = "yes"; + mainPart.Declaration.Encoding = "UTF-8"; + + _isDocumentInitialized = false; + InitializeCaches(); + } + + 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 AddSlideMaster(SlideMasterPart slideMasterPart) + { + var sourceDocument = (PresentationDocument)slideMasterPart.OpenXmlPackage; + EnsureDocumentInitialized(sourceDocument); + + var scaleFactor = GetScaleFactor(sourceDocument); + foreach (var slideLayoutPart in slideMasterPart.SlideLayoutParts) + { + _ = GetOrAddSlideLayoutPart(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]); + } + + var slideLayoutData = GetOrAddSlideLayoutPart(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)) + ) + ); + + newId++; + start++; + count--; + } + } + + // 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); + 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; + } + + 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/Fluent/SlidePartData.cs b/Clippit/PowerPoint/Fluent/SlidePartData.cs new file mode 100644 index 00000000..5550851b --- /dev/null +++ b/Clippit/PowerPoint/Fluent/SlidePartData.cs @@ -0,0 +1,155 @@ +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 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/FluentPresentationBuilder.cs b/Clippit/PowerPoint/FluentPresentationBuilder.cs deleted file mode 100644 index 41981371..00000000 --- a/Clippit/PowerPoint/FluentPresentationBuilder.cs +++ /dev/null @@ -1,1392 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -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]); - } - - 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)) - ) - ); - - 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..061fdd31 --- /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 void AddSlideMaster(SlideMasterPart slideMasterPart); + public void AppendSlides(PresentationDocument sourceDocument, int start, int count); +} diff --git a/Clippit/PowerPoint/PresentationBuilder.cs b/Clippit/PowerPoint/PresentationBuilder.cs index 6066b1d9..9128d525 100644 --- a/Clippit/PowerPoint/PresentationBuilder.cs +++ b/Clippit/PowerPoint/PresentationBuilder.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Clippit.Excel; +using Clippit.PowerPoint.Fluent; using DocumentFormat.OpenXml.Packaging; namespace Clippit.PowerPoint @@ -67,6 +69,11 @@ public SlideSource(string fileName, int start, int count, bool keepMaster) public static partial class PresentationBuilder { + public static IFluentPresentationBuilder Create(PresentationDocument document) + { + return new FluentPresentationBuilder(document); + } + public static PmlDocument BuildPresentation(List sources) { using var streamDoc = OpenXmlMemoryStreamDocument.CreatePresentationDocument(); @@ -116,10 +123,10 @@ public static IEnumerable PublishSlides(PresentationDocument srcDoc private static void ExtractSlide(PresentationDocument srcDoc, int slideNumber, PresentationDocument output) { - using var fluentBuilder = new FluentPresentationBuilder(output); + using var builder = new FluentPresentationBuilder(output); try { - fluentBuilder.AppendSlides(srcDoc, slideNumber, 1, true); + builder.AppendSlides(srcDoc, slideNumber, 1, true); } catch (PresentationBuilderInternalException dbie) { @@ -131,7 +138,7 @@ private static void ExtractSlide(PresentationDocument srcDoc, int slideNumber, P private static void BuildPresentation(List sources, PresentationDocument output) { - using var fluentBuilder = new FluentPresentationBuilder(output); + using var builder = Create(output); var sourceNum = 0; var openSettings = new OpenSettings { AutoSave = false }; @@ -145,10 +152,10 @@ private static void BuildPresentation(List sources, PresentationDoc { foreach (var slideMasterPart in doc.PresentationPart.SlideMasterParts) { - fluentBuilder.AppendMaster(doc, slideMasterPart); + builder.AddSlideMaster(slideMasterPart); } } - fluentBuilder.AppendSlides(doc, source.Start, source.Count); + builder.AppendSlides(doc, source.Start, source.Count); } catch (PresentationBuilderInternalException dbie) { diff --git a/Clippit/PowerPoint/SlidePartData.cs b/Clippit/PowerPoint/SlidePartData.cs deleted file mode 100644 index 486211dc..00000000 --- a/Clippit/PowerPoint/SlidePartData.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -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; - } - } -}