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