NanoXLSX.Reader 3.2.1
Loading...
Searching...
No Matches
XlsxReader.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 System;
9using System.Collections.Generic;
10using System.IO;
11using System.IO.Compression;
12using System.Threading.Tasks;
13using NanoXLSX.Exceptions;
14using NanoXLSX.Interfaces.Reader;
15using NanoXLSX.Registry;
16using NanoXLSX.Styles;
17using IOException = NanoXLSX.Exceptions.IOException;
18
20{
24 public class XlsxReader : IDisposable
25 {
26 #region privateFields
27 private readonly string filePath;
28 private readonly Stream inputStream;
29 private readonly ReaderOptions readerOptions;
30 private MemoryStream memoryStream;
31 #endregion
32
33 #region properties
37 public Workbook Workbook { get; internal set; }
38 #endregion
39
40 #region constructors
46 public XlsxReader(string path, ReaderOptions options = null)
47 {
48 filePath = path;
49 readerOptions = options;
50 }
51
57 public XlsxReader(Stream stream, ReaderOptions options = null)
58 {
59 inputStream = stream;
60 readerOptions = options;
61 }
62 #endregion
63
64 #region methods
65
72 public void Read()
73 {
74 try
75 {
76 using (memoryStream = new MemoryStream())
77 {
78 Task.Run(() => ReadInternal()).GetAwaiter().GetResult();
79 }
80 }
81 catch (NotSupportedContentException)
82 {
83 throw; // re-throw
84 }
85 catch (IOException)
86 {
87 throw; // re-throw
88 }
89 catch (Exception ex)
90 {
91 throw new IOException("There was an error while reading an XLSX file. Please see the inner exception:", ex);
92 }
93 }
94
102 public async Task ReadAsync()
103 {
104 try
105 {
106 using (memoryStream = new MemoryStream())
107 {
108 await ReadInternal();
109 }
110 }
111 catch (IOException)
112 {
113 throw; // re-throw
114 }
115 catch (Exception ex)
116 {
117 throw new IOException("There was an error while reading an XLSX file. Please see the inner exception:", ex);
118 }
119 }
120
125 private async Task ReadInternal()
126 {
127 ZipArchive zf;
128 if (inputStream == null && !string.IsNullOrEmpty(filePath))
129 {
130 using (FileStream fs = new FileStream(filePath, FileMode.Open))
131 {
132 await fs.CopyToAsync(memoryStream);
133 }
134 }
135 else if (inputStream != null)
136 {
137 using (inputStream)
138 {
139 await inputStream.CopyToAsync(memoryStream);
140 }
141 }
142 else
143 {
144 throw new IOException("No valid stream or file path was provided to open");
145 }
146
147 memoryStream.Position = 0;
148 zf = new ZipArchive(memoryStream, ZipArchiveMode.Read);
149
150 await Task.Run(() =>
151 {
152 ReadZip(zf);
153 }).ConfigureAwait(false);
154 }
155
160 private void ReadZip(ZipArchive zf)
161 {
162 MemoryStream ms;
163 Workbook wb = new Workbook
164 {
165 importInProgress = true // Disables checks during load
166 };
167 Dictionary<string, ZipArchiveEntry> entryLookup = new Dictionary<string, ZipArchiveEntry>(zf.Entries.Count, StringComparer.Ordinal);
168 foreach (ZipArchiveEntry entry in zf.Entries)
169 {
170 entryLookup[entry.FullName] = entry;
171 }
172
173 IDiscoveryReader discoveryReader = PlugInLoader.GetPlugIn<IDiscoveryReader>(PlugInUUID.DiscoveryReader, new DiscoveryReader());
174 discoveryReader.Init(zf, wb, readerOptions);
175 discoveryReader.Execute();
176 RelationshipCatalog relationshipCatalog = wb.AuxiliaryData.GetData<RelationshipCatalog>(PlugInUUID.DiscoveryReader, PlugInUUID.DiscoveryCatalogEntity);
177 if (relationshipCatalog == null)
178 {
179 throw new IOException("The relationship discovery reader did not provide a relationship catalog. The XLSX file may be corrupted.");
180 }
181
182 HandleQueuePlugIns(PlugInUUID.ReaderPackageRegistryQueue, entryLookup, relationshipCatalog, ref wb);
183 HandleQueuePlugIns(PlugInUUID.ReaderPrependingQueue, entryLookup, relationshipCatalog, ref wb);
184
185 IPluginBaseReader workbookReader = PlugInLoader.GetPlugIn<IPluginBaseReader>(PlugInUUID.WorkbookReader, new WorkbookReader());
186 RelationshipInfo workbookRelationship = GetRelationship(relationshipCatalog, string.Empty, GetDocumentType(workbookReader, new WorkbookReader().DocumentType), true);
187 string workbookPartPath = workbookRelationship.ResolvedTargetPath;
188
189 ISharedStringReader sharedStringsReader = PlugInLoader.GetPlugIn<ISharedStringReader>(PlugInUUID.SharedStringsReader, new SharedStringsReader());
190 RelationshipInfo sharedStringsRelationship = GetRelationship(relationshipCatalog, workbookPartPath, sharedStringsReader.DocumentType, false);
191 if (sharedStringsRelationship != null)
192 {
193 ZipArchiveEntry sharedStringsEntry = GetRequiredEntry(sharedStringsRelationship, entryLookup);
194 if (PlugInLoader.HasQueuePlugins(PlugInUUID.SharedStringsInlineReader))
195 {
196 // Inline plugins need a seekable stream; buffer so the handler can reset position
197 MemoryStream ssMs = GetEntryStream(sharedStringsRelationship.ResolvedTargetPath, entryLookup);
198 sharedStringsReader.Init(ssMs, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
199 sharedStringsReader.Execute();
200 }
201 else
202 {
203 // Direct-stream from ZIP entry — no intermediate MemoryStream
204 using (Stream sharedStringsStream = sharedStringsEntry.Open())
205 {
206 sharedStringsReader.Init(sharedStringsStream, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
207 sharedStringsReader.Execute();
208 }
209 }
210 }
211
212 IPluginBaseReader themeReader = PlugInLoader.GetPlugIn<IPluginBaseReader>(PlugInUUID.ThemeReader, new ThemeReader());
213 // Multiple workbook theme relationships are not defined clearly. Retain every relationship in the
214 // discovery catalog, but preserve the previous reader behavior by processing only the first one.
215 // "First" is deterministic and means XML order in the discovered workbook relationship part.
216 RelationshipInfo themeRelationship = GetRelationship(relationshipCatalog, workbookPartPath, GetDocumentType(themeReader, new ThemeReader().DocumentType), false);
217 if (themeRelationship != null)
218 {
219 ms = GetRequiredEntryStream(themeRelationship, entryLookup);
220 themeReader.Init(ms, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
221 themeReader.Execute();
222 }
223
224 StyleRepository.Instance.ImportInProgress = true; // TODO: To be checked
225 IPluginBaseReader styleReader = PlugInLoader.GetPlugIn<IPluginBaseReader>(PlugInUUID.StyleReader, new StyleReader());
226 RelationshipInfo styleRelationship = GetRelationship(relationshipCatalog, workbookPartPath, GetDocumentType(styleReader, new StyleReader().DocumentType), true);
227 ms = GetRequiredEntryStream(styleRelationship, entryLookup);
228 styleReader.Init(ms, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
229 styleReader.Execute();
230 StyleRepository.Instance.ImportInProgress = false;
231
232 ms = GetRequiredEntryStream(workbookRelationship, entryLookup);
233 workbookReader.Init(ms, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
234 workbookReader.Execute();
235
236 IPluginBaseReader metadataAppReader = PlugInLoader.GetPlugIn<IPluginBaseReader>(PlugInUUID.MetadataAppReader, new MetadataAppReader());
237 RelationshipInfo metadataAppRelationship = GetRelationship(relationshipCatalog, string.Empty, GetDocumentType(metadataAppReader, new MetadataAppReader().DocumentType), false);
238 if (metadataAppRelationship != null)
239 {
240 ms = GetRequiredEntryStream(metadataAppRelationship, entryLookup);
241 metadataAppReader.Init(ms, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
242 metadataAppReader.Execute();
243 }
244
245 IPluginBaseReader metadataCoreReader = PlugInLoader.GetPlugIn<IPluginBaseReader>(PlugInUUID.MetadataCoreReader, new MetadataCoreReader());
246 RelationshipInfo metadataCoreRelationship = GetRelationship(relationshipCatalog, string.Empty, GetDocumentType(metadataCoreReader, new MetadataCoreReader().DocumentType), false);
247 if (metadataCoreRelationship != null)
248 {
249 ms = GetRequiredEntryStream(metadataCoreRelationship, entryLookup);
250 metadataCoreReader.Init(ms, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
251 metadataCoreReader.Execute();
252 }
253
254 IPluginBaseReader relationships = PlugInLoader.GetPlugIn<IPluginBaseReader>(PlugInUUID.RelationshipReader, new RelationshipReader());
255 ms = GetEntryStream(GetRelationshipPartPath(workbookPartPath), entryLookup);
256 relationships.Init(ms, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
257 relationships.Execute(); // Only for remaining plugin handling
258
259 IWorksheetReader worksheetReader = PlugInLoader.GetPlugIn<IWorksheetReader>(PlugInUUID.WorksheetReader, new WorksheetReader());
260 worksheetReader.SharedStrings = sharedStringsRelationship == null ? null : sharedStringsReader.SharedStrings;
261 int worksheetVisualIndex = 0;
262 WorksheetDefinition definition;
263 while ((definition = wb.AuxiliaryData.GetData<WorksheetDefinition>(PlugInUUID.WorkbookReader, PlugInUUID.WorksheetDefinitionEntity, worksheetVisualIndex)) != null)
264 {
265 RelationshipInfo relationship = relationshipCatalog.GetBySourceAndId(workbookPartPath, definition.RelId);
266 if (relationship == null)
267 {
268 throw new IOException("There was an error while reading an XLSX file. The relationship target of the worksheet with the RelID " + definition.RelId + " was not found");
269 }
270 if (relationship.TargetMode != System.IO.Packaging.TargetMode.Internal
271 || !entryLookup.TryGetValue(relationship.ResolvedTargetPath, out ZipArchiveEntry worksheetEntry))
272 {
273 throw new IOException("There was an error while reading an XLSX file. The worksheet entry '" + relationship.ResolvedTargetPath + "' was not found in the archive");
274 }
275 worksheetReader.CurrentWorksheetID = worksheetVisualIndex;
276 if (PlugInLoader.HasQueuePlugins(PlugInUUID.WorksheetInlineReader))
277 {
278 // Inline plugins need a seekable stream; buffer so the handler can reset position
279 MemoryStream wsMs = GetEntryStream(relationship.ResolvedTargetPath, entryLookup);
280 worksheetReader.Init(wsMs, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
281 worksheetReader.Execute();
282 }
283 else
284 {
285 // Direct-stream from ZIP entry — largest single allocation on big files
286 using (Stream worksheetStream = worksheetEntry.Open())
287 {
288 worksheetReader.Init(worksheetStream, wb, readerOptions, ReaderPlugInHandler.HandleInlineQueuePlugins);
289 worksheetReader.Execute();
290 }
291 }
292 worksheetVisualIndex++;
293 }
294 if (wb.Worksheets.Count == 0)
295 {
296 throw new IOException("No worksheet was found in the workbook");
297 }
298 HandleQueuePlugIns(PlugInUUID.ReaderAppendingQueue, entryLookup, relationshipCatalog, ref wb);
299 // finalizing processor(s)
300 IPluginReadProcessor finalizingProcessor = PlugInLoader.GetPlugIn<IPluginReadProcessor>(PlugInUUID.FinalizingProcessor, new FinalizingProcessor());
301 finalizingProcessor.Init(wb, readerOptions, ReaderPlugInHandler.HandleInlineQueueProcessorPlugins);
302 finalizingProcessor.Execute();
303 // Read process is completed
304 wb.importInProgress = false; // Enables checks for runtime
305 wb.AuxiliaryData.ClearTemporaryData(); // Remove temporary staging data
306 this.Workbook = wb;
307 }
308
316 private static MemoryStream GetEntryStream(string name, Dictionary<string, ZipArchiveEntry> entryLookup)
317 {
318 if (!entryLookup.TryGetValue(name, out ZipArchiveEntry entry))
319 {
320 return null;
321 }
322 int capacity = (int)Math.Min(entry.Length, int.MaxValue);
323 MemoryStream ms = new MemoryStream(capacity);
324 using (Stream src = entry.Open())
325 {
326 src.CopyTo(ms);
327 }
328 ms.Position = 0;
329 return ms;
330 }
331
341 private static RelationshipInfo GetRelationship(RelationshipCatalog catalog, string sourcePartPath, string documentType, bool required)
342 {
343 foreach (RelationshipInfo relationship in catalog.GetByType(documentType))
344 {
345 if (relationship.TargetMode == System.IO.Packaging.TargetMode.Internal
346 && string.Equals(relationship.SourcePartPath, sourcePartPath, StringComparison.Ordinal))
347 {
348 return relationship;
349 }
350 }
351 if (required)
352 {
353 string sourceDescription = string.IsNullOrEmpty(sourcePartPath) ? "the package root" : "'" + sourcePartPath + "'";
354 throw new IOException("The required relationship type '" + documentType + "' was not found for " + sourceDescription);
355 }
356 return null;
357 }
358
366 private static ZipArchiveEntry GetRequiredEntry(RelationshipInfo relationship, Dictionary<string, ZipArchiveEntry> entryLookup)
367 {
368 // All callers obtain required relationships through GetRelationship or guard optional
369 // relationships before reaching this helper, so a null value is not a valid call state.
370 if (string.IsNullOrEmpty(relationship.ResolvedTargetPath)
371 || !entryLookup.TryGetValue(relationship.ResolvedTargetPath, out ZipArchiveEntry entry))
372 {
373 throw new IOException("The relationship target entry '" + relationship.ResolvedTargetPath + "' was not found in the archive");
374 }
375 return entry;
376 }
377
384 private static MemoryStream GetRequiredEntryStream(RelationshipInfo relationship, Dictionary<string, ZipArchiveEntry> entryLookup)
385 {
386 GetRequiredEntry(relationship, entryLookup);
387 return GetEntryStream(relationship.ResolvedTargetPath, entryLookup);
388 }
389
396 private static string GetDocumentType(IPluginBaseReader reader, string defaultDocumentType)
397 {
398 IDocumentReader documentReader = reader as IDocumentReader;
399 return documentReader == null || string.IsNullOrEmpty(documentReader.DocumentType)
400 ? defaultDocumentType
401 : documentReader.DocumentType;
402 }
403
409 private static string GetRelationshipPartPath(string sourcePartPath)
410 {
411 Uri sourcePartUri = new Uri("/" + sourcePartPath, UriKind.Relative);
412 Uri relationshipPartUri = System.IO.Packaging.PackUriHelper.GetRelationshipPartUri(sourcePartUri);
413 return relationshipPartUri.OriginalString.TrimStart('/');
414 }
415
423 private void HandleQueuePlugIns(string queueUuid, Dictionary<string, ZipArchiveEntry> entryLookup, RelationshipCatalog relationshipCatalog, ref Workbook workbook)
424 {
425 string lastUuid = null;
426 IPluginQueueReader queueReader;
427 do
428 {
429 string currentUuid;
430 queueReader = PlugInLoader.GetNextQueuePlugIn<IPluginQueueReader>(queueUuid, lastUuid, out currentUuid);
431 MemoryStream ms = null;
432 if (queueReader != null)
433 {
434 IDiscoveryPackageReader discoveryPackageReader = queueReader as IDiscoveryPackageReader;
435 if (discoveryPackageReader != null)
436 {
437 foreach (RelationshipInfo relationship in relationshipCatalog.GetByType(discoveryPackageReader.DocumentType))
438 {
439 if (relationship.TargetMode != System.IO.Packaging.TargetMode.Internal
440 || string.IsNullOrEmpty(relationship.ResolvedTargetPath)
441 || !entryLookup.ContainsKey(relationship.ResolvedTargetPath))
442 {
443 continue; // Internal, invalid or not discovered
444 }
445 discoveryPackageReader.CurrentRelationship = relationship;
446 ms = GetEntryStream(relationship.ResolvedTargetPath, entryLookup);
447 discoveryPackageReader.Init(ms, workbook, this.readerOptions, null);
448 discoveryPackageReader.Execute();
449 }
450 lastUuid = currentUuid;
451 continue;
452 }
453 if (queueReader is IPluginPackageReader)
454 {
455 string streamPartName = (queueReader as IPluginPackageReader).StreamEntryName;
456 if (!string.IsNullOrEmpty(streamPartName))
457 {
458 ms = GetEntryStream(streamPartName, entryLookup);
459 if (ms == null)
460 {
461 lastUuid = currentUuid;
462 continue; // Skip if the stream part name was defined but not found
463 }
464 }
465 }
466 queueReader.Init(ms, workbook, this.readerOptions, null); // stream may be null, inlinePluginAction is not used here
467 queueReader.Execute();
468 lastUuid = currentUuid;
469 }
470 else
471 {
472 lastUuid = null;
473 }
474
475 } while (queueReader != null);
476 }
477
481 public void Dispose()
482 {
483 this.inputStream?.Dispose();
484 GC.SuppressFinalize(this);
485 }
486
487 #endregion
488 }
489}
XlsxReader(string path, ReaderOptions options=null)
Constructor with file path as parameter.
Definition XlsxReader.cs:46
XlsxReader(Stream stream, ReaderOptions options=null)
Constructor with stream as parameter.
Definition XlsxReader.cs:57
async Task ReadAsync()
Reads the XLSX file from a file path or a file stream asynchronously.
void Dispose()
Disposes the XlsxReader instance.
void Read()
Reads the XLSX file from a file path or a file stream.
Definition XlsxReader.cs:72
Workbook Workbook
Gets the read workbook.
Definition XlsxReader.cs:37
Exceptions.IOException IOException