From 29974e3571c5e18e419a0bf904df228c95018ccb Mon Sep 17 00:00:00 2001 From: KarlKallman Date: Tue, 4 Aug 2026 14:34:47 +0200 Subject: [PATCH 1/4] 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); + } + } } } From 5c1f53c007dd6e952787c2f5bbd91c61c0f20cba Mon Sep 17 00:00:00 2001 From: KarlKallman Date: Wed, 5 Aug 2026 14:03:53 +0200 Subject: [PATCH 2/4] Add EPPlusSaveOption with SaveAsTemplate to save packages as Excel templates --- src/EPPlus/Configuration/EPPlusSaveOption.cs | 17 ++ src/EPPlus/Enums.cs | 29 +-- src/EPPlus/ExcelPackage.cs | 110 ++++++---- src/EPPlus/ExcelPackageAsync.cs | 81 ++++++++ src/EPPlus/ExcelWorkbook.cs | 6 +- src/EPPlusTest/ExcelPackageTests.cs | 200 ++++++++++++++++++- 6 files changed, 379 insertions(+), 64 deletions(-) create mode 100644 src/EPPlus/Configuration/EPPlusSaveOption.cs diff --git a/src/EPPlus/Configuration/EPPlusSaveOption.cs b/src/EPPlus/Configuration/EPPlusSaveOption.cs new file mode 100644 index 0000000000..b814fb41b2 --- /dev/null +++ b/src/EPPlus/Configuration/EPPlusSaveOption.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Configuration +{ + /// + /// + /// + public class EPPlusSaveOption + { + public string Password { get; set; } = null; + + public bool SaveAsTemplate { get; set; } = false; + } +} diff --git a/src/EPPlus/Enums.cs b/src/EPPlus/Enums.cs index d905205b01..13bbe09780 100644 --- a/src/EPPlus/Enums.cs +++ b/src/EPPlus/Enums.cs @@ -255,18 +255,19 @@ public enum eWorkbookVisibility /// /// 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 - } + /// + //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 34610482b7..ff37e9b825 100644 --- a/src/EPPlus/ExcelPackage.cs +++ b/src/EPPlus/ExcelPackage.cs @@ -29,6 +29,8 @@ Date Author Change #if (!NET35) using OfficeOpenXml.SensitivityLabels; using System.Linq; +using System.Security.AccessControl; + #endif @@ -1065,6 +1067,21 @@ public void Save(string password) Save(); } /// + /// Saves all the components back into the package. + /// This method recursively calls the Save method on all sub-components. + /// The package is closed after it has been saved + /// + /// An action used to configure the save options, for example saving the package as an Excel template. + public void Save(Action options) + { + var o = new EPPlusSaveOption(); + options.Invoke(o); + + Encryption.Password = o.Password; + SaveAsTemplate = o.SaveAsTemplate; + Save(); + } + /// /// Saves the workbook to a new file /// The package is closed after it has been saved /// @@ -1134,7 +1151,52 @@ public void SaveAs(Stream OutputStream, string password) Encryption.Password = password; SaveAs(OutputStream); } + /// + /// Saves the workbook to a new file + /// The package is closed after it has been saved + /// + /// The file location + /// An action used to configure the save options, for example saving the package as an Excel template. + public void SaveAs(string filePath, Action options) + { + var o = new EPPlusSaveOption(); + options.Invoke(o); + + Encryption.Password = o.Password; + SaveAsTemplate = o.SaveAsTemplate; + + SaveAs(new FileInfo(filePath)); + } + /// + /// Saves the workbook to a new file + /// The package is closed after it has been saved + /// + /// The file + /// An action used to configure the save options, for example saving the package as an Excel template. + public void SaveAs(FileInfo file, Action options) + { + var o = new EPPlusSaveOption(); + options.Invoke(o); + File = file; + Encryption.Password = o.Password; + SaveAsTemplate = o.SaveAsTemplate; + Save(); + } + /// + /// Copies the Package to the Outstream + /// The package is closed after it has been saved + /// + /// The stream to copy the package to + /// An action used to configure the save options, for example saving the package as an Excel template. + public void SaveAs(Stream OutputStream, Action options) + { + var o = new EPPlusSaveOption(); + options.Invoke(o); + Encryption.Password = o.Password; + SaveAsTemplate = o.SaveAsTemplate; + SaveAs(OutputStream); + } /// /// The output file. Null if no file is used /// @@ -1180,47 +1242,17 @@ public CompressionLevel Compression } } - 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. - /// + /// If true, the package is saved as an Excel template (.xltx, or .xltm if the workbook + /// contains a VBA project). If false, it is saved as a standard Excel workbook + /// (.xlsx/.xlsm). Default is false. /// - 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; - } + /// + /// This value is not derived from a loaded package, so a template that is loaded and saved + /// again becomes a standard workbook unless this is set to true. This mirrors how Excel + /// handles templates. + /// + public bool SaveAsTemplate { get; set; } = false; CompatibilitySettings _compatibility = null; /// diff --git a/src/EPPlus/ExcelPackageAsync.cs b/src/EPPlus/ExcelPackageAsync.cs index 16975717e6..109834b394 100644 --- a/src/EPPlus/ExcelPackageAsync.cs +++ b/src/EPPlus/ExcelPackageAsync.cs @@ -16,6 +16,14 @@ Date Author Change using System; using System.IO; using OfficeOpenXml.Utils.FileUtils; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance.FinancialDayCount; +using OfficeOpenXml.Configuration; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + + + + #if !NET35 && !NET40 using System.Threading; @@ -335,6 +343,24 @@ public async Task SaveAsync(string password, CancellationToken cancellationToken await SaveAsync(cancellationToken).ConfigureAwait(false); } + /// + /// Saves all the components back into the package asynchronously. + /// This method recursively calls the Save method on all sub-components. + /// The package is closed after it has been saved. + /// Supply a password to encrypt the workbook with. + /// + /// An action used to configure the save options, for example saving the package as an Excel template. + /// The cancellation token. + /// A task that represents the asynchronous save operation. + public async Task SaveAsync(Action options, CancellationToken cancellationToken = default) + { + var o = new EPPlusSaveOption(); + options.Invoke(o); + Encryption.Password = o.Password; + SaveAsTemplate = o.SaveAsTemplate; + await SaveAsync(cancellationToken).ConfigureAwait(false); + } + #endregion #region SaveAsAsync @@ -361,6 +387,22 @@ public async Task SaveAsAsync(string filePath, CancellationToken cancellationTok await SaveAsAsync(new FileInfo(filePath), cancellationToken); } + /// + /// Saves the workbook to a new file + /// The package is closed after it has been saved + /// + /// The file location + /// An action used to configure the save options, for example saving the package as an Excel template. + /// The cancellation token + public async Task SaveAsAsync(string filePath, Action options, CancellationToken cancellationToken = default) + { + var o = new EPPlusSaveOption(); + options.Invoke(o); + Encryption.Password = o.Password; + SaveAsTemplate = o.SaveAsTemplate; + await SaveAsAsync(new FileInfo(filePath), cancellationToken); + } + /// /// Saves the workbook to a new file /// The package is closed after it has been saved @@ -375,6 +417,24 @@ public async Task SaveAsAsync(FileInfo file, string password, CancellationToken Encryption.Password = password; await SaveAsync(cancellationToken).ConfigureAwait(false); } + + /// + /// Saves the workbook to a new file + /// The package is closed after it has been saved + /// + /// The file + /// This parameter overrides the Encryption.Password. + /// An action used to configure the save options, for example saving the package as an Excel template. + /// The cancellation token + public async Task SaveAsAsync(FileInfo file, Action options, CancellationToken cancellationToken = default) + { + var o = new EPPlusSaveOption(); + options.Invoke(o); + File = file; + Encryption.Password = o.Password; + SaveAsTemplate = o.SaveAsTemplate; + await SaveAsync(cancellationToken).ConfigureAwait(false); + } /// /// Saves the workbook to a new file /// The package is closed after it has been saved @@ -404,6 +464,27 @@ public async Task SaveAsAsync(Stream OutputStream, CancellationToken cancellatio await StreamUtil.CopyStreamAsync(_stream, OutputStream, cancellationToken).ConfigureAwait(false); } } + /// + /// Copies the Package to the Outstream + /// The package is closed after it has been saved + /// + /// The stream to copy the package to + /// An action used to configure the save options, for example saving the package as an Excel template. + /// The cancellation token + public async Task SaveAsAsync(Stream OutputStream, Action options, CancellationToken cancellationToken = default) + { + var o = new EPPlusSaveOption(); + options.Invoke(o); + SaveAsTemplate = o.SaveAsTemplate; + Encryption.Password = o.Password; + File = null; + await SaveAsync(cancellationToken).ConfigureAwait(false); + + if (OutputStream != _stream) + { + await StreamUtil.CopyStreamAsync(_stream, OutputStream, cancellationToken).ConfigureAwait(false); + } + } /// /// Copies the Package to the Outstream diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs index 07730063c1..72f01dbc18 100644 --- a/src/EPPlus/ExcelWorkbook.cs +++ b/src/EPPlus/ExcelWorkbook.cs @@ -1563,7 +1563,7 @@ internal void Save() // Workbook Save bool isMacroEnabled = !(_vba == null && !_package.ZipPackage.PartExists(new Uri(ExcelVbaProject.PartUri, UriKind.Relative))); - var targetContentType = GetWorkbookContentType(_package.DocumentFormat, isMacroEnabled); + var targetContentType = GetWorkbookContentType(_package.SaveAsTemplate, isMacroEnabled); if (Part.ContentType != targetContentType) { @@ -1699,9 +1699,9 @@ internal void Save() // Workbook Save } } - internal string GetWorkbookContentType(eDocumentFormat format, bool isMacroEnabled) + internal string GetWorkbookContentType(bool saveAsTemplate, bool isMacroEnabled) { - if (format == eDocumentFormat.Template) + if (saveAsTemplate) { return isMacroEnabled ? ContentTypes.contentTypeTemplateMacroEnabled diff --git a/src/EPPlusTest/ExcelPackageTests.cs b/src/EPPlusTest/ExcelPackageTests.cs index 627455cf77..eac79708dc 100644 --- a/src/EPPlusTest/ExcelPackageTests.cs +++ b/src/EPPlusTest/ExcelPackageTests.cs @@ -30,11 +30,13 @@ Date Author Change using FakeItEasy.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml; +using OfficeOpenXml.Constants; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; namespace EPPlusTest { @@ -98,7 +100,7 @@ public void SaveAsTemplate_Debug() var ws = package.Workbook.Worksheets.Add("Sheet1"); ws.Cells["A1"].Value = "Test"; - package.DocumentFormat = eDocumentFormat.Template; + package.SaveAsTemplate = true; SaveAndCleanup(package); } } @@ -111,8 +113,8 @@ public void SaveAsTemplate_WithVba_SetsTemplateMacroEnabledContentType() var ws = package.Workbook.Worksheets.Add("Sheet1"); ws.Cells["A1"].Value = "Test"; - package.Workbook.CreateVBAProject(); - package.DocumentFormat = eDocumentFormat.Template; + package.Workbook.CreateVBAProject(); + package.SaveAsTemplate = true; SaveAndCleanup(package); } } @@ -121,9 +123,7 @@ public void SaveAsTemplate_WithVba_SetsTemplateMacroEnabledContentType() public void ReadAndConvertToTemplate() { using (var package = OpenTemplatePackage("ExcelTemplateTest.xltx")) - { - Assert.AreEqual(eDocumentFormat.Template, package.DocumentFormat); - package.DocumentFormat = eDocumentFormat.Workbook; + { SaveAndCleanup(package); } } @@ -133,10 +133,194 @@ public void ReadAndConvertWorkbookToTemplate() { using (var package = OpenTemplatePackage("ExcelTemplateTest.xlsx")) { - Assert.AreEqual(eDocumentFormat.Workbook, package.DocumentFormat); - package.DocumentFormat = eDocumentFormat.Template; + package.SaveAsTemplate = true; SaveAndCleanup(package); } } + + [TestMethod] + public void SaveAs_FileInfo_WithOptions_WritesTemplateContentType() + { + var file = new FileInfo(Path.Combine(_worksheetPath, "SaveTemplate_FileInfo.xltx")); + + using (var package = new ExcelPackage()) + { + package.Workbook.Worksheets.Add("Sheet1"); + package.SaveAs(file, o => o.SaveAsTemplate = true); + } + + using (var reopened = new ExcelPackage(file)) + { + AssertWorkbookContentType(ContentTypes.contentTypeTemplateDefault, reopened); + } + } + + [TestMethod] + public void SaveAs_Stream_WithOptions_WritesTemplateContentType() + { + using (var ms = new MemoryStream()) + { + using (var package = new ExcelPackage()) + { + package.Workbook.Worksheets.Add("Sheet1"); + package.SaveAs(ms, o => o.SaveAsTemplate = true); + } + + ms.Position = 0; + using (var reopened = new ExcelPackage(ms)) + { + AssertWorkbookContentType(ContentTypes.contentTypeTemplateDefault, reopened); + } + } + } + + [TestMethod] + public void SaveAs_FilePath_WithOptions_WritesTemplateContentType() + { + var path = Path.Combine(_worksheetPath, "SaveTemplate_Path.xltx"); + + using (var package = new ExcelPackage()) + { + package.Workbook.Worksheets.Add("Sheet1"); + package.SaveAs(path, o => o.SaveAsTemplate = true); + } + + using (var reopened = new ExcelPackage(new FileInfo(path))) + { + AssertWorkbookContentType(ContentTypes.contentTypeTemplateDefault, reopened); + } + } + + [TestMethod] + public void Save_WithOptions_WritesTemplateContentType() + { + var file = new FileInfo(Path.Combine(_worksheetPath, "SaveTemplate_Save.xltx")); + if (file.Exists) file.Delete(); + + using (var package = new ExcelPackage(file)) + { + package.Workbook.Worksheets.Add("TestSheet"); + package.Save(o => o.SaveAsTemplate = true); + } + + using (var reopened = new ExcelPackage(file)) + { + AssertWorkbookContentType(ContentTypes.contentTypeTemplateDefault, reopened); + } + } + + [TestMethod] + public void SaveAs_WithOptions_DefaultsToWorkbookContentType() + { + using (var ms = new MemoryStream()) + { + using (var package = new ExcelPackage()) + { + package.Workbook.Worksheets.Add("Sheet1"); + package.SaveAs(ms, o => { }); + } + + ms.Position = 0; + using (var reopened = new ExcelPackage(ms)) + { + AssertWorkbookContentType(ContentTypes.contentTypeWorkbookDefault, reopened); + } + } + } + + [TestMethod] + public void ResaveLoadedTemplate_ConvertsToWorkbook() + { + using (var ms = new MemoryStream()) + { + using (var package = OpenTemplatePackage("ExcelTemplateTest.xltx")) + { + package.SaveAs(ms); + } + + ms.Position = 0; + using (var reopened = new ExcelPackage(ms)) + { + AssertWorkbookContentType(ContentTypes.contentTypeWorkbookDefault, reopened); + } + } + } + + [TestMethod] + public async Task SaveAsync_WithOptions_WritesTemplateContentType() + { + var file = new FileInfo(Path.Combine(_worksheetPath, "SaveTemplate_SaveAsync.xltx")); + if (file.Exists) file.Delete(); + + using (var package = new ExcelPackage(file)) + { + package.Workbook.Worksheets.Add("Sheet1"); + await package.SaveAsync(o => o.SaveAsTemplate = true); + } + + using (var reopened = new ExcelPackage(file)) + { + AssertWorkbookContentType(ContentTypes.contentTypeTemplateDefault, reopened); + } + } + + [TestMethod] + public async Task SaveAsAsync_FileInfo_WithOptions_WritesTemplateContentType() + { + var file = new FileInfo(Path.Combine(_worksheetPath, "SaveTemplate_AsyncFileInfo.xltx")); + if (file.Exists) file.Delete(); + + using (var package = new ExcelPackage()) + { + package.Workbook.Worksheets.Add("Sheet1"); + await package.SaveAsAsync(file, o => o.SaveAsTemplate = true); + } + + using (var reopened = new ExcelPackage(file)) + { + AssertWorkbookContentType(ContentTypes.contentTypeTemplateDefault, reopened); + } + } + + [TestMethod] + public async Task SaveAsAsync_FilePath_WithOptions_WritesTemplateContentType() + { + var path = Path.Combine(_worksheetPath, "SaveTemplate_AsyncPath.xltx"); + + using (var package = new ExcelPackage()) + { + package.Workbook.Worksheets.Add("Sheet1"); + await package.SaveAsAsync(path, o => o.SaveAsTemplate = true); + } + + using (var reopened = new ExcelPackage(new FileInfo(path))) + { + AssertWorkbookContentType(ContentTypes.contentTypeTemplateDefault, reopened); + } + } + + [TestMethod] + public async Task SaveAsAsync_Stream_WithOptions_WritesTemplateContentType() + { + using (var ms = new MemoryStream()) + { + using (var package = new ExcelPackage()) + { + package.Workbook.Worksheets.Add("Sheet1"); + await package.SaveAsAsync(ms, o => o.SaveAsTemplate = true); + } + + ms.Position = 0; + using (var reopened = new ExcelPackage(ms)) + { + AssertWorkbookContentType(ContentTypes.contentTypeTemplateDefault, reopened); + } + } + } + + private static void AssertWorkbookContentType(string expected, ExcelPackage package) + { + Assert.AreEqual(expected, package.Workbook.Part.ContentType); + } } } From 60ba65f5cb6c2e93e48187bd6fcad7dcb548b53b Mon Sep 17 00:00:00 2001 From: KarlKallman Date: Wed, 5 Aug 2026 15:10:27 +0200 Subject: [PATCH 3/4] Cleanup --- src/EPPlus/Configuration/EPPlusSaveOption.cs | 14 ++++++++++++-- src/EPPlus/Enums.cs | 18 ------------------ src/EPPlus/ExcelPackage.cs | 4 ---- src/EPPlus/ExcelPackageAsync.cs | 8 +------- src/EPPlus/Packaging/ZipPackage.cs | 4 ++-- 5 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/EPPlus/Configuration/EPPlusSaveOption.cs b/src/EPPlus/Configuration/EPPlusSaveOption.cs index b814fb41b2..bd5fa9d147 100644 --- a/src/EPPlus/Configuration/EPPlusSaveOption.cs +++ b/src/EPPlus/Configuration/EPPlusSaveOption.cs @@ -6,12 +6,22 @@ namespace OfficeOpenXml.Configuration { /// - /// + /// Options used when saving an . + /// Supplied via an action to the Save and SaveAs methods. /// public class EPPlusSaveOption { + /// + /// The password to encrypt the workbook with. + /// This overrides the Encryption.Password on the package. + /// If null, the password set on the package is used. + /// public string Password { get; set; } = null; - public bool SaveAsTemplate { get; set; } = false; + /// + /// If true, the package is saved as an Excel template (.xltx, or .xltm if the workbook + /// contains a VBA project) instead of a standard workbook. Default is false. + /// + public bool SaveAsTemplate { get; set; } = false; } } diff --git a/src/EPPlus/Enums.cs b/src/EPPlus/Enums.cs index 13bbe09780..c596fa3de1 100644 --- a/src/EPPlus/Enums.cs +++ b/src/EPPlus/Enums.cs @@ -252,22 +252,4 @@ 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 ff37e9b825..6969e983be 100644 --- a/src/EPPlus/ExcelPackage.cs +++ b/src/EPPlus/ExcelPackage.cs @@ -28,10 +28,6 @@ Date Author Change #if (!NET35) using OfficeOpenXml.SensitivityLabels; -using System.Linq; -using System.Security.AccessControl; - - #endif #if (Core) diff --git a/src/EPPlus/ExcelPackageAsync.cs b/src/EPPlus/ExcelPackageAsync.cs index 109834b394..4e00905693 100644 --- a/src/EPPlus/ExcelPackageAsync.cs +++ b/src/EPPlus/ExcelPackageAsync.cs @@ -20,11 +20,6 @@ Date Author Change using OfficeOpenXml.Configuration; using System.Runtime.CompilerServices; using System.Security.Cryptography; - - - - - #if !NET35 && !NET40 using System.Threading; using System.Threading.Tasks; @@ -422,8 +417,7 @@ public async Task SaveAsAsync(FileInfo file, string password, CancellationToken /// Saves the workbook to a new file /// The package is closed after it has been saved /// - /// The file - /// This parameter overrides the Encryption.Password. + /// The file /// An action used to configure the save options, for example saving the package as an Excel template. /// The cancellation token public async Task SaveAsAsync(FileInfo file, Action options, CancellationToken cancellationToken = default) diff --git a/src/EPPlus/Packaging/ZipPackage.cs b/src/EPPlus/Packaging/ZipPackage.cs index 6cdcf66a36..3208383084 100644 --- a/src/EPPlus/Packaging/ZipPackage.cs +++ b/src/EPPlus/Packaging/ZipPackage.cs @@ -535,8 +535,8 @@ internal void Save(Stream stream) part.ContentType == ContentTypes.contentTypeRichDataValue || part.ContentType == ContentTypes.contentTypeWorkbookDefault || part.ContentType == ContentTypes.contentTypeWorkbookMacroEnabled || - part.ContentType == ContentTypes.contentTypeTemplateDefault || // nytt - part.ContentType == ContentTypes.contentTypeTemplateMacroEnabled) // nytt) + part.ContentType == ContentTypes.contentTypeTemplateDefault || + part.ContentType == ContentTypes.contentTypeTemplateMacroEnabled) { saveAfterParts.Add(part); } From d109021a995838ba0a0a61c549ea758e6d58c4aa Mon Sep 17 00:00:00 2001 From: KarlKallman Date: Thu, 6 Aug 2026 09:38:06 +0200 Subject: [PATCH 4/4] Removed and edited tests --- src/EPPlusTest/ExcelPackageTests.cs | 42 +++++++++++------------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/EPPlusTest/ExcelPackageTests.cs b/src/EPPlusTest/ExcelPackageTests.cs index eac79708dc..83310d14fb 100644 --- a/src/EPPlusTest/ExcelPackageTests.cs +++ b/src/EPPlusTest/ExcelPackageTests.cs @@ -92,19 +92,6 @@ 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.SaveAsTemplate = true; - SaveAndCleanup(package); - } - } - [TestMethod] public void SaveAsTemplate_WithVba_SetsTemplateMacroEnabledContentType() { @@ -115,27 +102,23 @@ public void SaveAsTemplate_WithVba_SetsTemplateMacroEnabledContentType() package.Workbook.CreateVBAProject(); package.SaveAsTemplate = true; - SaveAndCleanup(package); + SaveWorkbook("SaveTemplate_Vba.xltm", package); } - } - [TestMethod] - public void ReadAndConvertToTemplate() - { - using (var package = OpenTemplatePackage("ExcelTemplateTest.xltx")) - { - SaveAndCleanup(package); - } + AssertSavedWorkbookContentType("SaveTemplate_Vba.xltm", + ContentTypes.contentTypeTemplateMacroEnabled); } [TestMethod] - public void ReadAndConvertWorkbookToTemplate() + public void ReadTemplate_ResaveWithoutOptions_ConvertsToWorkbook() { - using (var package = OpenTemplatePackage("ExcelTemplateTest.xlsx")) + using (var package = OpenTemplatePackage("ExcelTemplateTest.xltx")) { - package.SaveAsTemplate = true; - SaveAndCleanup(package); + SaveWorkbook("ReadTemplate_Resaved.xlsx", package); } + + AssertSavedWorkbookContentType("ReadTemplate_Resaved.xlsx", + ContentTypes.contentTypeWorkbookDefault); } [TestMethod] @@ -317,6 +300,13 @@ public async Task SaveAsAsync_Stream_WithOptions_WritesTemplateContentType() } } } + private void AssertSavedWorkbookContentType(string fileName, string expectedContentType) + { + using (var reopened = OpenPackage(fileName)) + { + AssertWorkbookContentType(expectedContentType, reopened); + } + } private static void AssertWorkbookContentType(string expected, ExcelPackage package) {