using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Utils;
using Sarand.Net.Standard.Proc.Xml;
using Sarand.Net.Standard.Proc.Zip;
namespace Sarand.Net.Standard.Proc.Excel
{
//=======================================================================
/// Style properties retrieved directly from spreadsheet xml data.
public class SaMsExcelStyle
{
public SaMsExcelStyle () { }
public int NumFmtId { get; set; }
public int FontId { get; set; }
public int FillId { get; set; }
public int BorderId { get; set; }
public int XfId { get; set; }
public bool QuotePrefix { get; set; }
public bool PivotButton { get; set; }
public bool ApplyNumberFormat { get; set; }
public bool ApplyFont { get; set; }
public bool ApplyFill { get; set; }
public bool ApplyBorder { get; set; }
public bool ApplyAlignment { get; set; }
public bool ApplyProtection { get; set; }
public XlHAlign AlignHorizontal { get; set; }
public XlVAlign AlignVertical { get; set; }
public String NumFmtCode { get; set; } = "";
};
//=======================================================================
/// Excel spreadsheet read directly from xml data rather than via COM/Interop.
public class SaMsSpreadsheet_Direct : SaMsSpreadsheet
{
//-------------------------------------------------------------------
// Data members.
internal class Workbook
{
internal SaMsZipArchive WorkbookArchive { get; set; }
internal SaMsXmlDocument WorkbookDoc { get; set; }
internal SaMsXmlDocument SharedStringsDoc { get; set; }
internal SaMsXmlDocument StylesDoc { get; set; }
internal uint WorksheetCount { get; set; }
internal uint ActiveSheetPos { get; set; }
internal bool IsValid () { return (WorkbookDoc != null && WorkbookDoc.GetRootElement() != null); }
}
private Workbook workbook;
private class Worksheet
{
internal uint WorksheetPos { get; set; }
internal uint WorksheetId { get; set; }
internal String WorksheetArchivePath { get; set; }
internal String WorksheetName { get; set; }
internal SaMsXmlDocument WorksheetDoc { get; set; }
internal uint ActiveColPos { get; set; }
internal uint ActiveRowPos { get; set; }
internal class NodesData
{
internal SaMsXmlElement Cols { get; set; }
internal SaMsXmlElement ActiveCol { get; set; }
internal SaMsXmlElement Rows { get; set; }
internal SaMsXmlElement ActiveRow { get; set; }
}
internal NodesData Nodes { get; } = new NodesData();
bool IsValid () { return (WorksheetDoc != null && WorksheetDoc.GetRootElement() != null); }
}
private Worksheet worksheet;
//-------------------------------------------------------------------
// Construction.
public SaMsSpreadsheet_Direct () : base()
{
}
protected override void Dispose (bool disposing)
{
if (disposing)
{
CloseWorkbook();
}
base.Dispose(disposing);
}
//-------------------------------------------------------------------
// Properties.
//-------------------------------------------------------------------
// W o r k b o o k
//-------------------------------------------------------------------
public override bool IsWorkbookValid ()
{
return (workbook != null && workbook.IsValid());
}
public override bool CreateWorkbook ()
{
CloseWorkbook();
ClearErrorState();
// Create new workbook.
workbook = new Workbook();
workbook.WorkbookArchive = new SaMsZipArchive();
workbook.SharedStringsDoc = new SaMsXmlDocument();
workbook.StylesDoc = new SaMsXmlDocument();
return SetErrorState("Creating workbook not supported");
}
public override bool OpenWorkbook (String filePath)
{
CloseWorkbook();
ClearErrorState();
if (!File.Exists(filePath))
return SetErrorState("Workbook not found");
List validExtensions = new List() { /*".xls",*/ ".xlsx", ".xlsb", ".zip" };
String extension = Path.GetExtension(filePath);
if (!validExtensions.ContainsCI(Path.GetExtension(filePath)))
return SetErrorState("Invalid document type (expected xlsx/xlsb)");
try
{
// Open workbook document.
workbook = new Workbook();
workbook.WorkbookArchive = new SaMsZipArchive();
if (workbook.WorkbookArchive.OpenArchive(filePath))
{
ExtractWorkbookElementsFromArchive();
}
else return SetErrorState("Failed to open document: " + workbook.WorkbookArchive.ErrorStateMessage);
}
catch (Exception exc) { SetErrorState(ExceptionUtil.GetErrorStateMessage(exc)); }
return !HasErrorState();
}
public bool OpenWorkbook (byte[] workbookData)
{
CloseWorkbook();
ClearErrorState();
try
{
using (MemoryStream workbookStream = new MemoryStream(workbookData))
{
OpenWorkbook(workbookStream);
}
}
catch (Exception exc) { SetErrorState(ExceptionUtil.GetErrorStateMessage(exc)); }
return !HasErrorState();
}
public bool OpenWorkbook (Stream workbookStream)
{
CloseWorkbook();
ClearErrorState();
try
{
// From start of stream.
workbookStream.Seek(0, SeekOrigin.Begin);
// Open workbook document.
workbook = new Workbook();
workbook.WorkbookArchive = new SaMsZipArchive();
if (workbook.WorkbookArchive.OpenArchive(workbookStream))
{
ExtractWorkbookElementsFromArchive();
}
else return SetErrorState("Failed to open document: " + workbook.WorkbookArchive.ErrorStateMessage);
}
catch (Exception exc) { SetErrorState(ExceptionUtil.GetErrorStateMessage(exc)); }
return !HasErrorState();
}
private bool ExtractWorkbookElementsFromArchive ()
{
ClearErrorState();
try
{
// Extract the workbook data.
using (MemoryStream workbookFile = workbook.WorkbookArchive.ExtractFileToStream("xl/workbook.xml"))
{
if (workbookFile == null) return SetErrorState("Failed to load workbook data");
workbook.WorkbookDoc = new SaMsXmlDocument();
if (workbook.WorkbookDoc.LoadXml(workbookFile))
{
// Workbook successfully opened.
workbook.ActiveSheetPos = 1;
// Shared strings:
using (MemoryStream sharedStringsFile = workbook.WorkbookArchive.ExtractFileToStream("xl/sharedStrings.xml"))
{
if (sharedStringsFile != null)
{
workbook.SharedStringsDoc = new SaMsXmlDocument();
workbook.SharedStringsDoc.LoadXml(sharedStringsFile);
}
}
// Styles:
using (MemoryStream stylesFile = workbook.WorkbookArchive.ExtractFileToStream("xl/styles.xml"))
{
if (stylesFile != null)
{
workbook.StylesDoc = new SaMsXmlDocument();
workbook.StylesDoc.LoadXml(stylesFile);
}
}
// Set worksheet count and open first worksheet.
PopulateWorksheetCount();
OpenWorksheet(1);
}
else return SetErrorState("Failed to read workbook data");
}
}
catch (Exception exc) { SetErrorState(ExceptionUtil.GetErrorStateMessage(exc)); }
return !HasErrorState();
}
public override bool SaveDocument (String filePath, int fileFormat, bool bAutomaticallyOverwrite)
{
ClearErrorState();
if (fileFormat == 0)
fileFormat = (int)XlFileFormat.xlOpenXMLWorkbook;
if (fileFormat != (int)XlFileFormat.xlOpenXMLWorkbook)
SetErrorState("Unsupported file format: " + ((XlFileFormat)fileFormat).ToString());
if (!HasErrorState() && !workbook.IsValid())
SetErrorState("Workbook is not open");
SetErrorState("Saving workbook not supported");
return !HasErrorState();
}
public override bool CloseWorkbook ()
{
ClearErrorState();
CloseWorksheet();
// Clear workbook.
if (workbook != null)
{
try
{
if (workbook.StylesDoc != null) workbook.StylesDoc.Dispose(); workbook.StylesDoc = null;
if (workbook.SharedStringsDoc != null) workbook.SharedStringsDoc.Dispose(); workbook.SharedStringsDoc = null;
if (workbook.WorkbookDoc != null) workbook.WorkbookDoc.Dispose(); workbook.WorkbookDoc = null;
if (workbook.WorkbookArchive != null) workbook.WorkbookArchive.Dispose(); workbook.WorkbookArchive = null;
}
catch (Exception exc) { SetErrorState(ExceptionUtil.GetErrorStateMessage(exc)); }
workbook = null;
}
return !HasErrorState();
}
//-------------------------------------------------------------------
// W o r k s h e e t
//-------------------------------------------------------------------
public override bool IsWorksheetValid ()
{
return (worksheet != null && worksheet.WorksheetDoc != null);
}
public override uint WorksheetCount { get { return workbook.WorksheetCount; } }
private void PopulateWorksheetCount ()
{
if (!IsWorkbookValid()) return;
try
{
// Count worksheets in workbook.
SaMsXmlElement nodeWorkbook = workbook.WorkbookDoc.GetRootElement();
SaMsXmlElement nodeSheets = nodeWorkbook.GetFirstChildElement("sheets");
workbook.WorksheetCount = (uint)nodeSheets.GetChildElements("sheet").Count;
}
catch (Exception exc) { HandleException("GetWorksheetCount", exc); }
}
public override bool SetWorksheetCount (int worksheetCount)
{
ClearErrorState();
return SetErrorState("Modifying workbook not supported");
}
public override bool AddWorksheet(String name)
{
return SetErrorState("Modifying workbook not supported");
}
public override bool RemoveWorksheet(uint worksheetPos)
{
return SetErrorState("Modifying workbook not supported");
}
public override bool OpenWorksheet (uint worksheetPos)
{
ClearErrorState();
if (!workbook.IsValid()) return SetErrorState("Workbook is not open");
if (worksheetPos > WorksheetCount) return SetErrorState("Invalid sheet specified");
if (worksheetPos <= 0) worksheetPos = Math.Max(1, workbook.ActiveSheetPos);
CloseWorksheet();
try
{
worksheet = new Worksheet();
// Locate worksheet in workbook by 0-index.
SaMsXmlElement workbookElement = workbook.WorkbookDoc.GetRootElement();
SaMsXmlElement sheetsElement = workbookElement.GetFirstChildElement("sheets");
long sheetCount = 0;
foreach (SaMsXmlElement sheetElement in sheetsElement.GetChildElements("sheet"))
{
sheetCount++;
if (sheetCount == worksheetPos)
{
// Extract the worksheet data.
uint sheetId = (uint)StringUtil.ToInt(sheetElement.GetAttributeValue("sheetId"));
String worksheetArchivePath = "xl/worksheets/sheet" + worksheetPos.ToString() + ".xml";
using (MemoryStream worksheetFile = workbook.WorkbookArchive.ExtractFileToStream(worksheetArchivePath))
{
if (worksheetFile == null) return SetErrorState("Failed to load worksheet data");
worksheet.WorksheetDoc = new SaMsXmlDocument();
if (worksheet.WorksheetDoc.LoadXml(worksheetFile))
{
workbook.ActiveSheetPos = worksheetPos;
worksheet.WorksheetPos = worksheetPos;
worksheet.WorksheetId = sheetId;
worksheet.WorksheetArchivePath = worksheetArchivePath;
worksheet.WorksheetName = sheetElement.GetAttributeValue("name");
worksheet.ActiveColPos = 1;
worksheet.ActiveRowPos = 1;
SaMsXmlElement nodeWorksheet = worksheet.WorksheetDoc.GetRootElement();
worksheet.Nodes.Cols = nodeWorksheet.GetFirstChildElement("cols");
worksheet.Nodes.ActiveCol = worksheet.Nodes.Cols.GetFirstChildElement("col");
worksheet.Nodes.Rows = nodeWorksheet.GetFirstChildElement("sheetData");
worksheet.Nodes.ActiveRow = worksheet.Nodes.Rows.GetFirstChildElement("row");
workbook.ActiveSheetPos = worksheetPos;
}
else return SetErrorState("Failed to read data for worksheet " + worksheetPos.ToString());
}
break;
}
}
if (worksheet.WorksheetDoc == null) return SetErrorState("Unable to load worksheet " + worksheetPos.ToString());
}
catch (Exception exc) { HandleException("OpenWorksheet", exc); }
return !HasErrorState();
}
public override uint WorksheetPos { get { return workbook.ActiveSheetPos; } }
public override String WorksheetName
{
get { return ""; }
set { }
}
public override bool SetWorksheetAttr (SaMsExcelItemAttr attr)
{
return SetErrorState("Not supported");
}
public override bool CloseWorksheet ()
{
ClearErrorState();
if (worksheet != null)
{
try
{
if (worksheet.WorksheetDoc != null) worksheet.WorksheetDoc.Dispose(); worksheet.WorksheetDoc = null;
if (worksheet.Nodes.Cols != null) worksheet.Nodes.Cols.Dispose(); worksheet.Nodes.Cols = null;
if (worksheet.Nodes.ActiveCol != null) worksheet.Nodes.ActiveCol.Dispose(); worksheet.Nodes.ActiveCol = null;
if (worksheet.Nodes.Rows != null) worksheet.Nodes.Rows.Dispose(); worksheet.Nodes.Rows = null;
if (worksheet.Nodes.ActiveRow != null) worksheet.Nodes.ActiveRow.Dispose(); worksheet.Nodes.ActiveRow = null;
}
catch (Exception exc) { SetErrorState(ExceptionUtil.GetErrorStateMessage(exc)); }
worksheet = null;
}
return !HasErrorState();
}
//-------------------------------------------------------------------
// Worksheet Page Setup.
public override bool SetPageSetup (SaMsExcelPageSetup_Config pageSetupConfig)
{
return SetErrorState("Not supported");
}
public override bool SetPageBreak (uint col, uint row)
{
return SetErrorState("Not supported");
}
//-------------------------------------------------------------------
// C o l u m n s
//-------------------------------------------------------------------
public override uint GetActiveColPos ()
{
return worksheet.ActiveColPos;
}
private SaMsXmlElement GetColElement (uint colPos)
{
ClearErrorState();
try
{
if (worksheet.ActiveColPos == colPos) return worksheet.Nodes.ActiveCol;
int colCount = 0;
foreach (SaMsXmlElement colElement in worksheet.Nodes.Cols.GetChildElements("Col"))
{
if (++colCount == colPos)
{
if (worksheet.Nodes.ActiveCol != null) worksheet.Nodes.ActiveCol.Dispose();
worksheet.ActiveColPos = colPos;
worksheet.Nodes.ActiveCol = colElement;
return colElement;
}
}
}
catch (Exception exc) { HandleException("GetColElement", exc); }
return new SaMsXmlElement(null);
}
public override double GetColWidth (uint colPos, SaExcelLogicalUnit unit = SaExcelLogicalUnit.Point)
{
ClearErrorState();
try
{
// Retrieve width (in points) from column.
SaMsXmlNode nodeCol = GetColElement(colPos);
double width = nodeCol.GetAttributeValue("width").ToDouble();
// Adjust for units.
if (unit == SaExcelLogicalUnit.Pixel) return PointsToPixels(width, false);
if (unit == SaExcelLogicalUnit.Cm) return PointsToCms(width);
if (unit == SaExcelLogicalUnit.Inch) return PointsToInches(width);
return width;
}
catch (Exception exc) { HandleException("GetColWidth", exc); }
return 0.0;
}
public override bool SetColWidth (uint colPos, double fWidth, SaExcelLogicalUnit unit = SaExcelLogicalUnit.Point)
{
return SetErrorState("Not supported");
}
//-------------------------------------------------------------------
// R o w s
//-------------------------------------------------------------------
public override uint GetActiveRowPos ()
{
return worksheet.ActiveRowPos;
}
SaMsXmlElement GetRowElement (uint rowPos)
{
ClearErrorState();
try
{
if (worksheet.ActiveRowPos == rowPos) return worksheet.Nodes.ActiveRow;
foreach (SaMsXmlElement rowElement in worksheet.Nodes.Rows.GetChildElements("row"))
{
if (rowElement.GetAttributeValue("r").ToLong() == rowPos)
{
if (worksheet.Nodes.ActiveRow != null) worksheet.Nodes.ActiveRow.Dispose();
worksheet.ActiveRowPos = rowPos;
worksheet.Nodes.ActiveRow = rowElement;
return rowElement;
}
}
}
catch (Exception exc) { HandleException("GetRowElement", exc); }
return new SaMsXmlElement(null);
}
public override double GetRowHeight (uint rowPos, SaExcelLogicalUnit unit = SaExcelLogicalUnit.Point)
{
ClearErrorState();
try
{
// Retrieve height (in points) from row.
SaMsXmlElement rowElement = GetRowElement(rowPos);
double height = rowElement.GetAttributeValue("ht").ToDouble();
// Adjust for units.
if (unit == SaExcelLogicalUnit.Pixel) return PointsToPixels(height, true);
if (unit == SaExcelLogicalUnit.Cm) return PointsToCms(height);
if (unit == SaExcelLogicalUnit.Inch) return PointsToInches(height);
return height;
}
catch (Exception exc) { HandleException("GetRowHeight", exc); }
return 0.0;
}
public override bool SetRowHeight (uint rowPos, double fHeight, SaExcelLogicalUnit unit = SaExcelLogicalUnit.Point)
{
return SetErrorState("Not supported");
}
//-------------------------------------------------------------------
// R o w s
//-------------------------------------------------------------------
public override List GetRowDataValues (uint rowPos, int maxColumns = 0)
{
ClearErrorState();
List values = new List();
try
{
SaMsXmlElement rowElement = GetRowElement(rowPos);
foreach (SaMsXmlElement cellElement in rowElement.GetChildElements("c"))
{
// CellId may be needed in case cells are skipped in the data.
// Doesn't appear to be the case, even when previous cell is merged with next.
String cellId = cellElement.GetAttributeValue("r");
values.Add(GetCellDataValue(cellElement));
if (maxColumns > 0 && values.Count >= maxColumns) break;
}
}
catch (Exception exc) { HandleException("GetRowDataValues", exc); }
return values;
}
//-------------------------------------------------------------------
// C e l l s
//-------------------------------------------------------------------
SaMsXmlElement GetCellElement (uint colPos, uint rowPos)
{
ClearErrorState();
String cellId = GetCellId(colPos,rowPos);
try
{
SaMsXmlElement rowElement = GetRowElement(rowPos);
foreach (SaMsXmlElement cellElement in rowElement.GetChildElements("c"))
{
if (cellElement.GetAttributeValue("r").EqualsCI(cellId))
return cellElement;
}
}
catch (Exception exc) { HandleException("GetCellElement", exc); }
return new SaMsXmlElement(null);
}
public override void SetActiveCell (uint colPos, uint rowPos)
{
// ToDo SetActiveCell.
SetErrorState("Not supported");
}
//-------------------------------------------------------------------
private bool IsCellFormula (uint colPos, uint rowPos)
{
ClearErrorState();
bool bIsFormula = false;
try
{
SaMsXmlElement cellElement = GetCellElement(colPos,rowPos);
bIsFormula = !cellElement.GetAttributeValue("f").IsEmpty();
}
catch (Exception exc) { HandleException("IsCellFormula", exc); }
return bIsFormula;
}
private bool IsCellNumeric (uint colPos, uint rowPos)
{
ClearErrorState();
try
{
SaMsXmlElement cellElement = GetCellElement(colPos,rowPos);
SaMsExcelStyle cellStyle = GetCellStyle(cellElement);
return IsCellNumeric(cellElement, cellStyle);
}
catch (Exception exc) { HandleException("IsCellNumeric", exc); }
return false;
}
private bool IsCellNumeric (SaMsXmlElement cellElement)
{
ClearErrorState();
SaMsExcelStyle cellStyle = GetCellStyle(cellElement);
return IsCellNumeric(cellElement, cellStyle);
}
private bool IsCellNumeric (SaMsXmlElement cellElement, SaMsExcelStyle cellStyle)
{
ClearErrorState();
return (cellElement.GetAttributeValue("t").IsEmpty() && !IsDateTimeStyle(cellStyle.NumFmtCode));
}
private bool IsCellDate (uint colPos, uint rowPos)
{
ClearErrorState();
bool bIsDate = false;
try
{
SaMsXmlElement cellElement = GetCellElement(colPos,rowPos);
SaMsExcelStyle cellStyle = GetCellStyle(cellElement);
return IsCellDate(cellElement, cellStyle);
}
catch (Exception exc) { HandleException("IsCellDate", exc); }
return bIsDate;
}
private bool IsCellDate (SaMsXmlElement cellElement)
{
return IsCellDate(cellElement, GetCellStyle(cellElement));
}
private bool IsCellDate (SaMsXmlElement cellElement, SaMsExcelStyle cellStyle)
{
ClearErrorState();
return IsDateTimeStyle(cellStyle.NumFmtCode);
}
//-------------------------------------------------------------------
public override String GetCellFormula (uint colPos, uint rowPos)
{
ClearErrorState();
try
{
SaMsXmlElement cellElement = GetCellElement(colPos,rowPos);
String formula = cellElement.GetAttributeValue("f");
if (formula.IsEmpty())
formula = GetCellText(cellElement, true);
return formula;
}
catch (Exception exc) { HandleException("GetCellFormula", exc); }
return "";
}
private String GetCellText (SaMsXmlElement cellElement, bool trimValue = true)
{
ClearErrorState();
String value = "";
try
{
if (cellElement.GetAttributeValue("t").EqualsCI("s"))
value = GetSharedString(cellElement);
else
value = cellElement.GetChildText("v");
if (trimValue) value.TrimAll();
}
catch (Exception exc) { HandleException("GetCellText", exc); }
return value;
}
public override DataValue GetCellDataValue (uint colPos, uint rowPos)
{
return GetCellDataValue(GetCellElement(colPos, rowPos), false);
}
private DataValue GetCellDataValue (SaMsXmlElement cellElement, bool trimTextValue = true)
{
ClearErrorState();
try
{
SaMsExcelStyle cellStyle = GetCellStyle(cellElement);
if (cellElement.GetAttributeValue("t").EqualsCI("s"))
{
String value = GetSharedString(cellElement);
if (trimTextValue) value = value.TrimAll();
return new DataValue(value);
}
else
{
String value = cellElement.GetChildText("v");
if (IsDateTimeStyle(cellStyle.NumFmtCode))
return new DataValue(DateTime.FromOADate(value.ToDouble()));
if (IsCellNumeric(cellElement, cellStyle))
{
if (value.IsNumeric())
return new DataValue(value.ToLong());
if (value.IsDecimal())
{
#if DEBUG
//if (StringUtil.ToInt(value) > 600) Debugger.Break();
#endif
return new DataValue(value.ToDecimal());
}
}
if (trimTextValue) value = value.TrimAll();
return new DataValue(value);
}
}
catch (Exception exc) { HandleException("GetCellDataValue", exc); }
return new DataValue();
}
//-------------------------------------------------------------------
// Shared string for cell.
private String GetSharedString (SaMsXmlElement nodeElement)
{
ClearErrorState();
try
{
SaMsXmlElement sharedStringElement = GetSharedStringElement(nodeElement);
return sharedStringElement.GetChildText("t");
}
catch (Exception exc) { HandleException("GetSharedString", exc); }
return "";
}
private SaMsXmlElement GetSharedStringElement (SaMsXmlElement cellElement)
{
ClearErrorState();
if (workbook.SharedStringsDoc == null || cellElement == null || !cellElement.Valid) return null;
if (!cellElement.GetAttributeValue("t").EqualsCI("s") || !cellElement.GetChildText("v").IsNumeric()) return null;
// Shared strings are indexed from 0.
long sharedStringIdx = -1;
try
{
long iIdx = cellElement.GetChildText("v").ToLong();
SaMsXmlElement sstElement = workbook.SharedStringsDoc.GetRootElement();
foreach (SaMsXmlElement siElement in sstElement.GetChildElements("si"))
{
if (++sharedStringIdx == iIdx)
return siElement;
}
}
catch (Exception exc) { HandleException("GetSharedStringNode", exc); }
return new SaMsXmlElement(null);
}
//-------------------------------------------------------------------
public override bool SetCellFormula (uint colPos, uint rowPos, String value)
{
return SetErrorState("Not supported");
}
public override bool SetCellFormula (uint colPos, uint rowPos, String value, SaMsExcelItemAttr attr)
{
return SetErrorState("Not supported");
}
public override bool SetRowFormulas (uint colPosStart, uint rowPos, List