NanoXLSX.Writer 3.0.0-rc.3
Loading...
Searching...
No Matches
XlsxWriter.cs
1/*
2 * NanoXLSX is a small .NET library to generate and read XLSX (Microsoft Excel 2007 or newer) files in an easy and native way
3 * Copyright Raphael Stoeckli © 2025
4 * This library is licensed under the MIT License.
5 * You find a copy of the license in project folder or on: http://opensource.org/licenses/MIT
6 */
7
8using NanoXLSX.Exceptions;
11using NanoXLSX.Registry;
12using NanoXLSX.Styles;
13using NanoXLSX.Utils;
14using System;
15using System.Collections.Generic;
16using System.IO;
17using System.IO.Packaging;
18using System.Linq;
19using System.Text;
20using System.Threading.Tasks;
21using System.Xml;
22using IOException = NanoXLSX.Exceptions.IOException;
23using PackagePartType = NanoXLSX.Internal.Structures.PackagePartDefinition.PackagePartType;
24using XmlElement = NanoXLSX.Utils.Xml.XmlElement;
25
27{
32 internal class XlsxWriter : IBaseWriter
33 {
34
35 #region staticFields
36 private static readonly DocumentPath WORKBOOK = new DocumentPath("workbook.xml", "xl/");
37 private static readonly DocumentPath STYLES = new DocumentPath("styles.xml", "xl/");
38 private static readonly DocumentPath APP_PROPERTIES = new DocumentPath("app.xml", "docProps/");
39 private static readonly DocumentPath CORE_PROPERTIES = new DocumentPath("core.xml", "docProps/");
40 private static readonly DocumentPath SHARED_STRINGS = new DocumentPath("sharedStrings.xml", "xl/");
41 private static readonly DocumentPath THEME = new DocumentPath("theme1.xml", "xl/theme/");
42 #endregion
43
44 #region privateFields
45 private int rootPackageIndex = 1;
46 private int xlPackageIndex = 1;
47
48 private readonly List<PackagePartDefinition> packagePartDefinitions = new List<PackagePartDefinition>();
49
50 private readonly Dictionary<string, Dictionary<string, PackagePart>> packageParts = new Dictionary<string, Dictionary<string, PackagePart>>();
51 private readonly Dictionary<int, DocumentPath> worksheetPaths = new Dictionary<int, DocumentPath>();
52 private Package package = null;
53
54 #endregion
55
56 #region properties
60 public Workbook Workbook { get; }
61
65 public StyleManager Styles { get; private set; }
66
70 public ISharedStringWriter SharedStringWriter { get; set; }
71
72 #endregion
73
74 #region constructors
79 public XlsxWriter(Workbook workbook)
80 {
81 this.Workbook = workbook;
82 }
83 #endregion
84
85 #region documentCreation_methods
86
95 public void Save()
96 {
97 try
98 {
99 FileStream fs = new FileStream(Workbook.Filename, FileMode.Create);
100 SaveAsStream(fs);
101
102 }
103 catch (Exception e)
104 {
105 throw new IOException("An error occurred while saving. See inner exception for details: " + e.Message, e);
106 }
107 }
108
114 public async Task SaveAsync()
115 {
116 await Task.Run(() => { Save(); });
117 }
118
122 private void RegisterCommonPackageParts()
123 {
124 // Workbook should always be the lowest index
125 RegisterPackagePart(PackagePartType.Root, PackagePartDefinition.WORKBOOK_PACKAGE_PART_INDEX, WORKBOOK, @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument");
126 if (this.Workbook.WorkbookMetadata != null)
127 {
128 int index = PackagePartDefinition.METADATA_PACKAGE_PART_START_INDEX;
129 RegisterPackagePart(PackagePartType.Root, index, CORE_PROPERTIES, @"application/vnd.openxmlformats-package.core-properties+xml", @"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties");
130 RegisterPackagePart(PackagePartType.Root, index + 1000, APP_PROPERTIES, @"application/vnd.openxmlformats-officedocument.extended-properties+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties");
131 }
132 int worksheetOrderNumber = PackagePartDefinition.WORKSHEET_PACKAGE_PART_START_INDEX;
133 if (this.Workbook.Worksheets.Count == 0)
134 {
135 RegisterPackagePart(PackagePartType.Worksheet, worksheetOrderNumber, "sheet1.xml", "xl/worksheets", @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet");
136 }
137 else
138 {
139 for (int i = 0; i < this.Workbook.Worksheets.Count; i++)
140 {
141 string fileName = "sheet" + ParserUtils.ToString(i + 1) + ".xml";
142 RegisterPackagePart(PackagePartType.Worksheet, worksheetOrderNumber, fileName, "xl/worksheets", @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet");
143 worksheetOrderNumber++;
144 }
145 }
146 int postWorksheetOrderNumber = PackagePartDefinition.POST_WORSHEET_PACKAGE_PART_START_INDEX;
147 if (Workbook.WorkbookTheme != null)
148 {
149 RegisterPackagePart(PackagePartType.Other, postWorksheetOrderNumber, THEME, @"application/vnd.openxmlformats-officedocument.theme+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme");
150 postWorksheetOrderNumber += 1000;
151 }
152 RegisterPackagePart(PackagePartType.Other, postWorksheetOrderNumber, STYLES, @"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles");
153 postWorksheetOrderNumber += 1000;
154 RegisterPackagePart(PackagePartType.Other, postWorksheetOrderNumber, SHARED_STRINGS, @"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings");
155 // TODO: add themeIndex once if media is embedded
156 }
157
161 private void PreparePackage()
162 {
163 List<PackagePartDefinition> definitions = PackagePartDefinition.Sort(this.packagePartDefinitions);
164 PackagePartDefinition workbookDefinition = definitions.First(p => p.OrderNumber == PackagePartDefinition.WORKBOOK_PACKAGE_PART_INDEX);
165 PackagePart workbookPart = CreateRootPackagePart(workbookDefinition.Path, workbookDefinition.ContentType, workbookDefinition.RelationshipType);
166 foreach (PackagePartDefinition definition in definitions)
167 {
168 if (definition.OrderNumber == PackagePartDefinition.WORKBOOK_PACKAGE_PART_INDEX)
169 {
170 continue;
171 }
172 if (definition.PartType == PackagePartType.Root)
173 {
174 CreateRootPackagePart(definition.Path, definition.ContentType, definition.RelationshipType);
175 }
176 else
177 {
178 CreateXlPackagePart(workbookPart, definition.Path, definition.ContentType, definition.RelationshipType);
179 if (definition.PartType == PackagePartType.Worksheet)
180 {
181 worksheetPaths.Add(definition.GetWorksheetIndex(), definition.Path);
182 }
183 }
184 }
185 }
186
194 internal PackagePart CreateRootPackagePart(DocumentPath documentPath, string contentType, string relationshipType)
195 {
196 Uri uri = new Uri(documentPath.GetFullPath(), UriKind.Relative);
197 PackagePart part = this.package.CreatePart(uri, contentType, CompressionOption.Normal);
198 if (!packageParts.ContainsKey(documentPath.Path))
199 {
200 packageParts.Add(documentPath.Path, new Dictionary<string, PackagePart>());
201 }
202 packageParts[documentPath.Path].Add(documentPath.Filename, part);
203 this.package.CreateRelationship(uri, TargetMode.Internal, relationshipType, "rId" + ParserUtils.ToString(rootPackageIndex));
204 rootPackageIndex++;
205 return part;
206 }
207
215 internal void CreateXlPackagePart(PackagePart parentPart, DocumentPath documentPath, string contentType, string relationshipType)
216 {
217 Uri uri = new Uri(documentPath.GetFullPath(), UriKind.Relative);
218 PackagePart part = this.package.CreatePart(uri, contentType, CompressionOption.Normal);
219 if (!packageParts.ContainsKey(documentPath.Path))
220 {
221 packageParts.Add(documentPath.Path, new Dictionary<string, PackagePart>());
222 }
223 packageParts[documentPath.Path].Add(documentPath.Filename, part);
224 parentPart.CreateRelationship(uri, TargetMode.Internal, relationshipType, "rId" + ParserUtils.ToString(xlPackageIndex));
225 xlPackageIndex++;
226 }
227
237 internal void RegisterPackagePart(PackagePartDefinition.PackagePartType type, int orderNumber, string fileNameInPackage, string pathInPackage, string contentType, string relationshipType)
238 {
239 this.packagePartDefinitions.Add(new PackagePartDefinition(type, orderNumber, fileNameInPackage, pathInPackage, contentType, relationshipType));
240 }
241
250 internal void RegisterPackagePart(PackagePartType type, int orderNumber, DocumentPath documentPath, string contentType, string relationshipType)
251 {
252 this.packagePartDefinitions.Add(new PackagePartDefinition(type, orderNumber, documentPath, contentType, relationshipType));
253 }
254
261 public void SaveAsStream(Stream stream, bool leaveOpen = false)
262 {
263 Workbook.ResolveMergedCells();
264 this.Styles = StyleManager.GetManagedStyles(Workbook);
265 try
266 {
267 HandlePackageRegistryQueuePlugIns();
268 HandleQueuePlugIns(PlugInUUID.WriterPrependingQueue);
269
270 RegisterCommonPackageParts();
271 using (Package xlsxPackage = Package.Open(stream, FileMode.Create))
272 {
273 this.package = xlsxPackage;
274 PreparePackage();
275 PackagePart part;
276
277 // Workbook
278 IPlugInWriter workbookWriter = PlugInLoader.GetPlugIn<IPlugInWriter>(PlugInUUID.WorkbookWriter, new WorkbookWriter());
279 workbookWriter.Init(this);
280 workbookWriter.Execute();
281 part = packageParts[WORKBOOK.Path][WORKBOOK.Filename];
282 AppendXmlToPackagePart(workbookWriter.XmlElement, part);
283
284 // Style
285 IPlugInWriter styleWriter = PlugInLoader.GetPlugIn<IPlugInWriter>(PlugInUUID.StyleWriter, new StyleWriter());
286 styleWriter.Init(this);
287 styleWriter.Execute();
288 part = packageParts[STYLES.Path][STYLES.Filename];
289 AppendXmlToPackagePart(styleWriter.XmlElement, part);
290
291 // Shared strings - preparation
292 SharedStringWriter = PlugInLoader.GetPlugIn<ISharedStringWriter>(PlugInUUID.SharedStringsWriter, new SharedStringWriter());
293 SharedStringWriter.Init(this);
294 // Worksheets
295 IWorksheetWriter worksheetWriter = PlugInLoader.GetPlugIn<IWorksheetWriter>(PlugInUUID.WorksheetWriter, new WorksheetWriter());
296 worksheetWriter.Init(this);
297 if (Workbook.Worksheets.Count > 0)
298 {
299 for (int i = 0; i < Workbook.Worksheets.Count; i++)
300 {
301 Worksheet item = Workbook.Worksheets[i];
302 part = packageParts[worksheetPaths[i].Path][worksheetPaths[i].Filename];
303 worksheetWriter.CurrentWorksheet = item;
304 worksheetWriter.Execute();
305 AppendXmlToPackagePart(worksheetWriter.XmlElement, part);
306 }
307 }
308 else
309 {
310 part = packageParts[worksheetPaths[0].Path][worksheetPaths[0].Filename];
311 worksheetWriter.CurrentWorksheet = new Worksheet("sheet1");
312 worksheetWriter.Execute();
313 AppendXmlToPackagePart(worksheetWriter.XmlElement, part);
314 }
315
316 // Shared strings - write after collection of strings
317 part = packageParts[SHARED_STRINGS.Path][SHARED_STRINGS.Filename];
318 SharedStringWriter.Execute();
319 AppendXmlToPackagePart(SharedStringWriter.XmlElement, part);
320
321 // Metadata
322 if (this.Workbook.WorkbookMetadata != null)
323 {
324 IPlugInWriter metadataAppWriter = PlugInLoader.GetPlugIn<IPlugInWriter>(PlugInUUID.MetadataAppWriter, new MetadataAppWriter());
325 metadataAppWriter.Init(this);
326 metadataAppWriter.Execute();
327 part = packageParts[APP_PROPERTIES.Path][APP_PROPERTIES.Filename];
328 AppendXmlToPackagePart(metadataAppWriter.XmlElement, part);
329 IPlugInWriter metadataCoreWriter = PlugInLoader.GetPlugIn<IPlugInWriter>(PlugInUUID.MetadataCoreWriter, new MetadataCoreWriter());
330 metadataCoreWriter.Init(this);
331 metadataCoreWriter.Execute();
332 part = packageParts[CORE_PROPERTIES.Path][CORE_PROPERTIES.Filename];
333 AppendXmlToPackagePart(metadataCoreWriter.XmlElement, part);
334 }
335
336 // Theme
337 if (Workbook.WorkbookTheme != null)
338 {
339 IPlugInWriter themeWriter = PlugInLoader.GetPlugIn<IPlugInWriter>(PlugInUUID.ThemeWriter, new ThemeWriter());
340 themeWriter.Init(this);
341 themeWriter.Execute();
342 part = packageParts[THEME.Path][THEME.Filename];
343 AppendXmlToPackagePart(themeWriter.XmlElement, part);
344 }
345
346 HandleQueuePlugIns(PlugInUUID.WriterAppendingQueue);
347
348 this.package.Flush();
349 this.package.Close();
350 if (!leaveOpen)
351 {
352 stream.Close();
353 }
354
355 }
356 }
357 catch (Exception e)
358 {
359 throw new IOException("An error occurred while saving. See inner exception for details: " + e.Message, e);
360 }
361 }
362
370 public async Task SaveAsStreamAsync(Stream stream, bool leaveOpen = false)
371 {
372 await Task.Run(() => { SaveAsStream(stream, leaveOpen); });
373 }
374 #endregion
375
380 private void HandleQueuePlugIns(string queueUuid)
381 {
382 IPlugInWriter queueWriter;
383 string lastUuid = null;
384 do
385 {
386 queueWriter = PlugInLoader.GetNextQueuePlugIn<IPlugInWriter>(queueUuid, lastUuid, out string currentUuid);
387 if (queueWriter != null)
388 {
389 queueWriter.Init(this);
390 queueWriter.Execute();
391 if (queueWriter is IPlugInPackageWriter packageWriter)
392 {
393 if (!string.IsNullOrEmpty(packageWriter.PackagePartPath) && !string.IsNullOrEmpty(packageWriter.PackagePartFileName))
394 {
395 if (packageParts.ContainsKey(packageWriter.PackagePartPath) && packageParts[packageWriter.PackagePartPath].ContainsKey(packageWriter.PackagePartFileName))
396 {
397 PackagePart pp = packageParts[packageWriter.PackagePartPath][packageWriter.PackagePartFileName];
398 AppendXmlToPackagePart(packageWriter.XmlElement, pp);
399 }
400 }
401 }
402 lastUuid = currentUuid;
403 }
404 else
405 {
406 lastUuid = null;
407 }
408
409 } while (queueWriter != null);
410 }
411
415 private void HandlePackageRegistryQueuePlugIns()
416 {
417 IPlugInPackageWriter queueWriter;
418 string lastUuid = null;
419 do
420 {
421 queueWriter = PlugInLoader.GetNextQueuePlugIn<IPlugInPackageWriter>(PlugInUUID.WriterPackageRegistryQueue, lastUuid, out string currentUuid);
422 if (queueWriter != null)
423 {
424 queueWriter.Execute(); // Execute anything that could be defined
425 PackagePartType packagePartType;
426 if (queueWriter.IsRootPackagePart)
427 {
428 packagePartType = PackagePartType.Root;
429 }
430 else
431 {
432 packagePartType = PackagePartType.Other;
433 }
434 RegisterPackagePart(packagePartType, queueWriter.OrderNumber, new DocumentPath(queueWriter.PackagePartFileName, queueWriter.PackagePartPath), queueWriter.ContentType, queueWriter.RelationshipType);
435 lastUuid = currentUuid;
436 }
437 else
438 {
439 lastUuid = null;
440 }
441
442 } while (queueWriter != null);
443 }
444
450 private void AppendXmlToPackagePart(XmlElement rootElement, PackagePart pp)
451 {
452 XmlDocument doc = rootElement.TransformToDocument(); // This creates a System.Xml.XmlDocument from a custom XmlElement instance
453 using (MemoryStream ms = new MemoryStream())
454 {
455 XmlWriterSettings settings = new XmlWriterSettings
456 {
457 Encoding = new UTF8Encoding(false), // No BOM
458 Indent = true,
459 OmitXmlDeclaration = false // Include <?xml version="1.0" encoding="utf-8"?>
460 };
461
462 using (XmlWriter writer = XmlWriter.Create(ms, settings))
463 {
464 writer.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"");
465 doc.WriteTo(writer);
466 writer.Flush();
467 }
468
469 AddStreamToPackagePart(ms, pp);
470 }
471 }
472
478 internal void AddStreamToPackagePart(MemoryStream stream, PackagePart pp)
479 {
480 stream.Position = 0;
481 stream.CopyTo(pp.GetStream());
482 stream.Flush();
483 }
484
485 }
486}