From 29974e3571c5e18e419a0bf904df228c95018ccb Mon Sep 17 00:00:00 2001 From: KarlKallman Date: Tue, 4 Aug 2026 14:34:47 +0200 Subject: [PATCH] Added support forAdd support for saving packages as Excel templates (.xltx/.xltm) --- src/EPPlus/Constants/ContentTypes.cs | 6 ++- src/EPPlus/Enums.cs | 17 ++++++ src/EPPlus/ExcelPackage.cs | 78 ++++++++++++++++------------ src/EPPlus/ExcelWorkbook.cs | 38 +++++++++----- src/EPPlus/Packaging/ZipPackage.cs | 4 +- src/EPPlusTest/ExcelPackageTests.cs | 53 ++++++++++++++++++- 6 files changed, 146 insertions(+), 50 deletions(-) diff --git a/src/EPPlus/Constants/ContentTypes.cs b/src/EPPlus/Constants/ContentTypes.cs index eb98ae61af..ab6abd6c55 100644 --- a/src/EPPlus/Constants/ContentTypes.cs +++ b/src/EPPlus/Constants/ContentTypes.cs @@ -19,7 +19,11 @@ internal class ContentTypes internal const string contentTypeWorkbookMacroEnabled = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"; internal const string contentTypeSharedString = @"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"; internal const string contentTypeMetaData = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml"; - + + //Template + internal const string contentTypeTemplateDefault = @"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"; + internal const string contentTypeTemplateMacroEnabled = "application/vnd.ms-excel.template.macroEnabled.main+xml"; + //Theme internal const string contentTypeThemeOverride = "application/vnd.openxmlformats-officedocument.themeOverride+xml"; internal const string contentTypeTheme = @"application/vnd.openxmlformats-officedocument.theme+xml"; diff --git a/src/EPPlus/Enums.cs b/src/EPPlus/Enums.cs index c596fa3de1..d905205b01 100644 --- a/src/EPPlus/Enums.cs +++ b/src/EPPlus/Enums.cs @@ -252,4 +252,21 @@ public enum eWorkbookVisibility /// Visible } + /// + /// Specifies the document format to use when saving a package. + /// + public enum eDocumentFormat + { + /// + /// A standard Excel workbook (.xlsx, or .xlsm if the workbook contains a VBA project). + /// This is the default format. + /// + Workbook, + /// + /// An Excel template (.xltx, or .xltm if the workbook contains a VBA project). + /// When opened in Excel, a template creates a new workbook based on its contents + /// rather than opening the template file itself for editing. + /// + Template + } } diff --git a/src/EPPlus/ExcelPackage.cs b/src/EPPlus/ExcelPackage.cs index 6aec485c67..34610482b7 100644 --- a/src/EPPlus/ExcelPackage.cs +++ b/src/EPPlus/ExcelPackage.cs @@ -28,6 +28,8 @@ Date Author Change #if (!NET35) using OfficeOpenXml.SensitivityLabels; +using System.Linq; + #endif #if (Core) @@ -881,39 +883,6 @@ internal void SavePart(Uri uri, XmlDocument xmlDoc) var xmlWriter = XmlWriter.Create(stream, xmlSettings); xmlDoc.Save(xmlWriter); } - /// - /// Saves the XmlDocument into the package at the specified Uri. - /// - /// The Uri of the component - /// The XmlDocument to save - internal void SaveWorkbook(Uri uri, XmlDocument xmlDoc) - { - Packaging.ZipPackagePart part = _zipPackage.GetPart(uri); - if (Workbook.VbaProject == null) - { - if (part.ContentType != ContentTypes.contentTypeWorkbookDefault) - { - part = _zipPackage.CreatePart(uri, ContentTypes.contentTypeWorkbookDefault, Compression); - } - } - else - { - if (part.ContentType != ContentTypes.contentTypeWorkbookMacroEnabled) - { - var rels = part.GetRelationships(); - _zipPackage.DeletePart(uri); - part = ZipPackage.CreatePart(uri, ContentTypes.contentTypeWorkbookMacroEnabled); - foreach (ZipPackageRelationship rel in rels) - { - ZipPackage.DeleteRelationship(rel.Id); - part.CreateRelationship(rel.TargetUri, rel.TargetMode, rel.RelationshipType); - } - } - } - var stream = part.GetStream(FileMode.Create, FileAccess.Write); - xmlDoc.Save(stream); - } - #endregion #region Dispose @@ -1210,6 +1179,49 @@ public CompressionLevel Compression ZipPackage.Compression = value; } } + + private eDocumentFormat? _format = null; + + /// + /// Gets or sets the file format used when the package is saved. + /// + /// When a package is loaded, this property is derived from the content type of the + /// workbook part, so a file opened as a template (.xltx/.xltm) reports + /// . + /// . + /// + /// + /// Setting this property overrides the derived value, allowing a loaded template to be + /// saved as a workbook or vice versa. Whether the workbook is saved as a macro-enabled + /// file is determined automatically by the presence of a VBA project. + /// + /// + public eDocumentFormat DocumentFormat + { + get + { + if (_format == null) + { + _format = ResolveFormatFromPackage(); + } + return _format.Value; + } + set + { + _format = value; + } + } + + private eDocumentFormat ResolveFormatFromPackage() + { + if (_zipPackage == null) return eDocumentFormat.Workbook; + + var wbPart = _zipPackage.GetPartByContentType(ContentTypes.contentTypeTemplateDefault) + ?? _zipPackage.GetPartByContentType(ContentTypes.contentTypeTemplateMacroEnabled); + + return wbPart != null ? eDocumentFormat.Template : eDocumentFormat.Workbook; + } + CompatibilitySettings _compatibility = null; /// /// Compatibility settings for older versions of EPPlus. diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs index 282f47ff65..07730063c1 100644 --- a/src/EPPlus/ExcelWorkbook.cs +++ b/src/EPPlus/ExcelWorkbook.cs @@ -1559,21 +1559,15 @@ internal void Save() // Workbook Save SetXmlNodeBool("d:calcPr/@fullPrecision", FullPrecision, false); if(_workbookCreatedInEPPlus == false) EnsureCalculationFeatures(); //Ensure that the calculation features are included in the file to make sure that new functions are supported. - - if (_vba == null && !_package.ZipPackage.PartExists(new Uri(ExcelVbaProject.PartUri, UriKind.Relative))) + + bool isMacroEnabled = !(_vba == null && + !_package.ZipPackage.PartExists(new Uri(ExcelVbaProject.PartUri, UriKind.Relative))); + + var targetContentType = GetWorkbookContentType(_package.DocumentFormat, isMacroEnabled); + + if (Part.ContentType != targetContentType) { - if (Part.ContentType != ContentTypes.contentTypeWorkbookDefault && - Part.ContentType != ContentTypes.contentTypeWorkbookMacroEnabled) - { - Part.ContentType = ContentTypes.contentTypeWorkbookDefault; - } - } - else - { - if (Part.ContentType != ContentTypes.contentTypeWorkbookMacroEnabled) - { - Part.ContentType = ContentTypes.contentTypeWorkbookMacroEnabled; - } + Part.ContentType = targetContentType; } UpdateDefinedNamesXml(); @@ -1705,6 +1699,22 @@ internal void Save() // Workbook Save } } + internal string GetWorkbookContentType(eDocumentFormat format, bool isMacroEnabled) + { + if (format == eDocumentFormat.Template) + { + return isMacroEnabled + ? ContentTypes.contentTypeTemplateMacroEnabled + : ContentTypes.contentTypeTemplateDefault; + } + else + { + return isMacroEnabled + ? ContentTypes.contentTypeWorkbookMacroEnabled + : ContentTypes.contentTypeWorkbookDefault; + } + } + private void HandleLicense() { switch (ExcelPackage.License.LicenseType) diff --git a/src/EPPlus/Packaging/ZipPackage.cs b/src/EPPlus/Packaging/ZipPackage.cs index 3e2fa34c4f..6cdcf66a36 100644 --- a/src/EPPlus/Packaging/ZipPackage.cs +++ b/src/EPPlus/Packaging/ZipPackage.cs @@ -534,7 +534,9 @@ internal void Save(Stream stream) part.ContentType == ContentTypes.contentTypeRichDataValueStructure || part.ContentType == ContentTypes.contentTypeRichDataValue || part.ContentType == ContentTypes.contentTypeWorkbookDefault || - part.ContentType == ContentTypes.contentTypeWorkbookMacroEnabled) + part.ContentType == ContentTypes.contentTypeWorkbookMacroEnabled || + part.ContentType == ContentTypes.contentTypeTemplateDefault || // nytt + part.ContentType == ContentTypes.contentTypeTemplateMacroEnabled) // nytt) { saveAfterParts.Add(part); } diff --git a/src/EPPlusTest/ExcelPackageTests.cs b/src/EPPlusTest/ExcelPackageTests.cs index 2d26ba33cb..627455cf77 100644 --- a/src/EPPlusTest/ExcelPackageTests.cs +++ b/src/EPPlusTest/ExcelPackageTests.cs @@ -26,6 +26,8 @@ Date Author Change ******************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *******************************************************************************/ +using EPPlusTest.Drawing.Chart.Styling; +using FakeItEasy.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml; using System; @@ -37,7 +39,7 @@ Date Author Change namespace EPPlusTest { [TestClass] - public class ExcelPackageTests + public class ExcelPackageTests : TestBase { [TestMethod, Ignore] public void ConstructorWithStringPath() @@ -87,5 +89,54 @@ public void ShouldEncryptAndDecryptPackage(EncryptionAlgorithm algorithm) } } } + + [TestMethod] + public void SaveAsTemplate_Debug() + { + using (var package = new ExcelPackage()) + { + var ws = package.Workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = "Test"; + + package.DocumentFormat = eDocumentFormat.Template; + SaveAndCleanup(package); + } + } + + [TestMethod] + public void SaveAsTemplate_WithVba_SetsTemplateMacroEnabledContentType() + { + using (var package = new ExcelPackage()) + { + var ws = package.Workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = "Test"; + + package.Workbook.CreateVBAProject(); + package.DocumentFormat = eDocumentFormat.Template; + SaveAndCleanup(package); + } + } + + [TestMethod] + public void ReadAndConvertToTemplate() + { + using (var package = OpenTemplatePackage("ExcelTemplateTest.xltx")) + { + Assert.AreEqual(eDocumentFormat.Template, package.DocumentFormat); + package.DocumentFormat = eDocumentFormat.Workbook; + SaveAndCleanup(package); + } + } + + [TestMethod] + public void ReadAndConvertWorkbookToTemplate() + { + using (var package = OpenTemplatePackage("ExcelTemplateTest.xlsx")) + { + Assert.AreEqual(eDocumentFormat.Workbook, package.DocumentFormat); + package.DocumentFormat = eDocumentFormat.Template; + SaveAndCleanup(package); + } + } } }