NanoXLSX.Formatting 3.2.0
Loading...
Searching...
No Matches
FormattedSharedStringsReader.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.Text;
12using System.Xml;
13using NanoXLSX.Colors;
15using NanoXLSX.Interfaces;
16using NanoXLSX.Interfaces.Reader;
17using NanoXLSX.Registry;
18using NanoXLSX.Registry.Attributes;
19using NanoXLSX.Styles;
20using NanoXLSX.Utils;
21using NanoXLSX.Utils.Xml;
22using IOException = NanoXLSX.Exceptions.IOException;
23
25{
29 [NanoXlsxPlugIn(PlugInUUID = PlugInUUID.SharedStringsReader, PlugInOrder = 1000)]
30 internal class FormattedSharedStringsReader : ISharedStringReader
31 {
35 internal static readonly int AUXILIARY_DATA_ID = 854563987;
36
37 #region privateFields
38 private bool capturePhoneticCharacters;
39 private readonly List<PhoneticInfo> phoneticsInfo;
40 private Stream stream;
41 private Workbook workbook;
42 #endregion
43
44 #region properties
45
52 public List<string> SharedStrings { get; private set; }
53
57 public Dictionary<string, FormattedText> FormattedTexts { get; private set; }
58
62 public Workbook Workbook { get => workbook; set => workbook = value; }
66 public IOptions Options { get; set; }
70 public Action<Stream, Workbook, string, IOptions, int?> InlinePluginHandler { get; set; }
71
75 public string DocumentType { get { return @"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"; } }
76
77 #endregion
78
79 #region constructors
83 public FormattedSharedStringsReader()
84 {
85 phoneticsInfo = new List<PhoneticInfo>();
86 SharedStrings = new List<string>();
87 FormattedTexts = new Dictionary<string, FormattedText>();
88 }
89 #endregion
90
91 #region methods
99 public void Init(Stream stream, Workbook workbook, IOptions readerOptions, Action<Stream, Workbook, string, IOptions, int?> inlinePluginHandler)
100 {
101 this.stream = stream;
102 this.workbook = workbook;
103 if (readerOptions is ITextOptions options)
104 {
105 this.capturePhoneticCharacters = options.EnforcePhoneticCharacterImport;
106 }
107 this.InlinePluginHandler = inlinePluginHandler;
108 }
109
114 public void Execute()
115 {
116 try
117 {
118 using (stream) // Close after processing
119 {
120 bool hasFormattedText = false;
121 StringBuilder sb = new StringBuilder();
122 using (XmlReader reader = XmlReader.Create(stream, XmlStreamUtils.CreateSettings()))
123 {
124 while (reader.Read())
125 {
126 if (!XmlStreamUtils.IsElement(reader, "si"))
127 {
128 continue;
129 }
130
131 sb.Clear();
132 phoneticsInfo.Clear();
133
134 FormattedText formattedText;
135 using (XmlReader siReader = reader.ReadSubtree())
136 {
137 siReader.Read(); // consume the <si> open tag
138 formattedText = ProcessSharedStringItem(siReader, ref sb);
139 }
140
141 string textValue;
142 if (capturePhoneticCharacters)
143 {
144 textValue = ProcessPhoneticCharacters(sb);
145 formattedText.OverridePlainText(textValue);
146 }
147 else if (formattedText != null && string.IsNullOrEmpty(formattedText.PlainText) && sb.Length > 0)
148 {
149 textValue = sb.ToString();
150 formattedText.OverridePlainText(textValue); // Fallback to prevent data loss
151 }
152 else
153 {
154 textValue = sb.ToString();
155 }
156
157 if (formattedText != null)
158 {
159 string key = PlugInUUID.SharedStringsReader + textValue;
160 SharedStrings.Add(key);
161 FormattedTexts[key] = formattedText;
162 hasFormattedText = true;
163 }
164 else
165 {
166 SharedStrings.Add(textValue);
167 }
168 }
169 }
170 InlinePluginHandler?.Invoke(stream, Workbook, PlugInUUID.SharedStringsInlineReader, Options, null);
171 if (hasFormattedText)
172 {
173 Workbook.AuxiliaryData.SetData(PlugInUUID.SharedStringsReader, AUXILIARY_DATA_ID, FormattedTexts);
174 }
175 }
176 }
177 catch (Exception ex)
178 {
179 throw new IOException("The XML entry could not be read from the " + nameof(stream) + ". Please see the inner exception:", ex);
180 }
181 }
182
189 private FormattedText ProcessSharedStringItem(XmlReader siReader, ref StringBuilder sb)
190 {
191 bool hasRuns = false;
192 bool hasFormattedContent = false;
193 FormattedText formattedText = null;
194
195 while (siReader.Read())
196 {
197 if (siReader.NodeType != XmlNodeType.Element)
198 continue;
199
200 if (XmlStreamUtils.IsElement(siReader, "r"))
201 {
202 hasRuns = true;
203 hasFormattedContent = true;
204 if (formattedText == null)
205 formattedText = new FormattedText();
206 using (XmlReader runReader = siReader.ReadSubtree())
207 {
208 runReader.Read(); // consume the <r> open tag
209 ProcessTextRun(runReader, formattedText, ref sb);
210 }
211 }
212 else if (XmlStreamUtils.IsElement(siReader, "rPh"))
213 {
214 hasFormattedContent = true;
215 if (formattedText == null)
216 formattedText = new FormattedText();
217 using (XmlReader rPhReader = siReader.ReadSubtree())
218 {
219 rPhReader.Read(); // consume the <rPh> open tag
220 ProcessPhoneticRun(rPhReader, formattedText);
221 }
222 }
223 else if (XmlStreamUtils.IsElement(siReader, "phoneticPr"))
224 {
225 hasFormattedContent = true;
226 if (formattedText == null)
227 formattedText = new FormattedText();
228 ProcessPhoneticProperties(siReader, formattedText);
229 using (siReader.ReadSubtree()) { } // consume element; positions reader at end element
230 }
231 else if (XmlStreamUtils.IsElement(siReader, "t") && !hasRuns)
232 {
233 // Plain text or text accompanying only phonetic runs (no rich-text runs)
234 string text;
235 using (XmlReader tReader = siReader.ReadSubtree())
236 {
237 tReader.Read(); // position at <t>
238 text = tReader.ReadElementContentAsString();
239 }
240 sb.Append(text);
241 }
242 }
243
244 return hasFormattedContent ? formattedText : null;
245 }
246
253 private void ProcessTextRun(XmlReader runReader, FormattedText formattedText, ref StringBuilder sb)
254 {
255 Font fontStyle = null;
256 string text = null;
257
258 while (runReader.Read())
259 {
260 if (runReader.NodeType != XmlNodeType.Element)
261 continue;
262
263 if (XmlStreamUtils.IsElement(runReader, "rPr"))
264 {
265 using (XmlReader rPrReader = runReader.ReadSubtree())
266 {
267 rPrReader.Read(); // consume the <rPr> open tag
268 fontStyle = ParseRunProperties(rPrReader);
269 }
270 }
271 else if (XmlStreamUtils.IsElement(runReader, "t"))
272 {
273 using (XmlReader tReader = runReader.ReadSubtree())
274 {
275 tReader.Read(); // position at <t>
276 text = tReader.ReadElementContentAsString();
277 }
278 sb.Append(text);
279 }
280 }
281
282 if (!string.IsNullOrEmpty(text))
283 {
284 formattedText.AddRun(text, fontStyle);
285 }
286 }
287
293 private Font ParseRunProperties(XmlReader rPrReader)
294 {
295 Font font = new Font();
296
297 while (rPrReader.Read())
298 {
299 if (rPrReader.NodeType != XmlNodeType.Element)
300 continue;
301
302 string nodeName = rPrReader.LocalName;
303
304 if (nodeName.Equals("rFont", StringComparison.OrdinalIgnoreCase))
305 {
306 font.Name = rPrReader.GetAttribute("val");
307 }
308 else if (nodeName.Equals("charset", StringComparison.OrdinalIgnoreCase))
309 {
310 string val = rPrReader.GetAttribute("val");
311 if (!string.IsNullOrEmpty(val))
312 font.Charset = (Font.CharsetValue)ParserUtils.ParseInt(val);
313 }
314 else if (nodeName.Equals("family", StringComparison.OrdinalIgnoreCase))
315 {
316 string val = rPrReader.GetAttribute("val");
317 if (!string.IsNullOrEmpty(val))
318 font.Family = (Font.FontFamilyValue)ParserUtils.ParseInt(val);
319 }
320 else if (nodeName.Equals("b", StringComparison.OrdinalIgnoreCase))
321 {
322 font.Bold = true;
323 }
324 else if (nodeName.Equals("i", StringComparison.OrdinalIgnoreCase))
325 {
326 font.Italic = true;
327 }
328 else if (nodeName.Equals("strike", StringComparison.OrdinalIgnoreCase))
329 {
330 font.Strike = true;
331 }
332 else if (nodeName.Equals("outline", StringComparison.OrdinalIgnoreCase))
333 {
334 font.Outline = true;
335 }
336 else if (nodeName.Equals("shadow", StringComparison.OrdinalIgnoreCase))
337 {
338 font.Shadow = true;
339 }
340 else if (nodeName.Equals("condense", StringComparison.OrdinalIgnoreCase))
341 {
342 font.Condense = true;
343 }
344 else if (nodeName.Equals("extend", StringComparison.OrdinalIgnoreCase))
345 {
346 font.Extend = true;
347 }
348 else if (nodeName.Equals("color", StringComparison.OrdinalIgnoreCase))
349 {
350 font.ColorValue = ParseColor(rPrReader);
351 }
352 else if (nodeName.Equals("sz", StringComparison.OrdinalIgnoreCase))
353 {
354 string val = rPrReader.GetAttribute("val");
355 if (!string.IsNullOrEmpty(val))
356 font.Size = ParserUtils.ParseFloat(val);
357 }
358 else if (nodeName.Equals("u", StringComparison.OrdinalIgnoreCase))
359 {
360 string val = rPrReader.GetAttribute("val");
361 font.Underline = string.IsNullOrEmpty(val) ? Font.UnderlineValue.Single : ParseUnderlineValue(val);
362 }
363 else if (nodeName.Equals("vertAlign", StringComparison.OrdinalIgnoreCase))
364 {
365 string val = rPrReader.GetAttribute("val");
366 if (!string.IsNullOrEmpty(val))
367 font.VerticalAlign = ParseVerticalAlignValue(val);
368 }
369 else if (nodeName.Equals("scheme", StringComparison.OrdinalIgnoreCase))
370 {
371 string val = rPrReader.GetAttribute("val");
372 if (!string.IsNullOrEmpty(val))
373 font.Scheme = ParseSchemeValue(val);
374 }
375 }
376
377 return font;
378 }
379
385 private Color ParseColor(XmlReader reader)
386 {
387 string autoValue = reader.GetAttribute("auto");
388 string indexedValue = reader.GetAttribute("indexed");
389 string rgbValue = reader.GetAttribute("rgb");
390 string themeValue = reader.GetAttribute("theme");
391 string systemValue = reader.GetAttribute("system");
392 string tintValue = reader.GetAttribute("tint");
393
394 Color color = null;
395
396 if (!string.IsNullOrEmpty(autoValue))
397 color = Color.CreateAuto();
398 else if (!string.IsNullOrEmpty(indexedValue))
399 color = Color.CreateIndexed(ParserUtils.ParseInt(indexedValue));
400 else if (!string.IsNullOrEmpty(rgbValue))
401 color = Color.CreateRgb(rgbValue);
402 else if (!string.IsNullOrEmpty(themeValue))
403 color = Color.CreateTheme(ParserUtils.ParseInt(themeValue));
404 else if (!string.IsNullOrEmpty(systemValue))
405 color = Color.CreateSystem(SystemColor.MapStringToValue(systemValue));
406
407 if (color != null && !string.IsNullOrEmpty(tintValue))
408 color.Tint = ParserUtils.ParseFloat(tintValue);
409
410 return color;
411 }
412
418 private void ProcessPhoneticRun(XmlReader rPhReader, FormattedText formattedText)
419 {
420 string startBase = rPhReader.GetAttribute("sb");
421 string endBase = rPhReader.GetAttribute("eb");
422 string text = null;
423
424 while (rPhReader.Read())
425 {
426 if (rPhReader.NodeType != XmlNodeType.Element)
427 continue;
428
429 if (XmlStreamUtils.IsElement(rPhReader, "t"))
430 {
431 using (XmlReader tReader = rPhReader.ReadSubtree())
432 {
433 tReader.Read(); // position at <t>
434 text = tReader.ReadElementContentAsString();
435 }
436 }
437 }
438
439 if (!string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(startBase) && !string.IsNullOrEmpty(endBase))
440 {
441 uint sb = (uint)ParserUtils.ParseInt(startBase);
442 uint eb = (uint)ParserUtils.ParseInt(endBase);
443 formattedText.AddPhoneticRun(text, sb, eb);
444
445 if (capturePhoneticCharacters)
446 {
447 phoneticsInfo.Add(new PhoneticInfo(text, startBase, endBase));
448 }
449 }
450 }
451
457 private void ProcessPhoneticProperties(XmlReader reader, FormattedText formattedText)
458 {
459 string typeValue = reader.GetAttribute("type");
460 string alignmentValue = reader.GetAttribute("alignment");
461
462 PhoneticRun.PhoneticType type = PhoneticRun.PhoneticType.FullwidthKatakana;
463 if (!string.IsNullOrEmpty(typeValue))
464 type = ParsePhoneticType(typeValue);
465
467 if (!string.IsNullOrEmpty(alignmentValue))
468 alignment = ParsePhoneticAlignment(alignmentValue);
469
470 formattedText.SetPhoneticProperties(new Font(), type, alignment);
471 }
472
478 private string ProcessPhoneticCharacters(StringBuilder sb)
479 {
480 string text = sb.ToString();
481 StringBuilder sb2 = new StringBuilder();
482 int currentTextIndex = 0;
483 foreach (PhoneticInfo info in phoneticsInfo)
484 {
485 sb2.Append(text.Substring(currentTextIndex, info.StartIndex + info.Length - currentTextIndex));
486 sb2.Append('(').Append(info.Value).Append(')');
487 currentTextIndex = info.StartIndex + info.Length;
488 }
489 sb2.Append(text.Substring(currentTextIndex));
490
491 return sb2.ToString();
492 }
493
497 private Font.UnderlineValue ParseUnderlineValue(string value)
498 {
499 switch (value.ToLowerInvariant())
500 {
501 case "double":
502 return Font.UnderlineValue.Double;
503 case "singleaccounting":
504 return Font.UnderlineValue.SingleAccounting;
505 case "doubleaccounting":
506 return Font.UnderlineValue.DoubleAccounting;
507 default:
508 return Font.UnderlineValue.Single;
509 }
510 }
511
515 private Font.VerticalTextAlignValue ParseVerticalAlignValue(string value)
516 {
517 switch (value.ToLowerInvariant())
518 {
519 case "superscript":
520 return Font.VerticalTextAlignValue.Superscript;
521 case "subscript":
522 return Font.VerticalTextAlignValue.Subscript;
523 default:
524 return Font.VerticalTextAlignValue.Baseline;
525 }
526 }
527
531 private Font.SchemeValue ParseSchemeValue(string value)
532 {
533 switch (value.ToLowerInvariant())
534 {
535 case "major":
536 return Font.SchemeValue.Major;
537 case "minor":
538 return Font.SchemeValue.Minor;
539 default:
540 return Font.SchemeValue.None;
541 }
542 }
543
547 private PhoneticRun.PhoneticType ParsePhoneticType(string value)
548 {
549 switch (value.ToLowerInvariant())
550 {
551 case "halfwidthkatakana":
552 return PhoneticRun.PhoneticType.HalfwidthKatakana;
553 case "hiragana":
554 return PhoneticRun.PhoneticType.Hiragana;
555 case "noconversion":
556 return PhoneticRun.PhoneticType.NoConversion;
557 default:
558 return PhoneticRun.PhoneticType.FullwidthKatakana;
559 }
560 }
561
565 private PhoneticRun.PhoneticAlignment ParsePhoneticAlignment(string value)
566 {
567 switch (value.ToLowerInvariant())
568 {
569 case "nocontrol":
570 return PhoneticRun.PhoneticAlignment.NoControl;
571 case "center":
572 return PhoneticRun.PhoneticAlignment.Center;
573 case "distributed":
574 return PhoneticRun.PhoneticAlignment.Distributed;
575 default:
576 return PhoneticRun.PhoneticAlignment.Left;
577 }
578 }
579
580 #endregion
581
582 #region sub-classes
587 sealed class PhoneticInfo
588 {
592 public string Value { get; private set; }
596 public int StartIndex { get; private set; }
600 public int Length { get; private set; }
601
608 public PhoneticInfo(string value, string start, string end)
609 {
610 Value = value;
611 StartIndex = ParserUtils.ParseInt(start);
612 Length = ParserUtils.ParseInt(end) - StartIndex;
613
614 }
615 }
616 #endregion
617 }
618}
Represents a phonetic run that provides pronunciation guidance for text.
PhoneticAlignment
Enumeration for phonetic text alignment.
PhoneticType
Enumeration for phonetic text types.
Represents a formatted text entry in Excel shared strings, supporting rich text with multiple runs an...
override string ToString()
Gets the string representation of the formatted text without formatting (plain text)....
FormattedText AddPhoneticRun(string text, uint startBase, uint endBase)
Adds a phonetic run for pronunciation guidance (Ruby text, like Furigana, Pinyin or Zhuyin).
string PlainText
Gets the plain text content by concatenating all runs.
FormattedText AddRun(string text, Font fontStyle=null)
Adds a text run with the specified style.
FormattedText SetPhoneticProperties(Font fontReference, PhoneticRun.PhoneticType type=PhoneticRun.PhoneticType.FullwidthKatakana, PhoneticRun.PhoneticAlignment alignment=PhoneticRun.PhoneticAlignment.Left)
Sets the phonetic properties for this formatted text, applied to the phonetic run (Ruby text).