NanoXLSX.Writer 3.1.0
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 © 2026
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;
9using NanoXLSX.Interfaces.Writer;
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 worksheetWriter.ReleaseXmlElement();
307 GC.Collect(1, GCCollectionMode.Optimized); //
308 }
309 }
310 else
311 {
312 part = packageParts[worksheetPaths[0].Path][worksheetPaths[0].Filename];
313 worksheetWriter.CurrentWorksheet = new Worksheet("sheet1");
314 worksheetWriter.Execute();
315 AppendXmlToPackagePart(worksheetWriter.XmlElement, part);
316 worksheetWriter.ReleaseXmlElement();
317 }
318
319 // Shared strings - write after collection of strings
320 part = packageParts[SHARED_STRINGS.Path][SHARED_STRINGS.Filename];
321 SharedStringWriter.Execute();
322 AppendXmlToPackagePart(SharedStringWriter.XmlElement, part);
323
324 // Metadata
325 if (this.Workbook.WorkbookMetadata != null)
326 {
327 IPluginWriter metadataAppWriter = PlugInLoader.GetPlugIn<IPluginWriter>(PlugInUUID.MetadataAppWriter, new MetadataAppWriter());
328 metadataAppWriter.Init(this);
329 metadataAppWriter.Execute();
330 part = packageParts[APP_PROPERTIES.Path][APP_PROPERTIES.Filename];
331 AppendXmlToPackagePart(metadataAppWriter.XmlElement, part);
332 IPluginWriter metadataCoreWriter = PlugInLoader.GetPlugIn<IPluginWriter>(PlugInUUID.MetadataCoreWriter, new MetadataCoreWriter());
333 metadataCoreWriter.Init(this);
334 metadataCoreWriter.Execute();
335 part = packageParts[CORE_PROPERTIES.Path][CORE_PROPERTIES.Filename];
336 AppendXmlToPackagePart(metadataCoreWriter.XmlElement, part);
337 }
338
339 // Theme
340 if (Workbook.WorkbookTheme != null)
341 {
342 IPluginWriter themeWriter = PlugInLoader.GetPlugIn<IPluginWriter>(PlugInUUID.ThemeWriter, new ThemeWriter());
343 themeWriter.Init(this);
344 themeWriter.Execute();
345 part = packageParts[THEME.Path][THEME.Filename];
346 AppendXmlToPackagePart(themeWriter.XmlElement, part);
347 }
348
349 HandleQueuePlugIns(PlugInUUID.WriterAppendingQueue);
350
351 this.package.Flush();
352 this.package.Close();
353 if (!leaveOpen)
354 {
355 stream.Close();
356 }
357
358 }
359 }
360 catch (Exception e)
361 {
362 throw new IOException("An error occurred while saving. See inner exception for details: " + e.Message, e);
363 }
364 }
365
373 public async Task SaveAsStreamAsync(Stream stream, bool leaveOpen = false)
374 {
375 await Task.Run(() => { SaveAsStream(stream, leaveOpen); });
376 }
377 #endregion
378
383 private void HandleQueuePlugIns(string queueUuid)
384 {
385 IPluginWriter queueWriter;
386 string lastUuid = null;
387 do
388 {
389 queueWriter = PlugInLoader.GetNextQueuePlugIn<IPluginWriter>(queueUuid, lastUuid, out string currentUuid);
390 if (queueWriter != null)
391 {
392 queueWriter.Init(this);
393 queueWriter.Execute();
394 if (queueWriter is IPluginPackageWriter packageWriter)
395 {
396 if (!string.IsNullOrEmpty(packageWriter.PackagePartPath) && !string.IsNullOrEmpty(packageWriter.PackagePartFileName))
397 {
398 if (packageParts.ContainsKey(packageWriter.PackagePartPath) && packageParts[packageWriter.PackagePartPath].ContainsKey(packageWriter.PackagePartFileName))
399 {
400 PackagePart pp = packageParts[packageWriter.PackagePartPath][packageWriter.PackagePartFileName];
401 AppendXmlToPackagePart(packageWriter.XmlElement, pp);
402 }
403 }
404 }
405 lastUuid = currentUuid;
406 }
407 else
408 {
409 lastUuid = null;
410 }
411
412 } while (queueWriter != null);
413 }
414
418 private void HandlePackageRegistryQueuePlugIns()
419 {
420 IPluginPackageWriter queueWriter;
421 string lastUuid = null;
422 do
423 {
424 queueWriter = PlugInLoader.GetNextQueuePlugIn<IPluginPackageWriter>(PlugInUUID.WriterPackageRegistryQueue, lastUuid, out string currentUuid);
425 if (queueWriter != null)
426 {
427 queueWriter.Execute(); // Execute anything that could be defined
428 PackagePartType packagePartType;
429 if (queueWriter.IsRootPackagePart)
430 {
431 packagePartType = PackagePartType.Root;
432 }
433 else
434 {
435 packagePartType = PackagePartType.Other;
436 }
437 RegisterPackagePart(packagePartType, queueWriter.OrderNumber, new DocumentPath(queueWriter.PackagePartFileName, queueWriter.PackagePartPath), queueWriter.ContentType, queueWriter.RelationshipType);
438 lastUuid = currentUuid;
439 }
440 else
441 {
442 lastUuid = null;
443 }
444
445 } while (queueWriter != null);
446 }
447
453 private void AppendXmlToPackagePart(XmlElement rootElement, PackagePart pp)
454 {
455 using (MemoryStream ms = new MemoryStream())
456 {
457 XmlWriterSettings settings = new XmlWriterSettings
458 {
459 Encoding = new UTF8Encoding(false), // No BOM
460 Indent = true,
461 OmitXmlDeclaration = true, // Include <?xml version="1.0" encoding="utf-8"?>
462 CloseOutput = false
463 };
464
465 using (XmlWriter writer = XmlWriter.Create(ms, settings))
466 {
467 writer.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"");
468 rootElement.WriteTo(writer);
469 writer.Flush();
470 }
471
472 AddStreamToPackagePart(ms, pp);
473 }
474 }
475
481 internal void AddStreamToPackagePart(MemoryStream stream, PackagePart pp)
482 {
483 stream.Position = 0;
484 stream.CopyTo(pp.GetStream());
485 stream.Flush();
486 }
487
488 }
489}