Skip to content

Commit

Permalink
Make ExcelHandler actually works
Browse files Browse the repository at this point in the history
  • Loading branch information
honguyenminh committed Feb 5, 2022
1 parent a983b7b commit 27c3644
Showing 1 changed file with 168 additions and 31 deletions.
199 changes: 168 additions & 31 deletions Image2Excel/ExcelHandler.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
using System.Text;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System.IO.Compression;

namespace Image2Excel;

/// <summary>
/// DO NOT run this in parallel
/// </summary>
public class ExcelHandler : IDisposable
{
private readonly string _tempDirectory;
public string TempDirectoryPath { get; }
private readonly Dictionary<string, int> _cellStyleId = new();

public ExcelHandler()
Expand All @@ -19,11 +17,11 @@ public ExcelHandler()
{
// Good luck making a duplicate folder of this.
// I mean it, good luck. You also need to time it right owo.
_tempDirectory = "Image2Excel_" + Guid.NewGuid() + DateTime.Now.ToString("o");
} while (Directory.Exists(_tempDirectory));
TempDirectoryPath = $"Image2Excel_{Guid.NewGuid()}_{DateTime.Now.ToString("d_HH-mm-ss-fff")}";
} while (Directory.Exists(TempDirectoryPath));

Directory.CreateDirectory(_tempDirectory);
Directory.SetCurrentDirectory(_tempDirectory);
var dirInfo = Directory.CreateDirectory(TempDirectoryPath);
TempDirectoryPath = dirInfo.FullName;
InitDirectories();
InitGlobalScopedFile();
InitWorkbook();
Expand All @@ -32,61 +30,73 @@ public ExcelHandler()
/// <summary>
/// Create needed directories
/// </summary>
private static void InitDirectories()
private void InitDirectories()
{
Directory.SetCurrentDirectory(TempDirectoryPath);
Directory.CreateDirectory(@"_rels");
Directory.CreateDirectory("docProps");
Directory.CreateDirectory(Path.Join("xl", "worksheets"));
Directory.CreateDirectory(Path.Join("xl", @"_rels"));
Directory.SetCurrentDirectory("..");
}

/// <summary>
/// Write necessary global-scoped metadata files
/// </summary>
private static void InitGlobalScopedFile()
private void InitGlobalScopedFile()
{
// Write all literal metadata files (no change needed, just copy and paste)
ResourceManager.WriteResourceToFile(@"FileInit..rels", Path.Join(@"_rels", @".rels"));
ResourceManager.WriteResourceToFile(@"FileInit.[Content_Types].xml", "[Content_Types].xml");

ResourceManager.WriteResourceToFile("FileInit..rels",
Path.Join(TempDirectoryPath, "_rels", ".rels"));
ResourceManager.WriteResourceToFile("FileInit.[Content_Types].xml",
Path.Join(TempDirectoryPath, "[Content_Types].xml"));

// Write metadata files with replaced content
string appMetadata = ResourceManager.GetResourceContent(@"FileInit.app.xml");
appMetadata = appMetadata.Replace("|Ver|", VersionTags.Version);
File.WriteAllText(Path.Join("docProps", "app.xml"), appMetadata);
string appMetadata = ResourceManager.GetResourceContent("FileInit.app.xml");
appMetadata = appMetadata.Replace("|Ver|", $"{VersionTags.Major}.{VersionTags.Minor}");
File.WriteAllText(Path.Join(TempDirectoryPath, "docProps", "app.xml"), appMetadata);

StringBuilder coreMetadata = new(ResourceManager.GetResourceContent("FileInit.core.xml"));
coreMetadata.Replace("|CurrentUsername|", Environment.UserName);
// Format current time as W3CDTF format
string formattedCurrentTime = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ssK");
string formattedCurrentTime = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ssZ");
coreMetadata.Replace("|CreationTime|", formattedCurrentTime);
File.WriteAllText(Path.Join("docProps", "core.xml"), coreMetadata.ToString());
File.WriteAllText(Path.Join(TempDirectoryPath, "docProps", "core.xml"), coreMetadata.ToString());
}

private static void InitWorkbook()
private void InitWorkbook()
{
Directory.SetCurrentDirectory("xl");
ResourceManager.WriteResourceToFile("WorkbookInit.workbook.xml", "workbook.xml");
ResourceManager.WriteResourceToFile("WorkbookInit.workbook.xml.rels", Path.Join("_rels", "workbook.xml.rels"));
ResourceManager.WriteResourceToFile("WorkbookInit.workbook.xml",
Path.Join(TempDirectoryPath, "xl", "workbook.xml"));
ResourceManager.WriteResourceToFile("WorkbookInit.workbook.xml.rels",
Path.Join(TempDirectoryPath, "xl", "_rels", "workbook.xml.rels"));
}

// TODO: replace dimension in sheet1 head
public void WriteStyles(Image<Rgb24> image)
/// <summary>
/// Write styles file for the given image
/// </summary>
/// <param name="image"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public void WriteStyles(Image<Rgba32> image)
{
// Process image
// Add all colors to dictionary and styles
StringBuilder fillStyles = new();
StringBuilder cellStyles = new();

for (int y = 0; y < image.Height; y++)
{
if (_cellStyleId.Count > 255)
{
Console.WriteLine("Too many colors. Max is 256 (blame Excel owo)");
return;
throw new ArgumentOutOfRangeException(nameof(image),
"Too many colors. Max is 256 (blame Excel owo)");
}

Span<Rgb24> row = image.GetPixelRowSpan(y);
// TODO: add default colors from excel here to avoid duplication
Span<Rgba32> row = image.GetPixelRowSpan(y);
for (int x = 0; x < image.Width; x++)
{
string hexCode = row[x].ToHex();
string hexCode = row[x].ToArgbHex();
if (_cellStyleId.ContainsKey(hexCode)) continue;

fillStyles.Append("<fill><patternFill patternType=\"solid\"><fgColor rgb=\"");
Expand All @@ -99,12 +109,139 @@ public void WriteStyles(Image<Rgb24> image)
cellStyles.Append("\" borderId=\"0\" xfId=\"0\" applyFill=\"1\"/>");
}
}

// Write styles to file
WriteStylesToFile(fillStyles.ToString(), cellStyles.ToString());
}

private void WriteStylesToFile(string fillStyles, string cellStyles)
{
using var fileStream = File.OpenWrite(Path.Join(TempDirectoryPath, "xl", "styles.xml"));
using StreamWriter writer = new(fileStream);

ResourceManager.WriteResourceToStream("Parts.styles.xml.head.txt", fileStream);

// Write fill styles
writer.Write("<fills count=\"");
writer.Write(_cellStyleId.Count + 2);
// Two mandatory styles, blame excel
writer.Write("\"><fill><patternFill patternType=\"none\"/></fill>"
+ "<fill><patternFill patternType=\"gray125\"/></fill>");

writer.Write(fillStyles);
writer.Flush();

ResourceManager.WriteResourceToStream("Parts.styles.xml.mid.txt", fileStream);

// Write cell styles
writer.Write($"<cellXfs count=\"{_cellStyleId.Count + 1}\">");
// Dummy one, because excel is stupid
writer.Write("<xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/>");
writer.Write(cellStyles);
writer.Flush();

ResourceManager.WriteResourceToStream("Parts.styles.xml.bottom.txt", fileStream);
}

public void WriteSheet(Image<Rgba32> image)
{
using var fileStream = File.OpenWrite(Path.Join(TempDirectoryPath, "xl", "worksheets", "sheet1.xml"));
using StreamWriter writer = new(fileStream);

ResourceManager.WriteResourceToStream("Parts.sheet1.xml.head.txt", fileStream);

// Write sheet data to temp file
string? highestColumnName = WriteTempSheet(image, "sheet1.xml.temp");

if (highestColumnName is null)
{
writer.Write("<dimension ref=\"A1:A1\"/>"); // Image is empty
}
else writer.Write($"<dimension ref=\"A1:{highestColumnName}{image.Height}\"/>");
writer.Write("<sheetFormatPr defaultColWidth=\"1\" defaultRowHeight=\"5.95\" customHeight=\"1\"/>");
writer.Flush();

// Read temp file back and save to main sheet file
string tempPath = Path.Join(TempDirectoryPath, "xl", "worksheets", "sheet1.xml.temp");
using (var tempFile = File.OpenRead(tempPath))
{
tempFile.CopyTo(fileStream);
}
File.Delete(tempPath);

ResourceManager.WriteResourceToStream("Parts.sheet1.xml.bottom.txt", fileStream);
}

/// <summary>
/// Write a temp file containing sheetData for a sheet inside the sheets folder
/// </summary>
/// <param name="image">Image to get data from</param>
/// <param name="tempName">Name of the temp file</param>
/// <returns>The highest column name of the sheet</returns>
private string? WriteTempSheet(Image<Rgba32> image, string tempName)
{
using var fileStream = File.OpenWrite(Path.Join(TempDirectoryPath, "xl", "worksheets", tempName));
using StreamWriter writer = new(fileStream);

writer.Write("<sheetData>");
char[] highestColChars = Array.Empty<char>();
for (int rowIndex = 0; rowIndex < image.Height; rowIndex++)
{
writer.Write($"<row r=\"{rowIndex + 1}\">");
// Build cell list
List<char> columnName = new() { 'A' };
Span<Rgba32> row = image.GetPixelRowSpan(rowIndex);
for (int x = 0; x < image.Width; x++)
{
string hexCode = row[x].ToArgbHex();
highestColChars = columnName.ToArray();
writer.Write("<c r=\"");
writer.Write(highestColChars);
writer.Write(rowIndex + 1);
writer.Write("\" s=\"");
writer.Write(_cellStyleId[hexCode]);
writer.Write("\"/>");

// Increase column name
bool keepRunning = true;
int index = columnName.Count - 1;
while (keepRunning && index > -1)
{
if (columnName[index] == 'Z')
{
columnName[index] = 'A';
index--;
continue;
}
columnName[index]++;
keepRunning = false;
}
if (index == -1)
{
columnName.Insert(0, 'A');
}
}

writer.Write("</row>");
}
writer.Write("</sheetData>");
writer.Flush();

return highestColChars.Length != 0 ? new string(highestColChars) : null;
}

public void Save(string filePath)
{
if (File.Exists(filePath))
throw new ArgumentException("File already existed");

ZipFile.CreateFromDirectory(TempDirectoryPath, filePath);
}

private void ReleaseUnmanagedResources()
{
if (Directory.Exists(_tempDirectory))
Directory.Delete(_tempDirectory, true);
if (Directory.Exists(TempDirectoryPath))
Directory.Delete(TempDirectoryPath, true);
}

public void Dispose()
Expand Down

0 comments on commit 27c3644

Please sign in to comment.