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); + } } }