.Shared.Return(readBuf);
+ }
+ }
+
+ private static CT_PhoneticPr ParsePhoneticPrAttributes(XmlReader reader)
+ {
+ var pr = new CT_PhoneticPr();
+ string fontId = reader.GetAttribute("fontId");
+ if (fontId != null && uint.TryParse(fontId, out uint fid)) pr.fontId = fid;
+ string type = reader.GetAttribute("type");
+ if (type != null && Enum.TryParse(type, out ST_PhoneticType pt)) pr.type = pt;
+ string alignment = reader.GetAttribute("alignment");
+ if (alignment != null && Enum.TryParse(alignment, out ST_PhoneticAlignment pa)) pr.alignment = pa;
+ return pr;
+ }
+
+ private static void ParseRPrChild(XmlReader reader, string localName, CT_RPrElt rPr)
+ {
+ switch (localName)
+ {
+ case "sz":
+ {
+ string val = reader.GetAttribute("val");
+ if (val != null && double.TryParse(val, NumberStyles.Any, CultureInfo.InvariantCulture, out double sz))
+ rPr.sz = new CT_FontSize { val = sz };
+ break;
+ }
+ case "color":
+ rPr.color = ParseColorAttributes(reader);
+ break;
+ case "rFont":
+ {
+ string val = reader.GetAttribute("val");
+ if (val != null) rPr.rFont = new CT_FontName { val = val };
+ break;
+ }
+ case "family":
+ {
+ string val = reader.GetAttribute("val");
+ if (val != null && int.TryParse(val, out int fam))
+ rPr.family = new CT_IntProperty { val = fam };
+ break;
+ }
+ case "charset":
+ {
+ string val = reader.GetAttribute("val");
+ if (val != null && int.TryParse(val, out int cs))
+ rPr.charset = new CT_IntProperty { val = cs };
+ break;
+ }
+ case "b":
+ rPr.b = ParseBoolProp(reader);
+ break;
+ case "i":
+ rPr.i = ParseBoolProp(reader);
+ break;
+ case "strike":
+ rPr.strike = ParseBoolProp(reader);
+ break;
+ case "outline":
+ rPr.outline = ParseBoolProp(reader);
+ break;
+ case "shadow":
+ rPr.shadow = ParseBoolProp(reader);
+ break;
+ case "condense":
+ rPr.condense = ParseBoolProp(reader);
+ break;
+ case "extend":
+ rPr.extend = ParseBoolProp(reader);
+ break;
+ case "u":
+ {
+ string val = reader.GetAttribute("val") ?? "single";
+ if (Enum.TryParse(val, out ST_UnderlineValues uv))
+ rPr.u = new CT_UnderlineProperty { val = uv };
+ break;
+ }
+ case "vertAlign":
+ {
+ string val = reader.GetAttribute("val");
+ if (val != null && Enum.TryParse(val, out ST_VerticalAlignRun va))
+ rPr.vertAlign = new CT_VerticalAlignFontProperty { val = va };
+ break;
+ }
+ case "scheme":
+ {
+ string val = reader.GetAttribute("val");
+ if (val != null && Enum.TryParse(val, out ST_FontScheme fs))
+ rPr.scheme = new CT_FontScheme { val = fs };
+ break;
+ }
+ }
+ }
+
+ private static CT_BooleanProperty ParseBoolProp(XmlReader reader)
+ {
+ string val = reader.GetAttribute("val");
+ // Default for boolean property is true when attribute is absent
+ bool v = val == null || (val != "0" && !val.Equals("false", StringComparison.OrdinalIgnoreCase));
+ return new CT_BooleanProperty { val = v };
+ }
+
+ private static CT_Color ParseColorAttributes(XmlReader reader)
+ {
+ var color = new CT_Color();
+ string auto = reader.GetAttribute("auto");
+ if (auto != null)
+ color.auto = auto != "0" && !auto.Equals("false", StringComparison.OrdinalIgnoreCase);
+ string indexed = reader.GetAttribute("indexed");
+ if (indexed != null && uint.TryParse(indexed, out uint idx)) color.indexed = idx;
+ string rgb = reader.GetAttribute("rgb");
+ if (rgb != null && rgb.Length >= 2)
+ {
+ byte[] bytes = new byte[rgb.Length / 2];
+ for (int i = 0; i < bytes.Length; i++)
+ bytes[i] = Convert.ToByte(rgb.Substring(i * 2, 2), 16);
+ color.rgb = bytes;
+ }
+ string theme = reader.GetAttribute("theme");
+ if (theme != null && uint.TryParse(theme, out uint th)) color.theme = th;
+ string tint = reader.GetAttribute("tint");
+ if (tint != null && double.TryParse(tint, NumberStyles.Any, CultureInfo.InvariantCulture, out double t)) color.tint = t;
+ return color;
+ }
+
+ ///
+ /// Read shared strings from a stream. Kept for backward compatibility; internally
+ /// delegates to the streaming parser.
+ ///
+ public void ReadFrom(Stream is1)
+ {
+ try
+ {
+ ReadFromStream(is1);
+ _loaded = true;
+ _stmapBuilt = false;
+ }
catch (XmlException e)
{
throw new IOException("unable to parse shared strings table", e);
@@ -139,6 +531,7 @@ private static String GetKey(CT_Rst st)
*/
public CT_Rst GetEntryAt(int idx)
{
+ EnsureLoaded();
return strings[idx];
}
@@ -152,6 +545,7 @@ public int Count
{
get
{
+ EnsureLoaded();
return count;
}
}
@@ -167,16 +561,17 @@ public int UniqueCount
{
get
{
+ EnsureLoaded();
return uniqueCount;
}
}
/**
- * Add an entry to this Shared String table (a new value is appened to the end).
+ * Add an entry to this Shared String table (a new value is appended to the end).
*
*
* If the Shared String table already Contains this CT_Rst bean, its index is returned.
- * Otherwise a new entry is aded.
+ * Otherwise a new entry is added.
*
*
* @param st the entry to add
@@ -184,6 +579,8 @@ public int UniqueCount
*/
public int AddEntry(CT_Rst st)
{
+ EnsureLoaded();
+ EnsureStmapBuilt();
String s = GetKey(st);
count++;
if (stmap.TryGetValue(s, out int entry))
@@ -199,8 +596,10 @@ public int AddEntry(CT_Rst st)
int idx = strings.Count;
stmap[s] = idx;
strings.Add(newSt);
+ _dirty = true;
return idx;
}
+
/**
* Provide low-level access to the underlying array of CT_Rst beans
*
@@ -210,36 +609,50 @@ public IList Items
{
get
{
+ EnsureLoaded();
return strings.AsReadOnly();
}
}
/**
- *
- * this table out as XML.
- *
+ * Write this table out as XML.
+ *
* @param out The stream to write to.
* @throws IOException if an error occurs while writing.
*/
public void WriteTo(Stream out1)
{
- // the following two lines turn off writing CDATA
- // see Bugzilla 48936
- //options.SetSaveCDataLengthThreshold(1000000);
- //options.SetSaveCDataEntityCountThreshold(-1);
+ EnsureLoaded();
CT_Sst sst = _sstDoc.GetSst();
sst.count = count;
- sst.uniqueCount = uniqueCount;
-
- //re-create the sst table every time saving a workbook
- _sstDoc.Save(out1);
+ sst.uniqueCount = uniqueCount;
+ _sstDoc.Save(out1);
}
+ ///
+ /// Returns true if the SST has been parsed from its backing part.
+ /// Used in tests to verify lazy-load behaviour.
+ ///
+ internal bool IsLoaded => _loaded;
+
+ ///
+ /// Prepares the part for commit. No-op when SST has not been modified,
+ /// preserving the original part bytes.
+ ///
+ protected internal override void PrepareForCommit()
+ {
+ if (_dirty)
+ base.PrepareForCommit();
+ }
+ ///
+ /// Commits the SST to the package. No-op when SST has not been modified,
+ /// so the original sharedStrings.xml bytes are preserved without parsing.
+ ///
protected internal override void Commit()
{
+ if (!_dirty) return;
PackagePart part = GetPackagePart();
- //Stream out1 = part.GetInputStream();
Stream out1 = part.GetOutputStream();
WriteTo(out1);
out1.Close();
diff --git a/testcases/ooxml/XSSF/Model/TestSharedStringsTable.cs b/testcases/ooxml/XSSF/Model/TestSharedStringsTable.cs
index f93a0996b8..9d596a265c 100644
--- a/testcases/ooxml/XSSF/Model/TestSharedStringsTable.cs
+++ b/testcases/ooxml/XSSF/Model/TestSharedStringsTable.cs
@@ -191,6 +191,116 @@ private List ReadStrings(String filename)
return strs;
}
+ ///
+ /// Verify that opening a workbook and writing it without accessing shared
+ /// strings does not cause the SST to be parsed or rewritten.
+ ///
+ [Test]
+ public void TestLazyLoadNotTriggeredByWrite()
+ {
+ XSSFWorkbook wb = XSSFTestDataSamples.OpenSampleWorkbook("sample.xlsx");
+ SharedStringsTable sst = wb.GetSharedStringSource();
+
+ // SST should NOT be loaded yet
+ ClassicAssert.IsFalse(sst.IsLoaded, "SST should not be loaded before any access");
+
+ // Write the workbook without touching any string cells
+ byte[] writtenBytes;
+ using (MemoryStream ms = new MemoryStream())
+ {
+ wb.Write(ms, false);
+ writtenBytes = ms.ToArray();
+ }
+
+ // SST should still be unloaded (not dirty, not accessed)
+ ClassicAssert.IsFalse(sst.IsLoaded, "SST should still not be loaded after Write() without string access");
+
+ // The written workbook should still contain the correct SST
+ XSSFWorkbook wb2 = new XSSFWorkbook(new MemoryStream(writtenBytes));
+ SharedStringsTable sst2 = wb2.GetSharedStringSource();
+
+ // Now access to force load
+ ClassicAssert.IsTrue(sst2.Count > 0, "Written workbook should have preserved the SST");
+ ClassicAssert.IsTrue(sst2.Items.Count > 0);
+
+ wb.Close();
+ wb2.Close();
+ }
+
+ ///
+ /// Verify that reading SST content marks it loaded but not dirty,
+ /// and that saving preserves the original SST data without re-serializing.
+ ///
+ [Test]
+ public void TestReadSstNotDirtyAfterAccess()
+ {
+ XSSFWorkbook wb1 = XSSFTestDataSamples.OpenSampleWorkbook("sample.xlsx");
+ SharedStringsTable sst1 = wb1.GetSharedStringSource();
+
+ // Access SST – this should load but not dirty it
+ int origCount = sst1.Count;
+ int origUnique = sst1.UniqueCount;
+ IList origItems = sst1.Items;
+ ClassicAssert.IsTrue(sst1.IsLoaded, "SST should be loaded after Count access");
+
+ // Round-trip: write + read back
+ XSSFWorkbook wb2 = XSSFTestDataSamples.WriteOutAndReadBack(wb1);
+ SharedStringsTable sst2 = wb2.GetSharedStringSource();
+
+ ClassicAssert.AreEqual(origCount, sst2.Count);
+ ClassicAssert.AreEqual(origUnique, sst2.UniqueCount);
+ ClassicAssert.AreEqual(origItems.Count, sst2.Items.Count);
+ for (int i = 0; i < origItems.Count; i++)
+ ClassicAssert.AreEqual(origItems[i].ToString(), sst2.Items[i].ToString());
+
+ wb1.Close();
+ wb2.Close();
+ }
+
+ ///
+ /// Verify that rich text runs and phonetic runs in 51519.xlsx are parsed
+ /// correctly by the streaming parser and survive a round-trip write + read.
+ ///
+ [Test]
+ public void TestPhoneticAndRichTextFidelity()
+ {
+ POIDataSamples ssTests = POIDataSamples.GetSpreadSheetInstance();
+ XSSFWorkbook wb = new XSSFWorkbook(ssTests.OpenResourceAsStream("51519.xlsx"));
+ SharedStringsTable sst = wb.GetSharedStringSource();
+
+ ClassicAssert.AreEqual(49, sst.Items.Count, "Expected 49 shared strings in 51519.xlsx");
+
+ // Entry 0: plain Japanese text (no rich runs)
+ CT_Rst entry0 = sst.GetEntryAt(0);
+ ClassicAssert.AreEqual("\u30B3\u30E1\u30F3\u30C8",
+ new XSSFRichTextString(entry0).ToString(),
+ "Entry 0 text mismatch");
+
+ // Entry 3: should have phonetic runs (rPh elements)
+ CT_Rst entry3 = sst.GetEntryAt(3);
+ ClassicAssert.IsNotNull(entry3.rPh, "Entry 3 should have phonetic runs");
+ ClassicAssert.IsTrue(entry3.rPh.Count > 0, "Entry 3 should have at least one phonetic run");
+
+ // Round-trip: write + read back
+ XSSFWorkbook wb2 = XSSFTestDataSamples.WriteOutAndReadBack(wb);
+ SharedStringsTable sst2 = wb2.GetSharedStringSource();
+
+ ClassicAssert.AreEqual(49, sst2.Items.Count, "Round-tripped SST should still have 49 entries");
+
+ CT_Rst entry0rt = sst2.GetEntryAt(0);
+ ClassicAssert.AreEqual("\u30B3\u30E1\u30F3\u30C8",
+ new XSSFRichTextString(entry0rt).ToString(),
+ "Entry 0 text mismatch after round-trip");
+
+ CT_Rst entry3rt = sst2.GetEntryAt(3);
+ ClassicAssert.IsNotNull(entry3rt.rPh, "Entry 3 should have phonetic runs after round-trip");
+ ClassicAssert.AreEqual(entry3.rPh.Count, entry3rt.rPh.Count,
+ "Phonetic run count should match after round-trip");
+
+ wb.Close();
+ wb2.Close();
+ }
+
}
}