diff --git a/src/EPPlus/Configuration/EPPlusSaveOption.cs b/src/EPPlus/Configuration/EPPlusSaveOption.cs
new file mode 100644
index 0000000000..bd5fa9d147
--- /dev/null
+++ b/src/EPPlus/Configuration/EPPlusSaveOption.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+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;
+
+ ///
+ /// 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/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/ExcelPackage.cs b/src/EPPlus/ExcelPackage.cs
index 6aec485c67..6969e983be 100644
--- a/src/EPPlus/ExcelPackage.cs
+++ b/src/EPPlus/ExcelPackage.cs
@@ -881,39 +881,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
@@ -1096,6 +1063,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
///
@@ -1165,7 +1147,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
///
@@ -1210,6 +1237,19 @@ public CompressionLevel Compression
ZipPackage.Compression = value;
}
}
+
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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;
///
/// Compatibility settings for older versions of EPPlus.
diff --git a/src/EPPlus/ExcelPackageAsync.cs b/src/EPPlus/ExcelPackageAsync.cs
index 16975717e6..4e00905693 100644
--- a/src/EPPlus/ExcelPackageAsync.cs
+++ b/src/EPPlus/ExcelPackageAsync.cs
@@ -16,7 +16,10 @@ 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;
using System.Threading.Tasks;
@@ -335,6 +338,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 +382,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 +412,23 @@ 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
+ /// 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 +458,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 282f47ff65..72f01dbc18 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.SaveAsTemplate, 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(bool saveAsTemplate, bool isMacroEnabled)
+ {
+ if (saveAsTemplate)
+ {
+ 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..3208383084 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 ||
+ part.ContentType == ContentTypes.contentTypeTemplateMacroEnabled)
{
saveAfterParts.Add(part);
}
diff --git a/src/EPPlusTest/ExcelPackageTests.cs b/src/EPPlusTest/ExcelPackageTests.cs
index 2d26ba33cb..83310d14fb 100644
--- a/src/EPPlusTest/ExcelPackageTests.cs
+++ b/src/EPPlusTest/ExcelPackageTests.cs
@@ -26,18 +26,22 @@ 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 OfficeOpenXml.Constants;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
+using System.Threading.Tasks;
namespace EPPlusTest
{
[TestClass]
- public class ExcelPackageTests
+ public class ExcelPackageTests : TestBase
{
[TestMethod, Ignore]
public void ConstructorWithStringPath()
@@ -87,5 +91,226 @@ public void ShouldEncryptAndDecryptPackage(EncryptionAlgorithm algorithm)
}
}
}
+
+ [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.SaveAsTemplate = true;
+ SaveWorkbook("SaveTemplate_Vba.xltm", package);
+ }
+
+ AssertSavedWorkbookContentType("SaveTemplate_Vba.xltm",
+ ContentTypes.contentTypeTemplateMacroEnabled);
+ }
+
+ [TestMethod]
+ public void ReadTemplate_ResaveWithoutOptions_ConvertsToWorkbook()
+ {
+ using (var package = OpenTemplatePackage("ExcelTemplateTest.xltx"))
+ {
+ SaveWorkbook("ReadTemplate_Resaved.xlsx", package);
+ }
+
+ AssertSavedWorkbookContentType("ReadTemplate_Resaved.xlsx",
+ ContentTypes.contentTypeWorkbookDefault);
+ }
+
+ [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 void AssertSavedWorkbookContentType(string fileName, string expectedContentType)
+ {
+ using (var reopened = OpenPackage(fileName))
+ {
+ AssertWorkbookContentType(expectedContentType, reopened);
+ }
+ }
+
+ private static void AssertWorkbookContentType(string expected, ExcelPackage package)
+ {
+ Assert.AreEqual(expected, package.Workbook.Part.ContentType);
+ }
}
}