using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Xml.Linq; namespace FastExcel { /// /// Fast Excel /// public partial class FastExcel : IDisposable { /// /// Output excel file /// public FileInfo ExcelFile { get { if (_excelFile == null) { throw new ApplicationException("ExcelFile was not provided"); } return _excelFile; } } /// /// The template excel file /// public FileInfo TemplateFile{ get { if (_templateFile == null) { throw new ApplicationException("TemplateFile was not provided"); } return _templateFile; } } private Stream ExcelFileStream { get; set; } private Stream TemplateFileStream { get; set; } /// /// Is the excel file read only /// public bool ReadOnly { get; private set; } internal SharedStrings SharedStrings { get; set; } internal ZipArchive Archive { get; set; } private bool UpdateExisting { get; set; } private bool _filesChecked; private readonly FileInfo _excelFile; private readonly FileInfo _templateFile; /// /// Maximum sheet number, obtained when a sheet is added /// internal int MaxSheetNumber { get; set; } /// /// A list of worksheet indexs to delete /// private List DeleteWorksheets { get; set; } /// /// A list of worksheet indexs to insert /// private List AddWorksheets { get; set; } /// /// Update an existing excel file /// /// location of an existing excel file /// is the file read only public FastExcel(FileInfo excelFile, bool readOnly = false) : this(null, excelFile, true, readOnly) { } /// /// Create a new excel file from a template /// /// template location /// location of where a new excel file will be saved to public FastExcel(FileInfo templateFile, FileInfo excelFile) : this(templateFile, excelFile, false, false) { } /// /// /// /// /// /// /// private FastExcel(FileInfo templateFile, FileInfo excelFile, bool updateExisting, bool readOnly = false) { if (updateExisting) { if (!excelFile.Exists) { var exceptionMessage = $"Input file '{excelFile.FullName}' does not exist"; throw new FileNotFoundException(exceptionMessage); } } else { if (excelFile.Exists) { var exceptionMessage = $"Output file '{excelFile.FullName}' already exists"; throw new Exception(exceptionMessage); } if (!templateFile.Exists) { var exceptionMessage = $"Template file '{templateFile.FullName}' was not found"; throw new FileNotFoundException(exceptionMessage); } } _templateFile = templateFile; _excelFile = excelFile; TemplateFileStream = templateFile != null ? new FileStream(templateFile.FullName, FileMode.Open, FileAccess.Read) : null; ExcelFileStream = updateExisting ? new FileStream(excelFile.FullName, FileMode.Open, readOnly ? FileAccess.Read : FileAccess.ReadWrite) : new FileStream(excelFile.FullName, FileMode.OpenOrCreate, FileAccess.ReadWrite); UpdateExisting = updateExisting; ReadOnly = readOnly; CheckFiles(); } /// /// Update an existing excel file stream /// /// public FastExcel(Stream excelStream) : this(null, excelStream, true) { } /// /// Create a new excel file from a template /// /// Input Template Stream /// Output Excel Stream /// /// public FastExcel(Stream templateStream, Stream excelStream, bool updateExisting = false, bool readOnly = false) { if (templateStream is FileStream templatefileStream) { _templateFile = new FileInfo(templatefileStream.Name); } if (excelStream is FileStream excelFileStream) { _excelFile = new FileInfo(excelFileStream.Name); } TemplateFileStream = templateStream; ExcelFileStream = excelStream; UpdateExisting = updateExisting; ReadOnly = readOnly; CheckFiles(); } internal void PrepareArchive(bool openSharedStrings = true) { if (Archive == null) { if (ReadOnly) { Archive = new ZipArchive(ExcelFileStream, ZipArchiveMode.Read); } else { Archive = new ZipArchive(ExcelFileStream, ZipArchiveMode.Update); } } // Get Strings file if (SharedStrings == null && openSharedStrings) { SharedStrings = new SharedStrings(Archive); } } /// /// Ensure files are ready for use /// internal void CheckFiles() { if (_filesChecked) { return; } if (UpdateExisting) { if (ExcelFileStream?.Length == 0) { throw new Exception("No input file name was supplied"); } } else { if (TemplateFileStream == null) { throw new Exception("No Template file was supplied"); } if (ExcelFileStream == null) { throw new Exception("No Ouput file name was supplied"); } else if (ExcelFileStream.Length > 0) { var exceptionMessage = $"Output file already exists"; throw new Exception(exceptionMessage); } } _filesChecked = true; } /// /// Update xl/_rels/workbook.xml.rels file /// private void UpdateRelations(bool ensureStrings) { if (!(ensureStrings || (DeleteWorksheets != null && DeleteWorksheets.Any()) || (AddWorksheets != null && AddWorksheets.Any()))) { // Nothing to update return; } using (Stream stream = Archive.GetEntry("xl/_rels/workbook.xml.rels").Open()) { XDocument document = XDocument.Load(stream); if (document == null) { //TODO error } bool update = false; List relationshipElements = document.Descendants().Where(d => d.Name.LocalName == "Relationship").ToList(); int id = relationshipElements.Count; if (ensureStrings) { //Ensure SharedStrings XElement relationshipElement = (from element in relationshipElements from attribute in element.Attributes() where attribute.Name == "Target" && attribute.Value.Equals("sharedStrings.xml", StringComparison.OrdinalIgnoreCase) select element).FirstOrDefault(); if (relationshipElement == null) { relationshipElement = new XElement(document.Root.GetDefaultNamespace() + "Relationship"); relationshipElement.Add(new XAttribute("Target", "sharedStrings.xml")); relationshipElement.Add(new XAttribute("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings")); relationshipElement.Add(new XAttribute("Id", string.Format("rId{0}", ++id))); document.Root.Add(relationshipElement); update = true; } } // Remove all references to sheets from this file as they are not requried if ((DeleteWorksheets != null && DeleteWorksheets.Any()) || (AddWorksheets != null && AddWorksheets.Any())) { XElement[] worksheetElements = (from element in relationshipElements from attribute in element.Attributes() where attribute.Name == "Type" && attribute.Value == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" select element).ToArray(); for (int i = worksheetElements.Length - 1; i > 0; i--) { worksheetElements[i].Remove(); update = true; } } if (update) { // Set the stream to the start stream.Position = 0; // Clear the stream stream.SetLength(0); // Open the stream so we can override all content of the sheet StreamWriter streamWriter = new StreamWriter(stream, Encoding.UTF8); document.Save(streamWriter); streamWriter.Flush(); } } } /// /// Update xl/workbook.xml file /// private string[] UpdateWorkbook() { if (!(DeleteWorksheets != null && DeleteWorksheets.Any() || (AddWorksheets != null && AddWorksheets.Any()))) { // Nothing to update return null; } List sheetNames = new List(); using (Stream stream = Archive.GetEntry("xl/workbook.xml").Open()) { XDocument document = XDocument.Load(stream); if (document == null) { throw new Exception("Unable to load workbook.xml"); } bool update = false; RenameAndRebildWorksheetProperties((from sheet in document.Descendants() where sheet.Name.LocalName == "sheet" select sheet).ToArray()); if (update) { // Re number sheet ids XNamespace r = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; int id = 1; foreach (XElement sheetElement in (from sheet in document.Descendants() where sheet.Name.LocalName == "sheet" select sheet)) { sheetElement.SetAttributeValue(r + "id", string.Format("rId{0}", id++)); sheetNames.Add(sheetElement.Attribute("name").Value); } //Set the stream to the start stream.Position = 0; // Clear the stream stream.SetLength(0); // Open the stream so we can override all content of the sheet StreamWriter streamWriter = new StreamWriter(stream); document.Save(streamWriter); streamWriter.Flush(); } } return sheetNames.ToArray(); } /// /// If sheets have been added or deleted, sheets need to be renamed /// private void RenameAndRebildWorksheetProperties(XElement[] sheets) { if (!((DeleteWorksheets != null && DeleteWorksheets.Any()) || (AddWorksheets != null && AddWorksheets.Any()))) { // Nothing to update return; } XNamespace r = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; List sheetProperties = (from sheet in sheets select new WorksheetProperties () { SheetId = int.Parse(sheet.Attribute("sheetId").Value), Name = sheet.Attribute("name").Value, CurrentIndex = int.Parse(sheet.Attribute(r + "id").Value) }).ToList(); // Remove deleted worksheets to sheetProperties if (DeleteWorksheets != null && DeleteWorksheets.Any()) { foreach (var item in DeleteWorksheets) { WorksheetProperties sheetToDelete = (from sp in sheetProperties where sp.SheetId == item select sp).FirstOrDefault(); if (sheetToDelete != null) { sheetProperties.Remove(sheetToDelete); } } } // Add new worksheets to sheetProperties if (AddWorksheets != null && AddWorksheets.Any()) { // Add the sheets in reverse, this will add them correctly with less work foreach (var item in AddWorksheets.Reverse()) { WorksheetProperties previousSheet = (from sp in sheetProperties where sp.SheetId == item.InsertAfterSheetId select sp).FirstOrDefault(); if (previousSheet == null) { throw new Exception(string.Format("Sheet name {0} cannot be added because the insertAfterSheetNumber or insertAfterSheetName is now invalid", item.Name)); } WorksheetProperties newWorksheet = new WorksheetProperties() { SheetId = item.SheetId, Name = item.Name, CurrentIndex = 0 // TODO Something?? }; sheetProperties.Insert(sheetProperties.IndexOf(previousSheet), newWorksheet); } } int index = 1; foreach (WorksheetProperties worksheet in sheetProperties) { if (worksheet.CurrentIndex != index) { ZipArchiveEntry entry = Archive.GetEntry(Worksheet.GetFileName(worksheet.CurrentIndex)); if (entry == null) { // TODO better message throw new Exception("Worksheets could not be rebuilt"); } } index++; } } /// /// Update [Content_Types].xml file /// private void UpdateContentTypes(bool ensureStrings) { if (!(ensureStrings || (DeleteWorksheets != null && DeleteWorksheets.Any()) || (AddWorksheets != null && AddWorksheets.Any()))) { // Nothing to update return; } using (Stream stream = Archive.GetEntry("[Content_Types].xml").Open()) { XDocument document = XDocument.Load(stream); if (document == null) { //TODO error } bool update = false; List overrideElements = document.Descendants().Where(d => d.Name.LocalName == "Override").ToList(); //Ensure SharedStrings if (ensureStrings) { XElement overrideElement = (from element in overrideElements from attribute in element.Attributes() where attribute.Name == "PartName" && attribute.Value.Equals("/xl/sharedStrings.xml", StringComparison.OrdinalIgnoreCase) select element).FirstOrDefault(); if (overrideElement == null) { overrideElement = new XElement(document.Root.GetDefaultNamespace() + "Override"); overrideElement.Add(new XAttribute("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml")); overrideElement.Add(new XAttribute("PartName", "/xl/sharedStrings.xml")); document.Root.Add(overrideElement); update = true; } } if (DeleteWorksheets != null && DeleteWorksheets.Any()) { foreach (var item in DeleteWorksheets) { // the file name is different for each xml file string fileName = string.Format("/xl/worksheets/sheet{0}.xml", item); XElement overrideElement = (from element in overrideElements from attribute in element.Attributes() where attribute.Name == "PartName" && attribute.Value == fileName select element).FirstOrDefault(); if (overrideElement != null) { overrideElement.Remove(); update = true; } } } if (AddWorksheets != null && AddWorksheets.Any()) { foreach (var item in AddWorksheets) { // the file name is different for each xml file string fileName = string.Format("/xl/worksheets/sheet{0}.xml", item.SheetId); XElement overrideElement = new XElement(document.Root.GetDefaultNamespace() + "Override"); overrideElement.Add(new XAttribute("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml")); overrideElement.Add(new XAttribute("PartName", fileName)); document.Root.Add(overrideElement); update = true; } } if (update) { // Set the stream to the start stream.Position = 0; // Clear the stream stream.SetLength(0); // Open the stream so we can override all content of the sheet StreamWriter streamWriter = new StreamWriter(stream, Encoding.UTF8); document.Save(streamWriter); streamWriter.Flush(); } } } /// /// Retrieves the index for given worksheet name /// /// /// 1 based index of sheet or 0 if not found public int GetWorksheetIndexFromName(string name) { return (from worksheet in Worksheets where worksheet.Name == name select worksheet.Index).FirstOrDefault(); } /// /// Update docProps/app.xml file /// private void UpdateDocPropsApp(string[] sheetNames) { /* if (sheetNames == null || !sheetNames.Any()) { // Nothing to update return; } using (Stream stream = this.Archive.GetEntry("docProps/app.xml ").Open()) { XDocument document = XDocument.Load(stream); if (document == null) { throw new Exception("Unable to load app.xml"); } // Update TilesOfParts // Update HeadingPairs if (this.AddWorksheets != null && this.AddWorksheets.Any()) { // Add the sheets in reverse, this will add them correctly with less work foreach (var item in this.AddWorksheets.Reverse()) { XElement previousSheetElement = (from sheet in document.Descendants() where sheet.Name.LocalName == "sheet" from attribute in sheet.Attributes() where attribute.Name == "sheetId" && attribute.Value == item.InsertAfterIndex.ToString() select sheet).FirstOrDefault(); if (previousSheetElement == null) { throw new Exception(string.Format("Sheet name {0} cannot be added because the insertAfterSheetNumber or insertAfterSheetName is now invalid", item.Name)); } XElement newSheetElement = new XElement(document.Root.GetDefaultNamespace() + "sheet"); newSheetElement.Add(new XAttribute("name", item.Name)); newSheetElement.Add(new XAttribute("sheetId", item.Index)); previousSheetElement.AddAfterSelf(newSheetElement); update = true; } } if (update) { // Re number sheet ids XNamespace r = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; int id = 1; foreach (XElement sheetElement in (from sheet in document.Descendants() where sheet.Name.LocalName == "sheet" select sheet)) { sheetElement.SetAttributeValue(r + "id", string.Format("rId{0}", id++)); } //Set the stream to the start stream.Position = 0; // Clear the stream stream.SetLength(0); // Open the stream so we can override all content of the sheet StreamWriter streamWriter = new StreamWriter(stream); document.Save(streamWriter); streamWriter.Flush(); } }*/ } /// /// Saves any pending changes to the Excel stream and adds/updates associated files if needed /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Main disposal function /// protected virtual void Dispose(bool disposing) { if (Archive == null) { return; } if (Archive.Mode != ZipArchiveMode.Read) { bool ensureSharedStrings = false; // Update or create xl/sharedStrings.xml file if (SharedStrings != null) { ensureSharedStrings = SharedStrings.PendingChanges; SharedStrings.Write(); } // Update xl/_rels/workbook.xml.rels file UpdateRelations(ensureSharedStrings); // Update xl/workbook.xml file string[] sheetNames = UpdateWorkbook(); // Update [Content_Types].xml file UpdateContentTypes(ensureSharedStrings); // Update docProps/app.xml file UpdateDocPropsApp(sheetNames); } Archive.Dispose(); } } }