From d372f3e72b6b2eda74886981bf060aa455e51bca Mon Sep 17 00:00:00 2001 From: Antony Liu Date: Tue, 5 Aug 2025 19:08:00 +0800 Subject: [PATCH 1/3] Add NSax for event user model --- Directory.Packages.props | 3 ++- ooxml/NPOI.OOXML.Core.csproj | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9e20d6f26..c6bc5d70b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,7 @@ + @@ -26,4 +27,4 @@ - + \ No newline at end of file diff --git a/ooxml/NPOI.OOXML.Core.csproj b/ooxml/NPOI.OOXML.Core.csproj index 08eca7f48..9b6a9de1f 100644 --- a/ooxml/NPOI.OOXML.Core.csproj +++ b/ooxml/NPOI.OOXML.Core.csproj @@ -26,6 +26,7 @@ + From 9eb9c9d6681d9e91f6d1561650ad2eb3ebd11db3 Mon Sep 17 00:00:00 2001 From: Antony Liu Date: Tue, 5 Aug 2025 19:35:39 +0800 Subject: [PATCH 2/3] Port poi code about event user model --- ooxml/SS/Converter/ExcelToHtmlConverter.cs | 6 +- .../ReadOnlySharedStringsTable.cs | 308 +++++++++ ooxml/XSSF/EventUserModel/XSSFReader.cs | 502 ++++++++++++++ .../EventUserModel/XSSFSheetXMLHandler.cs | 611 ++++++++++++++++++ ooxml/XSSF/Model/CommentsTable.cs | 10 +- ooxml/XSSF/Model/StylesTable.cs | 38 +- ooxml/XSSF/UserModel/XSSFFont.cs | 2 +- ooxml/XSSF/UserModel/XSSFRichTextString.cs | 2 +- ooxml/XSSF/UserModel/XSSFSheet.cs | 6 +- ooxml/XSSF/UserModel/XSSFWorkbook.cs | 6 +- .../ooxml/NPOI.OOXML.TestCases.Core.csproj | 2 +- .../TestReadOnlySharedStringsTable.cs | 125 ++++ .../XSSF/EventUserModel/TestXSSFReader.cs | 353 ++++++++++ .../ooxml/XSSF/Model/TestCommentsTable.cs | 10 +- testcases/ooxml/XSSF/Model/TestStylesTable.cs | 2 +- testcases/ooxml/XSSF/Model/TestThemesTable.cs | 6 +- .../ooxml/XSSF/UserModel/TestXSSFBugs.cs | 6 +- .../ooxml/XSSF/UserModel/TestXSSFComment.cs | 10 +- .../ooxml/XSSF/UserModel/TestXSSFWorkbook.cs | 4 +- testcases/test-data/spreadsheet/60825.xlsx | Bin 0 -> 6524 bytes testcases/test-data/spreadsheet/61034.xlsx | Bin 0 -> 32774 bytes 21 files changed, 1950 insertions(+), 59 deletions(-) create mode 100644 ooxml/XSSF/EventUserModel/ReadOnlySharedStringsTable.cs create mode 100644 ooxml/XSSF/EventUserModel/XSSFReader.cs create mode 100644 ooxml/XSSF/EventUserModel/XSSFSheetXMLHandler.cs create mode 100644 testcases/ooxml/XSSF/EventUserModel/TestReadOnlySharedStringsTable.cs create mode 100644 testcases/ooxml/XSSF/EventUserModel/TestXSSFReader.cs create mode 100644 testcases/test-data/spreadsheet/60825.xlsx create mode 100644 testcases/test-data/spreadsheet/61034.xlsx diff --git a/ooxml/SS/Converter/ExcelToHtmlConverter.cs b/ooxml/SS/Converter/ExcelToHtmlConverter.cs index e262b11f3..ed259c4a2 100644 --- a/ooxml/SS/Converter/ExcelToHtmlConverter.cs +++ b/ooxml/SS/Converter/ExcelToHtmlConverter.cs @@ -811,7 +811,7 @@ private static void BuildStyle_Border(IWorkbook workbook, StringBuilder style, S var stylesSource = ((XSSFWorkbook) workbook).GetStylesSource(); if (stylesSource != null) { - var theme = stylesSource.GetTheme(); + var theme = stylesSource.Theme; if (theme != null) color = theme.GetThemeColor(borderColor); } @@ -853,9 +853,9 @@ private static void BuildStyle_Font(IWorkbook workbook, StringBuilder style, IFo { StylesTable st = ((XSSFWorkbook)workbook).GetStylesSource(); XSSFColor fontColor = null; - if (st != null && st.GetTheme() != null) + if (st != null && st.Theme != null) { - fontColor = st.GetTheme().GetThemeColor(font.Color); + fontColor = st.Theme.GetThemeColor(font.Color); } else { diff --git a/ooxml/XSSF/EventUserModel/ReadOnlySharedStringsTable.cs b/ooxml/XSSF/EventUserModel/ReadOnlySharedStringsTable.cs new file mode 100644 index 000000000..66e13e8e3 --- /dev/null +++ b/ooxml/XSSF/EventUserModel/ReadOnlySharedStringsTable.cs @@ -0,0 +1,308 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +namespace NPOI.XSSF.EventUserModel +{ + using NPOI.OpenXml4Net.OPC; + using NPOI.XSSF.UserModel; + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using NSAX; + using NSAX.Helpers; + + /// + /// + /// + /// + /// This is a lightweight way to process the Shared Strings + /// table. Most of the text cells will reference something + /// from in here. + /// + /// + /// Note that each SI entry can have multiple T elements, if the + /// string is made up of bits with different formatting. + /// + /// + /// Example input: + /// + /// <?xml version="1.0" encoding="UTF-8" standalone="yes" ?> + /// <sst xmlns="http://schemas.Openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2"> + /// <si> + /// <r> + /// <rPr> + /// <b /> + /// <sz val="11" /> + /// <color theme="1" /> + /// <rFont val="Calibri" /> + /// <family val="2" /> + /// <scheme val="minor" /> + /// </rPr> + /// <t>This:</t> + /// </r> + /// <r> + /// <rPr> + /// <sz val="11" /> + /// <color theme="1" /> + /// <rFont val="Calibri" /> + /// <family val="2" /> + /// <scheme val="minor" /> + /// </rPr> + /// <t xml:space="preserve">Causes Problems</t> + /// </r> + /// </si> + /// <si> + /// <t>This does not</t> + /// </si> + /// </sst> + /// + /// + /// + public class ReadOnlySharedStringsTable : DefaultHandler + { + + private bool includePhoneticRuns; + /// + /// An integer representing the total count of strings in the workbook. This count does not + /// include any numbers, it counts only the total of text strings in the workbook. + /// + private int count; + + /// + /// An integer representing the total count of unique strings in the Shared String Table. + /// A string is unique even if it is a copy of another string, but has different formatting applied + /// at the character level. + /// + private int uniqueCount; + + /// + /// The shared strings table. + /// + private List strings; + + /// + /// Map of phonetic strings (if they exist) indexed + /// with the integer matching the index in strings + /// + private Dictionary phoneticStrings; + + /// + /// Calls with + /// a value of true for including phonetic runs + /// + /// The to use as basis for the shared-strings table. + /// If reading the data from the package fails. + /// if parsing the XML data fails. + public ReadOnlySharedStringsTable(OPCPackage pkg) + : this(pkg, true) + { + } + + /// + /// + /// The to use as basis for the shared-strings table. + /// whether or not to concatenate phoneticRuns onto the shared string + /// IOException If reading the data from the package fails. + /// SAXException if parsing the XML data fails. + /// @since POI 3.14-Beta3 + public ReadOnlySharedStringsTable(OPCPackage pkg, bool includePhoneticRuns) + { + this.includePhoneticRuns = includePhoneticRuns; + List parts = + pkg.GetPartsByContentType(XSSFRelation.SHARED_STRINGS.ContentType); + + // Some workbooks have no shared strings table. + if (parts.Count > 0) + { + PackagePart sstPart = parts[0]; + ReadFrom(sstPart.GetInputStream()); + } + } + + /// + /// + /// Like POIXMLDocumentPart constructor + /// + /// + /// Calls , with a + /// value of true to include phonetic runs. + /// + /// + /// @since POI 3.14-Beta1 + public ReadOnlySharedStringsTable(PackagePart part) + : this(part, true) + { + } + + /// + /// Like POIXMLDocumentPart constructor + /// + /// @since POI 3.14-Beta3 + public ReadOnlySharedStringsTable(PackagePart part, bool includePhoneticRuns) + + { + + this.includePhoneticRuns = includePhoneticRuns; + ReadFrom(part.GetInputStream()); + } + + /// + /// Read this shared strings table from an XML file. + /// + /// The input stream containing the XML document. + /// if an error occurs while reading. + /// if parsing the XML data fails. + public void ReadFrom(Stream is1) + { + // test if the file is empty, otherwise parse it + //PushbackInputStream pis = new PushbackInputStream(is1, 1); + //int emptyTest = pis.Read(); + //if (emptyTest > -1) + if(is1.Length > 0) + { + //pis.Unread(emptyTest); + InputSource sheetSource = new InputSource(is1); + //try + { + NSAX.AElfred.SAXDriver sheetParser = new NSAX.AElfred.SAXDriver(); + sheetParser.ContentHandler = (this); + sheetParser.Parse(sheetSource); + } + //catch (ParserConfigurationException e) + //{ + // throw new RuntimeException("SAX parser appears to be broken - " + e.GetMessage()); + //} + } + } + + /// + /// Return an integer representing the total count of strings in the workbook. This count does not + /// include any numbers, it counts only the total of text strings in the workbook. + /// + /// the total count of strings in the workbook + public int Count => count; + + /// + /// Returns an integer representing the total count of unique strings in the Shared String Table. + /// A string is unique even if it is a copy of another string, but has different formatting applied + /// at the character level. + /// + /// the total count of unique strings in the workbook + public int UniqueCount => uniqueCount; + + /// + /// Return the string at a given index. + /// Formatting is ignored. + /// + /// index of item to return. + /// the item at the specified position in this Shared String table. + public String GetEntryAt(int idx) + { + return strings[idx]; + } + + public List Items => strings; + + + //// ContentHandler methods //// + + private StringBuilder characters; + private bool tIsOpen; + private bool inRPh; + + public override void StartElement(String uri, String localName, String name, + IAttributes attributes) + { + + if (uri != null && !uri.Equals(XSSFRelation.NS_SPREADSHEETML)) + { + return; + } + + if ("sst".Equals(localName)) + { + String count = attributes.GetValue("count"); + if (count != null) this.count = Int32.Parse(count); + String uniqueCount = attributes.GetValue("uniqueCount"); + if (uniqueCount != null) this.uniqueCount = Int32.Parse(uniqueCount); + + this.strings = new List(this.uniqueCount); + this.phoneticStrings = new Dictionary(); + characters = new StringBuilder(); + } + else if ("si".Equals(localName)) + { + characters.Length = 0; + } + else if ("t".Equals(localName)) + { + tIsOpen = true; + } + else if ("rPh".Equals(localName)) + { + inRPh = true; + //append space...this assumes that rPh always comes After regular + if (includePhoneticRuns && characters.Length > 0) + { + characters.Append(" "); + } + } + } + + public override void EndElement(String uri, String localName, String name) + + { + + if (uri != null && !uri.Equals(XSSFRelation.NS_SPREADSHEETML)) + { + return; + } + + if ("si".Equals(localName)) + { + strings.Add(characters.ToString()); + } + else if ("t".Equals(localName)) + { + tIsOpen = false; + } + else if ("rPh".Equals(localName)) + { + inRPh = false; + } + } + + /// + /// Captures characters only if a t(ext) element is open. + /// + public override void Characters(char[] ch, int start, int length) + { + if (tIsOpen) + { + if (inRPh && includePhoneticRuns) + { + characters.Append(ch, start, length); + } + else if (!inRPh) + { + characters.Append(ch, start, length); + } + } + } + } +} + diff --git a/ooxml/XSSF/EventUserModel/XSSFReader.cs b/ooxml/XSSF/EventUserModel/XSSFReader.cs new file mode 100644 index 000000000..cc27d17fd --- /dev/null +++ b/ooxml/XSSF/EventUserModel/XSSFReader.cs @@ -0,0 +1,502 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace NPOI.XSSF.EventUserModel +{ + + using NPOI; + using NPOI.OpenXml4Net.Exceptions; + using NPOI.OpenXml4Net.OPC; + using NPOI.Util; + using NPOI.XSSF.Model; + using NPOI.XSSF.UserModel; + using NSAX; + using NSAX.Helpers; + using System.Xml; + + /// + /// This class makes it easy to Get at individual parts + /// of an OOXML .xlsx file, suitable for low memory sax + /// parsing or similar. + /// It makes up the core part of the EventUserModel support + /// for XSSF. + /// + public class XSSFReader + { + + private static ISet WORKSHEET_RELS = + new HashSet( + Arrays.AsList(new String[]{ + XSSFRelation.WORKSHEET.Relation, + XSSFRelation.CHARTSHEET.Relation, + }) + ); + //private static POILogger LOGGER = POILogFactory.GetLogger(XSSFReader.class); + + protected OPCPackage pkg; + protected PackagePart workbookPart; + + /// + /// Creates a new XSSFReader, for the given package + /// + public XSSFReader(OPCPackage pkg) + { + + this.pkg = pkg; + + PackageRelationship coreDocRelationship = this.pkg.GetRelationshipsByType( + PackageRelationshipTypes.CORE_DOCUMENT).GetRelationship(0); + + // strict OOXML likely not fully supported, see #57699 + // this code is similar to POIXMLDocumentPart.PartFromOPCPackage, but I could not combine it + // easily due to different return values + if (coreDocRelationship == null) + { + if (this.pkg.GetRelationshipsByType( + PackageRelationshipTypes.STRICT_CORE_DOCUMENT).GetRelationship(0) != null) + { + throw new POIXMLException("Strict OOXML isn't currently supported, please see bug #57699"); + } + + throw new POIXMLException("OOXML file structure broken/invalid - no core document found!"); + } + + // Get the part that holds the workbook + workbookPart = this.pkg.GetPart(coreDocRelationship); + } + + + /// + /// Opens up the Shared Strings Table, parses it, and + /// returns a handy object for working with + /// shared strings. + /// + public SharedStringsTable SharedStringsTable + { + get + { + List parts = pkg.GetPartsByContentType(XSSFRelation.SHARED_STRINGS.ContentType); + return parts.Count == 0 ? null : new SharedStringsTable(parts[0]); + } + } + + /// + /// Opens up the Styles Table, parses it, and + /// returns a handy object for working with cell styles + /// + public StylesTable StylesTable + { + get + { + List parts = pkg.GetPartsByContentType(XSSFRelation.STYLES.ContentType); + if(parts.Count == 0) + return null; + + // Create the Styles Table, and associate the Themes if present + StylesTable styles = new StylesTable(parts[0]); + parts = pkg.GetPartsByContentType(XSSFRelation.THEME.ContentType); + if(parts.Count != 0) + { + styles.Theme = (new ThemesTable(parts[0])); + } + return styles; + } + + } + + + /// + /// Returns an InputStream to read the contents of the + /// shared strings table. + /// + public Stream SharedStringsData => XSSFRelation.SHARED_STRINGS.GetContents(workbookPart); + + /// + /// Returns an InputStream to read the contents of the + /// styles table. + /// + public Stream StylesData => XSSFRelation.STYLES.GetContents(workbookPart); + + /// + /// Returns an InputStream to read the contents of the + /// themes table. + /// + public Stream ThemesData => XSSFRelation.THEME.GetContents(workbookPart); + + /// + /// Returns an InputStream to read the contents of the + /// main Workbook, which contains key overall data for + /// the file, including sheet definitions. + /// + public Stream WorkbookData => workbookPart.GetInputStream(); + + /// + /// Returns an InputStream to read the contents of the + /// specified Sheet. + /// + /// The relationId of the sheet, from a r:id on the workbook + public Stream GetSheet(String relId) + { + + PackageRelationship rel = workbookPart.GetRelationship(relId); + if (rel == null) + { + throw new ArgumentException("No Sheet found with r:id " + relId); + } + + PackagePartName relName = PackagingUriHelper.CreatePartName(rel.TargetUri); + PackagePart sheet = pkg.GetPart(relName); + if (sheet == null) + { + throw new ArgumentException("No data found for Sheet with r:id " + relId); + } + return sheet.GetInputStream(); + } + + /// + /// Returns an Iterator which will let you Get at all the + /// different Sheets in turn. + /// Each sheet's InputStream is only opened when fetched + /// from the Iterator. It's up to you to close the + /// InputStreams when done with each one. + /// + public IEnumerator GetSheetsData() + { + + return new SheetIterator(workbookPart); + } + + /// + /// Iterator over sheet data. + /// + public class SheetIterator : IEnumerator + { + + /// + /// Maps relId and the corresponding PackagePart + /// + private Dictionary sheetMap; + + /// + /// Current sheet reference + /// + XSSFSheetRef xssfSheetRef; + + /// + /// Iterator over CTSheet objects, returns sheets in logical order. + /// We can't rely on the Ooxml4J's relationship iterator because it returns objects in physical order, + /// i.e. as they are stored in the underlying package + /// + IEnumerator sheetIterator; + + + /// + /// Construct a new SheetIterator + /// + /// package part holding workbook.xml + internal SheetIterator(PackagePart wb) + { + /* + * The order of sheets is defined by the order of CTSheet elements in workbook.xml + */ + try + { + //step 1. Map sheet's relationship Id and the corresponding PackagePart + sheetMap = new Dictionary(); + OPCPackage pkg = wb.Package; + ISet worksheetRels = SheetRelationships; + foreach (PackageRelationship rel in wb.Relationships) + { + String relType = rel.RelationshipType; + if (worksheetRels.Contains(relType)) + { + PackagePartName relName = PackagingUriHelper.CreatePartName(rel.TargetUri); + sheetMap.Add(rel.Id, pkg.GetPart(relName)); + } + } + //step 2. Read array of CTSheet elements, wrap it in a LinkedList + //and construct an iterator + sheetIterator = CreateSheetIteratorFromWB(wb).GetEnumerator(); + } + catch (InvalidFormatException e) + { + throw new POIXMLException(e); + } + } + + static List CreateSheetIteratorFromWB(PackagePart wb) + { + XMLSheetRefReader xmlSheetRefReader = new XMLSheetRefReader(); + NSAX.AElfred.SAXDriver xmlReader; + try + { + xmlReader = new NSAX.AElfred.SAXDriver();// SAXHelper.newXMLReader(); + } + //catch (ParserConfigurationException e) + //{ + // throw new POIXMLException(e); + //} + catch (SAXException e) + { + throw new POIXMLException(e); + } + xmlReader.ContentHandler = (xmlSheetRefReader); + try + { + xmlReader.Parse(new InputSource(wb.GetInputStream())); + } + catch (SAXException e) + { + throw new POIXMLException(e); + } + + List validSheets = new List(); + foreach (XSSFSheetRef xssfSheetRef in xmlSheetRefReader.GetSheetRefs()) + { + //if there's no relationship id, silently skip the sheet + String sheetId = xssfSheetRef.Id; + if (sheetId != null && sheetId.Length > 0) + { + validSheets.Add(xssfSheetRef); + } + } + return validSheets; + } + + /// + /// Gets string representations of relationships + /// that are sheet-like. Added to allow subclassing + /// by XSSFBReader. This is used to decide what + /// relationships to load into the sheetRefs + /// + /// all relationships that are sheet-like + static ISet SheetRelationships => WORKSHEET_RELS; + + /// + /// Returns true if the iteration has more elements. + /// + /// true if the iterator has more elements. + //public bool HasNext() + //{ + // return sheetIterator.HasNext(); + //} + + /// + /// Returns input stream of the next sheet in the iteration + /// + /// input stream of the next sheet in the iteration + private Stream Next() + { + xssfSheetRef = sheetIterator.Current; + + String sheetId = xssfSheetRef.Id; + try + { + PackagePart sheetPkg = sheetMap[sheetId]; + return sheetPkg.GetInputStream(); + } + catch (IOException e) + { + throw new POIXMLException(e); + } + } + + public Stream Current => Next(); + + object IEnumerator.Current => Next(); + public bool MoveNext() + { + return sheetIterator.MoveNext(); + } + + public void Reset() + { + sheetIterator.Reset(); + } + + public void Dispose() + { + sheetIterator.Dispose(); + } + + /// + /// Returns name of the current sheet + /// + /// name of the current sheet + public String SheetName => xssfSheetRef.Name; + + /// + /// Returns the comments associated with this sheet, + /// or null if there aren't any + /// + public CommentsTable SheetComments + { + get + { + PackagePart sheetPkg = SheetPart; + + // Do we have a comments relationship? (Only ever one if so) + try + { + PackageRelationshipCollection commentsList = + sheetPkg.GetRelationshipsByType(XSSFRelation.SHEET_COMMENTS.Relation); + if(commentsList.Size > 0) + { + PackageRelationship comments = commentsList.GetRelationship(0); + PackagePartName commentsName = PackagingUriHelper.CreatePartName(comments.TargetUri); + PackagePart commentsPart = sheetPkg.Package.GetPart(commentsName); + return new CommentsTable(commentsPart); + } + } + catch(InvalidFormatException) + { + return null; + } + catch(IOException) + { + return null; + } + return null; + } + } + + /// + /// Returns the shapes associated with this sheet, + /// an empty list or null if there is an exception + /// + public List Shapes + { + get + { + PackagePart sheetPkg = SheetPart; + List shapes = new List(); + // Do we have a comments relationship? (Only ever one if so) + try + { + PackageRelationshipCollection drawingsList = sheetPkg.GetRelationshipsByType(XSSFRelation.DRAWINGS.Relation); + for(int i = 0; i < drawingsList.Size; i++) + { + PackageRelationship drawings = drawingsList.GetRelationship(i); + PackagePartName drawingsName = PackagingUriHelper.CreatePartName(drawings.TargetUri); + PackagePart drawingsPart = sheetPkg.Package.GetPart(drawingsName); + if(drawingsPart == null) + { + //parts can go missing; Excel ignores them silently -- TIKA-2134 + //LOGGER.log(POILogger.WARN, "Missing Drawing: " + drawingsName + ". Skipping it."); + continue; + } + XSSFDrawing drawing = new XSSFDrawing(drawingsPart); + shapes.AddRange(drawing.GetShapes()); + } + } + catch(XmlException) + { + return null; + } + catch(InvalidFormatException) + { + return null; + } + catch(IOException) + { + return null; + } + return shapes; + } + } + + public PackagePart SheetPart => sheetMap[xssfSheetRef.Id]; + + /// + /// We're read only, so remove isn't supported + /// + public void Remove() + { + throw new InvalidOperationException("Not supported"); + } + + + } + + public sealed class XSSFSheetRef + { + //do we need to store sheetId, too? + private String id; + private String name; + + public XSSFSheetRef(String id, String name) + { + this.id = id; + this.name = name; + } + + public String Id => id; + + public String Name => name; + } + + //scrapes sheet reference info and order from workbook.xml + private class XMLSheetRefReader : DefaultHandler + { + private static String SHEET = "sheet"; + private static String ID = "id"; + private static String NAME = "name"; + + private List sheetRefs = new List(); + + // read + // and add XSSFSheetRef(id="rId6", name="Sheet6") to sheetRefs + public override void StartElement(String uri, String localName, String qName, IAttributes attrs) + { + + if (localName.Equals(SHEET, StringComparison.OrdinalIgnoreCase)) + { + String name = null; + String id = null; + for (int i = 0; i < attrs.Length; i++) + { + String attrName = attrs.GetLocalName(i); + if (attrName.Equals(NAME, StringComparison.OrdinalIgnoreCase)) + { + name = attrs.GetValue(i); + } + else if (attrName.Equals(ID, StringComparison.OrdinalIgnoreCase)) + { + id = attrs.GetValue(i); + } + if (name != null && id != null) + { + sheetRefs.Add(new XSSFSheetRef(id, name)); + break; + } + } + } + } + + public List GetSheetRefs() + { + return sheetRefs; + } + } + } +} diff --git a/ooxml/XSSF/EventUserModel/XSSFSheetXMLHandler.cs b/ooxml/XSSF/EventUserModel/XSSFSheetXMLHandler.cs new file mode 100644 index 000000000..cbab9fc24 --- /dev/null +++ b/ooxml/XSSF/EventUserModel/XSSFSheetXMLHandler.cs @@ -0,0 +1,611 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace NPOI.XSSF.EventUserModel +{ + using static NPOI.XSSF.UserModel.XSSFRelation; + + + using NPOI.SS.UserModel; + using NPOI.SS.Util; + using NPOI.Util; + using NPOI.XSSF.Model; + using NPOI.XSSF.UserModel; + using NSAX.Helpers; + using NPOI.OpenXmlFormats.Spreadsheet; + using NSAX; + + /// + /// This class handles the processing of a sheet#.xml + /// sheet part of a XSSF .xlsx file, and generates + /// row and cell events for it. + /// + public class XSSFSheetXMLHandler : DefaultHandler + { + //private static POILogger logger = POILogFactory.GetLogger(XSSFSheetXMLHandler.class); + + /// + /// These are the different kinds of cells we support. + /// We keep track of the current one between + /// the start and end. + /// + enum XSSFDataType + { + Boolean, + Error, + Formula, + InlineString, + SSTString, + Number, + } + + /// + /// Table with the styles used for formatting + /// + private StylesTable stylesTable; + + /// + /// Table with cell comments + /// + private CommentsTable commentsTable; + + /// + /// Read only access to the shared strings table, for looking + /// up (most) string cell's contents + /// + private ReadOnlySharedStringsTable sharedStringsTable; + + /// + /// Where our text is going + /// + private SheetContentsHandler output; + + // Set when V start element is seen + private bool vIsOpen; + // Set when F start element is seen + private bool fIsOpen; + // Set when an Inline String "is" is seen + private bool isIsOpen; + // Set when a header/footer element is seen + private bool hfIsOpen; + + // Set when cell start element is seen; + // used when cell close element is seen. + private XSSFDataType nextDataType; + + // Used to format numeric cell values. + private short formatIndex; + private String formatString; + private DataFormatter formatter; + private int rowNum; + private int nextRowNum; // some sheets do not have rowNums, Excel can read them so we should try to handle them correctly as well + private String cellRef; + private bool formulasNotResults; + + // Gathers characters as they are seen. + private StringBuilder value = new StringBuilder(); + private StringBuilder formula = new StringBuilder(); + private StringBuilder headerFooter = new StringBuilder(); + + private Queue commentCellRefs; + + /// + /// Accepts objects needed while parsing. + /// + /// Table of styles + /// Table of shared strings + public XSSFSheetXMLHandler( + StylesTable styles, + CommentsTable comments, + ReadOnlySharedStringsTable strings, + SheetContentsHandler sheetContentsHandler, + DataFormatter dataFormatter, + bool formulasNotResults) + { + this.stylesTable = styles; + this.commentsTable = comments; + this.sharedStringsTable = strings; + this.output = sheetContentsHandler; + this.formulasNotResults = formulasNotResults; + this.nextDataType = XSSFDataType.Number; + this.formatter = dataFormatter; + Init(); + } + + /// + /// Accepts objects needed while parsing. + /// + /// Table of styles + /// Table of shared strings + public XSSFSheetXMLHandler( + StylesTable styles, + ReadOnlySharedStringsTable strings, + SheetContentsHandler sheetContentsHandler, + DataFormatter dataFormatter, + bool formulasNotResults) + : this(styles, null, strings, sheetContentsHandler, dataFormatter, formulasNotResults) + { + + } + + /// + /// Accepts objects needed while parsing. + /// + /// Table of styles + /// Table of shared strings + public XSSFSheetXMLHandler( + StylesTable styles, + ReadOnlySharedStringsTable strings, + SheetContentsHandler sheetContentsHandler, + bool formulasNotResults) + : this(styles, strings, sheetContentsHandler, new DataFormatter(), formulasNotResults) + { + + } + + private void Init() + { + if(commentsTable != null) + { + commentCellRefs = new Queue(); + //noinspection deprecation + foreach(CT_Comment comment in commentsTable.GetCTComments().commentList.GetCommentArray()) + { + commentCellRefs.Enqueue(new CellAddress(comment.@ref)); + } + } + } + + private bool IsTextTag(String name) + { + if("v".Equals(name)) + { + // Easy, normal v text tag + return true; + } + if("inlineStr".Equals(name)) + { + // Easy inline string + return true; + } + if("t".Equals(name) && isIsOpen) + { + // Inline string ... pair + return true; + } + // It isn't a text tag + return false; + } + public override void StartElement(String uri, String localName, String qName, + IAttributes attributes) + { + + + if(uri != null && !uri.Equals(NS_SPREADSHEETML)) + { + return; + } + + if(IsTextTag(localName)) + { + vIsOpen = true; + // Clear contents cache + value.Length = 0; + } + else if("is".Equals(localName)) + { + // Inline string outer tag + isIsOpen = true; + } + else if("f".Equals(localName)) + { + // Clear contents cache + formula.Length = 0; + + // Mark us as being a formula if not already + if(nextDataType == XSSFDataType.Number) + { + nextDataType = XSSFDataType.Formula; + } + + // Decide where to Get the formula string from + String type = attributes.GetValue("t"); + if(type != null && type.Equals("shared")) + { + // Is it the one that defines the shared, or uses it? + String ref1 = attributes.GetValue("ref"); + String si = attributes.GetValue("si"); + + if(ref1 != null) + { + // This one defines it + // TODO Save it somewhere + fIsOpen = true; + } + else + { + // This one uses a shared formula + // TODO Retrieve the shared formula and tweak it to + // match the current cell + if(formulasNotResults) + { + //logger.log(POILogger.WARN, "shared formulas not yet supported!"); + } + /*else { + // It's a shared formula, so we can't Get at the formula string yet + // However, they don't care about the formula string, so that's ok! + }*/ + } + } + else + { + fIsOpen = true; + } + } + else if("oddHeader".Equals(localName) || "evenHeader".Equals(localName) || + "firstHeader".Equals(localName) || "firstFooter".Equals(localName) || + "oddFooter".Equals(localName) || "evenFooter".Equals(localName)) + { + hfIsOpen = true; + // Clear contents cache + headerFooter.Length = 0; + } + else if("row".Equals(localName)) + { + String rowNumStr = attributes.GetValue("r"); + if(rowNumStr != null) + { + rowNum = Int32.Parse(rowNumStr) - 1; + } + else + { + rowNum = nextRowNum; + } + output.startRow(rowNum); + } + // c => cell + else if("c".Equals(localName)) + { + // Set up defaults. + this.nextDataType = XSSFDataType.Number; + this.formatIndex = -1; + this.formatString = null; + cellRef = attributes.GetValue("r"); + String cellType = attributes.GetValue("t"); + String cellStyleStr = attributes.GetValue("s"); + if("b".Equals(cellType)) + nextDataType = XSSFDataType.Boolean; + else if("e".Equals(cellType)) + nextDataType = XSSFDataType.Error; + else if("inlineStr".Equals(cellType)) + nextDataType = XSSFDataType.InlineString; + else if("s".Equals(cellType)) + nextDataType = XSSFDataType.SSTString; + else if("str".Equals(cellType)) + nextDataType = XSSFDataType.Formula; + else + { + // Number, but almost certainly with a special style or format + XSSFCellStyle style = null; + if(stylesTable != null) + { + if(cellStyleStr != null) + { + int styleIndex = int.Parse(cellStyleStr); + style = stylesTable.GetStyleAt(styleIndex); + } + else if(stylesTable.NumCellStyles > 0) + { + style = stylesTable.GetStyleAt(0); + } + } + if(style != null) + { + this.formatIndex = style.DataFormat; + this.formatString = style.GetDataFormatString(); + if(this.formatString == null) + this.formatString = BuiltinFormats.GetBuiltinFormat(this.formatIndex); + } + } + } + } + public override void EndElement(String uri, String localName, String qName) + + { + + + if(uri != null && !uri.Equals(NS_SPREADSHEETML)) + { + return; + } + + String thisStr = null; + + // v => contents of a cell + if(IsTextTag(localName)) + { + vIsOpen = false; + + // Process the value contents as required, now we have it all + switch(nextDataType) + { + case XSSFDataType.Boolean: + char first = value[0]; + thisStr = first == '0' ? "FALSE" : "TRUE"; + break; + + case XSSFDataType.Error: + thisStr = "ERROR:" + value; + break; + + case XSSFDataType.Formula: + if(formulasNotResults) + { + thisStr = formula.ToString(); + } + else + { + String fv = value.ToString(); + + if(this.formatString != null) + { + try + { + // Try to use the value as a formattable number + double d = double.Parse(fv); + thisStr = formatter.FormatRawCellContents(d, this.formatIndex, this.formatString); + } + catch(FormatException) + { + // Formula is a String result not a Numeric one + thisStr = fv; + } + } + else + { + // No formatting applied, just do raw value in all cases + thisStr = fv; + } + } + break; + + case XSSFDataType.InlineString: + // TODO: Can these ever have formatting on them? + XSSFRichTextString rtsi = new XSSFRichTextString(value.ToString()); + thisStr = rtsi.ToString(); + break; + + case XSSFDataType.SSTString: + String sstIndex = value.ToString(); + try + { + int idx = int.Parse(sstIndex); + XSSFRichTextString rtss = new XSSFRichTextString(sharedStringsTable.GetEntryAt(idx)); + thisStr = rtss.ToString(); + } + catch(FormatException) + { + //logger.log(POILogger.ERROR, "Failed to parse SST index '" + sstIndex, ex); + } + break; + + case XSSFDataType.Number: + String n = value.ToString(); + if(this.formatString != null && n.Length > 0) + thisStr = formatter.FormatRawCellContents(Double.Parse(n), this.formatIndex, this.formatString); + else + thisStr = n; + break; + + default: + thisStr = "(TODO: Unexpected type: " + nextDataType + ")"; + break; + } + + // Do we have a comment for this cell? + CheckForEmptyCellComments(EmptyCellCommentsCheckType.Cell); + XSSFComment comment = commentsTable != null ? commentsTable.FindCellComment(new CellAddress(cellRef)) : null; + + // Output + output.cell(cellRef, thisStr, comment); + } + else if("f".Equals(localName)) + { + fIsOpen = false; + } + else if("is".Equals(localName)) + { + isIsOpen = false; + } + else if("row".Equals(localName)) + { + // Handle any "missing" cells which had comments attached + CheckForEmptyCellComments(EmptyCellCommentsCheckType.EndOfRow); + + // Finish up the row + output.endRow(rowNum); + + // some sheets do not have rowNum Set in the XML, Excel can read them so we should try to read them as well + nextRowNum = rowNum + 1; + } + else if("sheetData".Equals(localName)) + { + // Handle any "missing" cells which had comments attached + CheckForEmptyCellComments(EmptyCellCommentsCheckType.EndOfSheetData); + } + else if("oddHeader".Equals(localName) || "evenHeader".Equals(localName) || + "firstHeader".Equals(localName)) + { + hfIsOpen = false; + output.headerFooter(headerFooter.ToString(), true, localName); + } + else if("oddFooter".Equals(localName) || "evenFooter".Equals(localName) || + "firstFooter".Equals(localName)) + { + hfIsOpen = false; + output.headerFooter(headerFooter.ToString(), false, localName); + } + } + + /// + /// Captures characters only if a suitable element is open. + /// Originally was just "v"; extended for inlineStr also. + /// + public override void Characters(char[] ch, int start, int length) + + { + + if(vIsOpen) + { + value.Append(ch, start, length); + } + if(fIsOpen) + { + formula.Append(ch, start, length); + } + if(hfIsOpen) + { + headerFooter.Append(ch, start, length); + } + } + + /// + /// Do a check for, and output, comments in otherwise empty cells. + /// + private void CheckForEmptyCellComments(EmptyCellCommentsCheckType type) + { + if(commentCellRefs != null && commentCellRefs.Count>0) + { + // If we've reached the end of the sheet data, output any + // comments we haven't yet already handled + if(type == EmptyCellCommentsCheckType.EndOfSheetData) + { + while(commentCellRefs.Count>0) + { + OutputEmptyCellComment(commentCellRefs.Dequeue()); + } + return; + } + + // At the end of a row, handle any comments for "missing" rows before us + if(this.cellRef == null) + { + if(type == EmptyCellCommentsCheckType.EndOfRow) + { + while(commentCellRefs.Count>0) + { + if(commentCellRefs.Peek().Row == rowNum) + { + OutputEmptyCellComment(commentCellRefs.Dequeue()); + } + else + { + return; + } + } + return; + } + else + { + throw new InvalidOperationException("Cell ref should be null only if there are only empty cells in the row; rowNum: " + rowNum); + } + } + + CellAddress nextCommentCellRef; + do + { + CellAddress cellRef = new CellAddress(this.cellRef); + CellAddress peekCellRef = commentCellRefs.Peek(); + if(type == EmptyCellCommentsCheckType.Cell && cellRef.Equals(peekCellRef)) + { + // remove the comment cell ref from the list if we're about to handle it alongside the cell content + commentCellRefs.Dequeue(); + return; + } + else + { + // fill in any gaps if there are empty cells with comment mixed in with non-empty cells + int comparison = peekCellRef.CompareTo(cellRef); + if(comparison > 0 && type == EmptyCellCommentsCheckType.EndOfRow && peekCellRef.Row <= rowNum) + { + nextCommentCellRef = commentCellRefs.Dequeue(); + OutputEmptyCellComment(nextCommentCellRef); + } + else if(comparison < 0 && type == EmptyCellCommentsCheckType.Cell && peekCellRef.Row <= rowNum) + { + nextCommentCellRef = commentCellRefs.Dequeue(); + OutputEmptyCellComment(nextCommentCellRef); + } + else + { + nextCommentCellRef = null; + } + } + } while(nextCommentCellRef != null && commentCellRefs.Count>0); + } + } + + + /// + /// Output an empty-cell comment. + /// + private void OutputEmptyCellComment(CellAddress cellRef) + { + XSSFComment comment = commentsTable.FindCellComment(cellRef); + output.cell(cellRef.FormatAsString(), null, comment); + } + + private enum EmptyCellCommentsCheckType + { + Cell, + EndOfRow, + EndOfSheetData + } + + /// + /// You need to implement this to handle the results + /// of the sheet parsing. + /// + public interface SheetContentsHandler + { + /// + /// A row with the (zero based) row number has started */ + /// + public void startRow(int rowNum); + /// + /// A row with the (zero based) row number has ended */ + /// + public void endRow(int rowNum); + /// + /// A cell, with the given formatted value (may be null), + /// and possibly a comment (may be null), was encountered */ + /// + public void cell(String cellReference, String formattedValue, XSSFComment comment); + /// + /// A header or footer has been encountered */ + /// + public void headerFooter(String text, bool IsHeader, String tagName); + } + } +} + diff --git a/ooxml/XSSF/Model/CommentsTable.cs b/ooxml/XSSF/Model/CommentsTable.cs index 7e545140a..763d359de 100644 --- a/ooxml/XSSF/Model/CommentsTable.cs +++ b/ooxml/XSSF/Model/CommentsTable.cs @@ -116,15 +116,9 @@ public void ReferenceUpdated(CellAddress oldReference, CT_Comment comment) } - public int GetNumberOfComments() - { - return comments.commentList.SizeOfCommentArray(); - } + public int NumberOfComments => comments.commentList.SizeOfCommentArray(); - public int GetNumberOfAuthors() - { - return comments.authors.SizeOfAuthorArray(); - } + public int NumberOfAuthors => comments.authors.SizeOfAuthorArray(); public String GetAuthor(long authorId) { diff --git a/ooxml/XSSF/Model/StylesTable.cs b/ooxml/XSSF/Model/StylesTable.cs index 8c8238739..72f123ea8 100644 --- a/ooxml/XSSF/Model/StylesTable.cs +++ b/ooxml/XSSF/Model/StylesTable.cs @@ -130,25 +130,26 @@ public void SetWorkbook(XSSFWorkbook wb) { this.workbook = wb; } - public ThemesTable GetTheme() + public ThemesTable Theme { - return theme; - } - - public void SetTheme(ThemesTable theme) - { - this.theme = theme; - - if (theme != null) theme.SetColorMap(indexedColors); - // Pass the themes table along to things which need to - // know about it, but have already been Created by now - foreach (XSSFFont font in fonts) + get { - font.SetThemesTable(theme); + return theme; } - foreach (XSSFCellBorder border in borders) + set { - border.SetThemesTable(theme); + this.theme = value; + + // Pass the themes table along to things which need to + // know about it, but have already been Created by now + foreach(XSSFFont font in fonts) + { + font.SetThemesTable(theme); + } + foreach(XSSFCellBorder border in borders) + { + border.SetThemesTable(theme); + } } } @@ -199,7 +200,7 @@ public void EnsureThemesTable() { if (theme != null) return; - SetTheme((ThemesTable)workbook.CreateRelationship(XSSFRelation.THEME, XSSFFactory.GetInstance())); + theme = (ThemesTable)workbook.CreateRelationship(XSSFRelation.THEME, XSSFFactory.GetInstance()); } /** * Read this shared styles table from an XML file. @@ -551,10 +552,7 @@ public ReadOnlyCollection GetFills() return fills.AsReadOnly(); } - public ReadOnlyCollection GetFonts() - { - return fonts.AsReadOnly(); - } + public ReadOnlyCollection Fonts => fonts.AsReadOnly(); public IDictionary GetNumberFormats() { diff --git a/ooxml/XSSF/UserModel/XSSFFont.cs b/ooxml/XSSF/UserModel/XSSFFont.cs index 720ce0c95..5e9eb65df 100644 --- a/ooxml/XSSF/UserModel/XSSFFont.cs +++ b/ooxml/XSSF/UserModel/XSSFFont.cs @@ -550,7 +550,7 @@ public override String ToString() // */ public long RegisterTo(StylesTable styles) { - this._themes = styles.GetTheme(); + this._themes = styles.Theme; short idx = (short)styles.PutFont(this, true); this._index = idx; return idx; diff --git a/ooxml/XSSF/UserModel/XSSFRichTextString.cs b/ooxml/XSSF/UserModel/XSSFRichTextString.cs index dd5f323c6..371de8dfe 100644 --- a/ooxml/XSSF/UserModel/XSSFRichTextString.cs +++ b/ooxml/XSSF/UserModel/XSSFRichTextString.cs @@ -756,7 +756,7 @@ private ThemesTable GetThemesTable() { if(styles == null) return null; - return styles.GetTheme(); + return styles.Theme; } } } diff --git a/ooxml/XSSF/UserModel/XSSFSheet.cs b/ooxml/XSSF/UserModel/XSSFSheet.cs index f4fc5f20e..37150c014 100644 --- a/ooxml/XSSF/UserModel/XSSFSheet.cs +++ b/ooxml/XSSF/UserModel/XSSFSheet.cs @@ -950,7 +950,7 @@ public bool HasComments return false; } - return sheetComments.GetNumberOfComments() > 0; + return sheetComments.NumberOfComments > 0; } } @@ -963,7 +963,7 @@ internal int NumberOfComments return 0; } - return sheetComments.GetNumberOfComments(); + return sheetComments.NumberOfComments; } } @@ -4138,7 +4138,7 @@ public void CopyTo(IWorkbook dest, string name, bool copyStyle, bool keepFormula StylesTable styles = ((XSSFWorkbook) dest).GetStylesSource(); if(copyStyle && Workbook.NumberOfFonts > 0) { - foreach(XSSFFont font in ((XSSFWorkbook) Workbook).GetStylesSource().GetFonts()) + foreach(XSSFFont font in ((XSSFWorkbook) Workbook).GetStylesSource().Fonts) { styles.PutFont(font); } diff --git a/ooxml/XSSF/UserModel/XSSFWorkbook.cs b/ooxml/XSSF/UserModel/XSSFWorkbook.cs index 60b280a5c..e17f431a2 100644 --- a/ooxml/XSSF/UserModel/XSSFWorkbook.cs +++ b/ooxml/XSSF/UserModel/XSSFWorkbook.cs @@ -366,7 +366,7 @@ internal override void OnDocumentRead() } } stylesSource.SetWorkbook(this); - stylesSource.SetTheme(theme); + stylesSource.Theme = theme; if (sharedStringSource == null) { @@ -1121,7 +1121,7 @@ public short NumberOfFonts { get { - return (short)stylesSource.GetFonts().Count; + return (short)stylesSource.Fonts.Count; } } @@ -1867,7 +1867,7 @@ public StylesTable GetStylesSource() public ThemesTable GetTheme() { if (stylesSource == null) return null; - return stylesSource.GetTheme(); + return stylesSource.Theme; } /** diff --git a/testcases/ooxml/NPOI.OOXML.TestCases.Core.csproj b/testcases/ooxml/NPOI.OOXML.TestCases.Core.csproj index 24573ba7c..bf948b49b 100644 --- a/testcases/ooxml/NPOI.OOXML.TestCases.Core.csproj +++ b/testcases/ooxml/NPOI.OOXML.TestCases.Core.csproj @@ -1,4 +1,4 @@ - + net472;net8.0 diff --git a/testcases/ooxml/XSSF/EventUserModel/TestReadOnlySharedStringsTable.cs b/testcases/ooxml/XSSF/EventUserModel/TestReadOnlySharedStringsTable.cs new file mode 100644 index 000000000..a0fcc1382 --- /dev/null +++ b/testcases/ooxml/XSSF/EventUserModel/TestReadOnlySharedStringsTable.cs @@ -0,0 +1,125 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace TestCases.XSSF.EventUserModel +{ + + using System.Text.RegularExpressions; + using NPOI.OpenXml4Net.OPC; + using NPOI.XSSF.Model; + using NPOI.XSSF.UserModel; + using NPOI.XSSF.EventUserModel; + using NUnit.Framework; + using NPOI.OpenXmlFormats.Spreadsheet; + using NUnit.Framework.Legacy; + + /// + /// Tests for + /// + [TestFixture] + public sealed class TestReadOnlySharedStringsTable + { + private static POIDataSamples _ssTests = POIDataSamples.GetSpreadSheetInstance(); + + [Test] + public void TestParse() + { + + OPCPackage pkg = OPCPackage.Open(_ssTests.OpenResourceAsStream("SampleSS.xlsx")); + List parts = pkg.GetPartsByName(new Regex("/xl/sharedStrings.xml", RegexOptions.Compiled)); + ClassicAssert.AreEqual(1, parts.Count); + + SharedStringsTable stbl = new SharedStringsTable(parts[0]); + ReadOnlySharedStringsTable rtbl = new ReadOnlySharedStringsTable(parts[0]); + + ClassicAssert.AreEqual(stbl.Count, rtbl.Count); + ClassicAssert.AreEqual(stbl.UniqueCount, rtbl.UniqueCount); + + ClassicAssert.AreEqual(stbl.Items.Count, stbl.UniqueCount); + ClassicAssert.AreEqual(rtbl.Items.Count, rtbl.UniqueCount); + for(int i = 0; i < stbl.UniqueCount; i++) + { + CT_Rst i1 = stbl.GetEntryAt(i); + String i2 = rtbl.GetEntryAt(i); + ClassicAssert.AreEqual(i1.t, i2); + } + + } + + //51519 + [Test] + public void TestPhoneticRuns() + { + + OPCPackage pkg = OPCPackage.Open(_ssTests.OpenResourceAsStream("51519.xlsx")); + List parts = pkg.GetPartsByName(new Regex("/xl/sharedStrings.xml", RegexOptions.Compiled)); + ClassicAssert.AreEqual(1, parts.Count); + + ReadOnlySharedStringsTable rtbl = new ReadOnlySharedStringsTable(parts[0], true); + List strings = rtbl.Items; + ClassicAssert.AreEqual(49, strings.Count); + + ClassicAssert.AreEqual("\u30B3\u30E1\u30F3\u30C8", rtbl.GetEntryAt(0)); + ClassicAssert.AreEqual("\u65E5\u672C\u30AA\u30E9\u30AF\u30EB \u30CB\u30DB\u30F3", rtbl.GetEntryAt(3)); + + //now do not include phonetic runs + rtbl = new ReadOnlySharedStringsTable(parts[0], false); + strings = rtbl.Items; + ClassicAssert.AreEqual(49, strings.Count); + + ClassicAssert.AreEqual("\u30B3\u30E1\u30F3\u30C8", rtbl.GetEntryAt(0)); + ClassicAssert.AreEqual("\u65E5\u672C\u30AA\u30E9\u30AF\u30EB", rtbl.GetEntryAt(3)); + + } + [Test] + public void TestEmptySSTOnPackageObtainedViaWorkbook() + { + + XSSFWorkbook wb = new XSSFWorkbook(_ssTests.OpenResourceAsStream("noSharedStringTable.xlsx")); + OPCPackage pkg = wb.Package; + assertEmptySST(pkg); + wb.Close(); + } + [Test] + public void TestEmptySSTOnPackageDirect() + { + + OPCPackage pkg = OPCPackage.Open(_ssTests.OpenResourceAsStream("noSharedStringTable.xlsx")); + assertEmptySST(pkg); + } + + private void assertEmptySST(OPCPackage pkg) + { + + ReadOnlySharedStringsTable sst = new ReadOnlySharedStringsTable(pkg); + ClassicAssert.AreEqual(0, sst.Count); + ClassicAssert.AreEqual(0, sst.UniqueCount); + ClassicAssert.IsNull(sst.Items); // same state it's left in if fed a package which has no SST part. + } + + } +} + diff --git a/testcases/ooxml/XSSF/EventUserModel/TestXSSFReader.cs b/testcases/ooxml/XSSF/EventUserModel/TestXSSFReader.cs new file mode 100644 index 000000000..6149c4c94 --- /dev/null +++ b/testcases/ooxml/XSSF/EventUserModel/TestXSSFReader.cs @@ -0,0 +1,353 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace TestCases.XSSF.EventUserModel +{ + using NPOI; + using NPOI.OpenXml4Net.OPC; + using NPOI.Util; + using NPOI.XSSF; + using NPOI.XSSF.EventUserModel; + using NPOI.XSSF.Model; + using NPOI.XSSF.UserModel; + using NUnit.Framework; + using NUnit.Framework.Legacy; + + + /// + /// Tests for + /// + [TestFixture] + public sealed class TestXSSFReader + { + private static POIDataSamples _ssTests = POIDataSamples.GetSpreadSheetInstance(); + + [Test] + public void TestGetBits() + { + + OPCPackage pkg = OPCPackage.Open(_ssTests.OpenResourceAsStream("SampleSS.xlsx")); + + XSSFReader r = new XSSFReader(pkg); + + ClassicAssert.IsNotNull(r.WorkbookData); + ClassicAssert.IsNotNull(r.SharedStringsData); + ClassicAssert.IsNotNull(r.StylesData); + + ClassicAssert.IsNotNull(r.SharedStringsTable); + ClassicAssert.IsNotNull(r.StylesTable); + } + + [Test] + public void TestStyles() + { + + OPCPackage pkg = OPCPackage.Open(_ssTests.OpenResourceAsStream("SampleSS.xlsx")); + + XSSFReader r = new XSSFReader(pkg); + + ClassicAssert.AreEqual(3, r.StylesTable.Fonts.Count); + ClassicAssert.AreEqual(0, r.StylesTable.NumDataFormats); + + // The Styles Table should have the themes associated with it too + ClassicAssert.IsNotNull(r.StylesTable.Theme); + + // Check we Get valid data for the two + ClassicAssert.IsNotNull(r.StylesData); + ClassicAssert.IsNotNull(r.ThemesData); + } + + [Test] + public void TestStrings() + { + + OPCPackage pkg = OPCPackage.Open(_ssTests.OpenResourceAsStream("SampleSS.xlsx")); + + XSSFReader r = new XSSFReader(pkg); + + ClassicAssert.AreEqual(11, r.SharedStringsTable.Items.Count); + ClassicAssert.AreEqual("Test spreadsheet", new XSSFRichTextString(r.SharedStringsTable.GetEntryAt(0)).ToString()); + } + + [Test] + public void TestSheets() + { + + OPCPackage pkg = OPCPackage.Open(_ssTests.OpenResourceAsStream("SampleSS.xlsx")); + + XSSFReader r = new XSSFReader(pkg); + byte[] data = new byte[4096]; + + // By r:id + ClassicAssert.IsNotNull(r.GetSheet("rId2")); + int read = IOUtils.ReadFully(r.GetSheet("rId2"), data); + ClassicAssert.AreEqual(974, read); + + // All + IEnumerator it = r.GetSheetsData(); + + int count = 0; + while(it.MoveNext()) + { + count++; + Stream inp = it.Current; + ClassicAssert.IsNotNull(inp); + read = IOUtils.ReadFully(inp, data); + inp.Close(); + + ClassicAssert.IsTrue(read > 400); + ClassicAssert.IsTrue(read < 1500); + } + ClassicAssert.AreEqual(3, count); + } + + /// + /// Check that the sheet iterator returns sheets in the logical order + /// (as they are defined in the workbook.xml) + /// + [Test] + public void TestOrderOfSheets() + { + + OPCPackage pkg = OPCPackage.Open(_ssTests.OpenResourceAsStream("reordered_sheets.xlsx")); + + XSSFReader r = new XSSFReader(pkg); + + String[] sheetNames = {"Sheet4", "Sheet2", "Sheet3", "Sheet1"}; + XSSFReader.SheetIterator it = (XSSFReader.SheetIterator)r.GetSheetsData(); + + int count = 0; + while(it.MoveNext()) + { + Stream inp = it.Current; + ClassicAssert.IsNotNull(inp); + inp.Close(); + + ClassicAssert.AreEqual(sheetNames[count], it.SheetName); + count++; + } + ClassicAssert.AreEqual(4, count); + } + [Test] + public void TestComments() + { + + OPCPackage pkg = XSSFTestDataSamples.OpenSamplePackage("comments.xlsx"); + XSSFReader r = new XSSFReader(pkg); + XSSFReader.SheetIterator it = (XSSFReader.SheetIterator)r.GetSheetsData(); + + int count = 0; + while(it.MoveNext()) + { + count++; + Stream inp = it.Current; + inp.Close(); + + if(count == 1) + { + ClassicAssert.IsNotNull(it.SheetComments); + CommentsTable ct = it.SheetComments; + ClassicAssert.AreEqual(1, ct.NumberOfAuthors); + ClassicAssert.AreEqual(3, ct.NumberOfComments); + } + else + { + ClassicAssert.IsNull(it.SheetComments); + } + } + ClassicAssert.AreEqual(3, count); + } + + /// + /// Iterating over a workbook with chart sheets in it, using the + /// XSSFReader method + /// + /// Exception + [Test] + public void Test50119() + { + + OPCPackage pkg = XSSFTestDataSamples.OpenSamplePackage("WithChartSheet.xlsx"); + XSSFReader r = new XSSFReader(pkg); + XSSFReader.SheetIterator it = (XSSFReader.SheetIterator)r.GetSheetsData(); + + while(it.MoveNext()) + { + Stream stream = it.Current; + stream.Close(); + } + } + + /// + /// Test text extraction from text box using GetShapes() + /// + /// Exception + [Test] + public void TestShapes() + { + + OPCPackage pkg = XSSFTestDataSamples.OpenSamplePackage("WithTextBox.xlsx"); + XSSFReader r = new XSSFReader(pkg); + XSSFReader.SheetIterator it = (XSSFReader.SheetIterator) r.GetSheetsData(); + + String text = GetShapesString(it); + StringAssert.Contains("Line 1", text); + StringAssert.Contains("Line 2", text); + StringAssert.Contains("Line 3", text); + } + + private String GetShapesString(XSSFReader.SheetIterator it) + { + StringBuilder sb = new StringBuilder(); + while(it.MoveNext()) + { + var _ = it.Current; + List shapes = it.Shapes; + if(shapes != null) + { + foreach(XSSFShape shape in shapes) + { + if(shape is XSSFSimpleShape) + { + String t = ((XSSFSimpleShape) shape).Text; + sb.Append(t).Append('\n'); + } + } + } + } + return sb.ToString(); + } + [Test] + public void TestBug57914() + { + + OPCPackage pkg = XSSFTestDataSamples.OpenSamplePackage("57914.xlsx"); + XSSFReader r; + + // for now expect this to Assert.Fail, when we fix 57699, this one should Assert.Fail so we know we should adjust + // this test as well + try + { + r = new XSSFReader(pkg); + Assert.Fail("This will Assert.Fail until bug 57699 is fixed"); + } + catch(POIXMLException e) + { + StringAssert.Contains("57699", e.Message); + return; + } + + XSSFReader.SheetIterator it = (XSSFReader.SheetIterator) r.GetSheetsData(); + + String text = GetShapesString(it); + StringAssert.Contains("Line 1", text); + StringAssert.Contains("Line 2", text); + StringAssert.Contains("Line 3", text); + } + + /// + /// NPE from XSSFReader$SheetIterator. on XLSX files generated by + /// the openpyxl library + /// + [Test] + public void Test58747() + { + + OPCPackage pkg = XSSFTestDataSamples.OpenSamplePackage("58747.xlsx"); + ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg); + ClassicAssert.IsNotNull(strings); + XSSFReader reader = new XSSFReader(pkg); + StylesTable styles = reader.StylesTable; + ClassicAssert.IsNotNull(styles); + + XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) reader.GetSheetsData(); + ClassicAssert.AreEqual(true, iter.MoveNext()); + var _ = iter.Current; + + ClassicAssert.AreEqual(false, iter.MoveNext()); + ClassicAssert.AreEqual("Orders", iter.SheetName); + + pkg.Close(); + } + + /// + /// NPE when sheet has no relationship id in the workbook + /// 60825 + /// + [Test] + public void TestSheetWithNoRelationshipId() + { + + OPCPackage pkg = XSSFTestDataSamples.OpenSamplePackage("60825.xlsx"); + ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg); + ClassicAssert.IsNotNull(strings); + XSSFReader reader = new XSSFReader(pkg); + StylesTable styles = reader.StylesTable; + ClassicAssert.IsNotNull(styles); + + XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) reader.GetSheetsData(); + iter.MoveNext(); + ClassicAssert.IsNotNull(iter.Current); + ClassicAssert.IsFalse(iter.MoveNext()); + + pkg.Close(); + } + + /// + /// + /// bug 61304: Call to XSSFReader.SheetsData returns duplicate sheets. + /// + /// + /// The problem seems to be caused only by those xlsx files which have a specific + /// order of the attributes inside the <sheet> tag of workbook.xml + /// + /// + /// Example (which causes the problems): + /// <sheet name="Sheet6" r:id="rId6" sheetId="4"/> + /// + /// + /// While this one works correctly: + /// <sheet name="Sheet6" sheetId="4" r:id="rId6"/> + /// + /// + [Test] + public void Test61034() + { + OPCPackage pkg = XSSFTestDataSamples.OpenSamplePackage("61034.xlsx"); + XSSFReader reader = new XSSFReader(pkg); + XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) reader.GetSheetsData(); + ISet seen = new HashSet(); + while(iter.MoveNext()) + { + Stream stream = iter.Current; + String sheetName = iter.SheetName; + CollectionAssert.DoesNotContain(seen, sheetName); + seen.Add(sheetName); + stream.Close(); + } + pkg.Close(); + } + } +} + diff --git a/testcases/ooxml/XSSF/Model/TestCommentsTable.cs b/testcases/ooxml/XSSF/Model/TestCommentsTable.cs index bde3999bb..c8fcfd9d8 100644 --- a/testcases/ooxml/XSSF/Model/TestCommentsTable.cs +++ b/testcases/ooxml/XSSF/Model/TestCommentsTable.cs @@ -38,7 +38,7 @@ public class TestCommentsTable public void FindAuthor() { CommentsTable sheetComments = new CommentsTable(); - ClassicAssert.AreEqual(1, sheetComments.GetNumberOfAuthors()); + ClassicAssert.AreEqual(1, sheetComments.NumberOfAuthors); ClassicAssert.AreEqual(0, sheetComments.FindAuthor("")); ClassicAssert.AreEqual("", sheetComments.GetAuthor(0)); @@ -205,22 +205,22 @@ public void RemoveComment() ClassicAssert.AreSame(a1, sheetComments.GetCTComment(addrA1)); ClassicAssert.AreSame(a2, sheetComments.GetCTComment(addrA2)); ClassicAssert.AreSame(a3, sheetComments.GetCTComment(addrA3)); - ClassicAssert.AreEqual(3, sheetComments.GetNumberOfComments()); + ClassicAssert.AreEqual(3, sheetComments.NumberOfComments); ClassicAssert.IsTrue(sheetComments.RemoveComment(addrA1)); - ClassicAssert.AreEqual(2, sheetComments.GetNumberOfComments()); + ClassicAssert.AreEqual(2, sheetComments.NumberOfComments); ClassicAssert.IsNull(sheetComments.GetCTComment(addrA1)); ClassicAssert.AreSame(a2, sheetComments.GetCTComment(addrA2)); ClassicAssert.AreSame(a3, sheetComments.GetCTComment(addrA3)); ClassicAssert.IsTrue(sheetComments.RemoveComment(addrA2)); - ClassicAssert.AreEqual(1, sheetComments.GetNumberOfComments()); + ClassicAssert.AreEqual(1, sheetComments.NumberOfComments); ClassicAssert.IsNull(sheetComments.GetCTComment(addrA1)); ClassicAssert.IsNull(sheetComments.GetCTComment(addrA2)); ClassicAssert.AreSame(a3, sheetComments.GetCTComment(addrA3)); ClassicAssert.IsTrue(sheetComments.RemoveComment(addrA3)); - ClassicAssert.AreEqual(0, sheetComments.GetNumberOfComments()); + ClassicAssert.AreEqual(0, sheetComments.NumberOfComments); ClassicAssert.IsNull(sheetComments.GetCTComment(addrA1)); ClassicAssert.IsNull(sheetComments.GetCTComment(addrA2)); ClassicAssert.IsNull(sheetComments.GetCTComment(addrA3)); diff --git a/testcases/ooxml/XSSF/Model/TestStylesTable.cs b/testcases/ooxml/XSSF/Model/TestStylesTable.cs index 6e36cfa9b..87f5bbf1a 100644 --- a/testcases/ooxml/XSSF/Model/TestStylesTable.cs +++ b/testcases/ooxml/XSSF/Model/TestStylesTable.cs @@ -100,7 +100,7 @@ public void doTestExisting(StylesTable st) ClassicAssert.AreEqual(1, st.StyleXfsSize); ClassicAssert.AreEqual(8, st.NumDataFormats); - ClassicAssert.AreEqual(2, st.GetFonts().Count); + ClassicAssert.AreEqual(2, st.Fonts.Count); ClassicAssert.AreEqual(2, st.GetFills().Count); ClassicAssert.AreEqual(1, st.GetBorders().Count); diff --git a/testcases/ooxml/XSSF/Model/TestThemesTable.cs b/testcases/ooxml/XSSF/Model/TestThemesTable.cs index d18bd9f30..a089d1aa6 100644 --- a/testcases/ooxml/XSSF/Model/TestThemesTable.cs +++ b/testcases/ooxml/XSSF/Model/TestThemesTable.cs @@ -249,16 +249,16 @@ public void TestAddNew() ClassicAssert.AreEqual(null, wb.GetTheme()); StylesTable styles = wb.GetStylesSource(); - ClassicAssert.AreEqual(null, styles.GetTheme()); + ClassicAssert.AreEqual(null, styles.Theme); styles.EnsureThemesTable(); - ClassicAssert.IsNotNull(styles.GetTheme()); + ClassicAssert.IsNotNull(styles.Theme); ClassicAssert.IsNotNull(wb.GetTheme()); wb = XSSFTestDataSamples.WriteOutAndReadBack(wb) as XSSFWorkbook; styles = wb.GetStylesSource(); - ClassicAssert.IsNotNull(styles.GetTheme()); + ClassicAssert.IsNotNull(styles.Theme); ClassicAssert.IsNotNull(wb.GetTheme()); } } diff --git a/testcases/ooxml/XSSF/UserModel/TestXSSFBugs.cs b/testcases/ooxml/XSSF/UserModel/TestXSSFBugs.cs index 4bef56af4..77f454b42 100644 --- a/testcases/ooxml/XSSF/UserModel/TestXSSFBugs.cs +++ b/testcases/ooxml/XSSF/UserModel/TestXSSFBugs.cs @@ -1415,7 +1415,7 @@ public void Test51850() // Sheet 2 has comments ClassicAssert.IsNotNull(sh2.GetCommentsTable(false)); - ClassicAssert.AreEqual(1, sh2.GetCommentsTable(false).GetNumberOfComments()); + ClassicAssert.AreEqual(1, sh2.GetCommentsTable(false).NumberOfComments); // Sheet 1 doesn't (yet) ClassicAssert.IsNull(sh1.GetCommentsTable(false)); @@ -1464,10 +1464,10 @@ public void Test51850() // Check the comments ClassicAssert.IsNotNull(sh2.GetCommentsTable(false)); - ClassicAssert.AreEqual(1, sh2.GetCommentsTable(false).GetNumberOfComments()); + ClassicAssert.AreEqual(1, sh2.GetCommentsTable(false).NumberOfComments); ClassicAssert.IsNotNull(sh1.GetCommentsTable(false)); - ClassicAssert.AreEqual(2, sh1.GetCommentsTable(false).GetNumberOfComments()); + ClassicAssert.AreEqual(2, sh1.GetCommentsTable(false).NumberOfComments); wb2.Close(); } diff --git a/testcases/ooxml/XSSF/UserModel/TestXSSFComment.cs b/testcases/ooxml/XSSF/UserModel/TestXSSFComment.cs index 548648621..3c217cb68 100644 --- a/testcases/ooxml/XSSF/UserModel/TestXSSFComment.cs +++ b/testcases/ooxml/XSSF/UserModel/TestXSSFComment.cs @@ -53,7 +53,7 @@ public void Constructor() ClassicAssert.IsNotNull(sheetComments.GetCTComments().commentList); ClassicAssert.IsNotNull(sheetComments.GetCTComments().authors); ClassicAssert.AreEqual(1, sheetComments.GetCTComments().authors.SizeOfAuthorArray()); - ClassicAssert.AreEqual(1, sheetComments.GetNumberOfAuthors()); + ClassicAssert.AreEqual(1, sheetComments.NumberOfAuthors); CT_Comment ctComment = sheetComments.NewComment(CellAddress.A1); CT_Shape vmlShape = new CT_Shape(); @@ -167,17 +167,17 @@ public void Author() CommentsTable sheetComments = new CommentsTable(); CT_Comment ctComment = sheetComments.NewComment(CellAddress.A1); - ClassicAssert.AreEqual(1, sheetComments.GetNumberOfAuthors()); + ClassicAssert.AreEqual(1, sheetComments.NumberOfAuthors); XSSFComment comment = new XSSFComment(sheetComments, ctComment, null); ClassicAssert.AreEqual("", comment.Author); comment.Author = ("Apache POI"); ClassicAssert.AreEqual("Apache POI", comment.Author); - ClassicAssert.AreEqual(2, sheetComments.GetNumberOfAuthors()); + ClassicAssert.AreEqual(2, sheetComments.NumberOfAuthors); comment.Author = ("Apache POI"); - ClassicAssert.AreEqual(2, sheetComments.GetNumberOfAuthors()); + ClassicAssert.AreEqual(2, sheetComments.NumberOfAuthors); comment.Author = (""); ClassicAssert.AreEqual("", comment.Author); - ClassicAssert.AreEqual(2, sheetComments.GetNumberOfAuthors()); + ClassicAssert.AreEqual(2, sheetComments.NumberOfAuthors); } [Test] diff --git a/testcases/ooxml/XSSF/UserModel/TestXSSFWorkbook.cs b/testcases/ooxml/XSSF/UserModel/TestXSSFWorkbook.cs index 054f0ab77..cac07ef33 100644 --- a/testcases/ooxml/XSSF/UserModel/TestXSSFWorkbook.cs +++ b/testcases/ooxml/XSSF/UserModel/TestXSSFWorkbook.cs @@ -279,7 +279,7 @@ public void Styles() // Has 8 number formats ClassicAssert.AreEqual(8, st.NumDataFormats); // Has 2 fonts - ClassicAssert.AreEqual(2, st.GetFonts().Count); + ClassicAssert.AreEqual(2, st.Fonts.Count); // Has 2 Fills ClassicAssert.AreEqual(2, st.GetFills().Count); // Has 1 border @@ -303,7 +303,7 @@ public void Styles() ClassicAssert.IsNotNull(ss); ClassicAssert.AreEqual(10, st.NumDataFormats); - ClassicAssert.AreEqual(2, st.GetFonts().Count); + ClassicAssert.AreEqual(2, st.Fonts.Count); ClassicAssert.AreEqual(2, st.GetFills().Count); ClassicAssert.AreEqual(1, st.GetBorders().Count); diff --git a/testcases/test-data/spreadsheet/60825.xlsx b/testcases/test-data/spreadsheet/60825.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ffcfe08e619a852608f2692c7516ba5ca2261e04 GIT binary patch literal 6524 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

%Q0`Wo{L&`CIRyO?uI@ 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 literal 0 HcmV?d00001 From 6d95ca23e2c3fcb6aef15b1d6bd6d5d795dda744 Mon Sep 17 00:00:00 2001 From: Antony Liu Date: Thu, 7 Aug 2025 21:37:31 +0800 Subject: [PATCH 3/3] event based excel extractor --- main/HSSF/Extractor/ExcelExtractor.cs | 14 +- main/SS/Extractor/ExcelExtractor.cs | 4 +- ooxml/NPOI.OOXML.Core.csproj | 1 - ooxml/POIXMLTextExtractor.cs | 6 +- .../EventUserModel/XSSFSheetXMLHandler.cs | 20 +- .../Extractor/XSSFEventBasedExcelExtractor.cs | 451 ++++++++++++++---- ooxml/XSSF/Extractor/XSSFExcelExtractor.cs | 37 +- .../TestXSSFEventBasedExcelExtractor.cs | 435 +++++++++++++++++ .../XSSF/Extractor/TestXSSFExcelExtractor.cs | 10 +- 9 files changed, 836 insertions(+), 142 deletions(-) create mode 100644 testcases/ooxml/XSSF/Extractor/TestXSSFEventBasedExcelExtractor.cs diff --git a/main/HSSF/Extractor/ExcelExtractor.cs b/main/HSSF/Extractor/ExcelExtractor.cs index 9e9b92097..2f3cf5d2f 100644 --- a/main/HSSF/Extractor/ExcelExtractor.cs +++ b/main/HSSF/Extractor/ExcelExtractor.cs @@ -42,7 +42,7 @@ public class ExcelExtractor : POIOLE2TextExtractor, IExcelExtractor private bool formulasNotResults = false; private bool includeCellComments = false; private bool includeBlankCells = false; - private bool includeHeaderFooter = true; + private bool includeHeadersFooters = true; ///

/// Initializes a new instance of the class. /// @@ -65,13 +65,13 @@ public ExcelExtractor(POIFSFileSystem fs) /// /// Should header and footer be included? Default is true /// - public bool IncludeHeaderFooter + public bool IncludeHeadersFooters { get { - return this.includeHeaderFooter; + return this.includeHeadersFooters; } set { - this.includeHeaderFooter = value; + this.includeHeadersFooters = value; } } /// @@ -137,6 +137,8 @@ public bool IncludeBlankCells } } + public bool AddTabEachEmptyCell { get; set; } + /// /// Retreives the text contents of the file /// @@ -168,7 +170,7 @@ public override String Text } // Header text, if there is any - if (sheet.Header != null && includeHeaderFooter) + if (sheet.Header != null && includeHeadersFooters) { text.Append( ExtractHeaderFooter(sheet.Header) @@ -289,7 +291,7 @@ public override String Text } // Finally Feader text, if there is any - if (sheet.Footer != null && includeHeaderFooter) + if (sheet.Footer != null && includeHeadersFooters) { text.Append( ExtractHeaderFooter(sheet.Footer) diff --git a/main/SS/Extractor/ExcelExtractor.cs b/main/SS/Extractor/ExcelExtractor.cs index 9cfb8ec40..91c298abd 100644 --- a/main/SS/Extractor/ExcelExtractor.cs +++ b/main/SS/Extractor/ExcelExtractor.cs @@ -27,7 +27,9 @@ public interface IExcelExtractor bool IncludeCellComments { get; set; } bool IncludeSheetNames { get; set; } bool FormulasNotResults { get; set; } - bool IncludeHeaderFooter { get; set; } + bool IncludeHeadersFooters { get; set; } + //Add a tab delimiter for each empty cell. + bool AddTabEachEmptyCell { get; set; } /** * Retreives the text contents of the file */ diff --git a/ooxml/NPOI.OOXML.Core.csproj b/ooxml/NPOI.OOXML.Core.csproj index 9b6a9de1f..fb7a63b6a 100644 --- a/ooxml/NPOI.OOXML.Core.csproj +++ b/ooxml/NPOI.OOXML.Core.csproj @@ -13,7 +13,6 @@ - diff --git a/ooxml/POIXMLTextExtractor.cs b/ooxml/POIXMLTextExtractor.cs index ec85b072f..ecfbcaf78 100644 --- a/ooxml/POIXMLTextExtractor.cs +++ b/ooxml/POIXMLTextExtractor.cs @@ -39,21 +39,21 @@ public POIXMLTextExtractor(POIXMLDocument document) /** * Returns the core document properties */ - public CoreProperties GetCoreProperties() + public virtual CoreProperties GetCoreProperties() { return _document.GetProperties().CoreProperties; } /** * Returns the extended document properties */ - public ExtendedProperties GetExtendedProperties() + public virtual ExtendedProperties GetExtendedProperties() { return _document.GetProperties().ExtendedProperties; } /** * Returns the custom document properties */ - public CustomProperties GetCustomProperties() + public virtual CustomProperties GetCustomProperties() { return _document.GetProperties().CustomProperties; } diff --git a/ooxml/XSSF/EventUserModel/XSSFSheetXMLHandler.cs b/ooxml/XSSF/EventUserModel/XSSFSheetXMLHandler.cs index cbab9fc24..71c7a04d9 100644 --- a/ooxml/XSSF/EventUserModel/XSSFSheetXMLHandler.cs +++ b/ooxml/XSSF/EventUserModel/XSSFSheetXMLHandler.cs @@ -281,7 +281,7 @@ public override void StartElement(String uri, String localName, String qName, { rowNum = nextRowNum; } - output.startRow(rowNum); + output.StartRow(rowNum); } // c => cell else if("c".Equals(localName)) @@ -427,7 +427,7 @@ public override void EndElement(String uri, String localName, String qName) XSSFComment comment = commentsTable != null ? commentsTable.FindCellComment(new CellAddress(cellRef)) : null; // Output - output.cell(cellRef, thisStr, comment); + output.Cell(cellRef, thisStr, comment); } else if("f".Equals(localName)) { @@ -443,7 +443,7 @@ public override void EndElement(String uri, String localName, String qName) CheckForEmptyCellComments(EmptyCellCommentsCheckType.EndOfRow); // Finish up the row - output.endRow(rowNum); + output.EndRow(rowNum); // some sheets do not have rowNum Set in the XML, Excel can read them so we should try to read them as well nextRowNum = rowNum + 1; @@ -457,13 +457,13 @@ public override void EndElement(String uri, String localName, String qName) "firstHeader".Equals(localName)) { hfIsOpen = false; - output.headerFooter(headerFooter.ToString(), true, localName); + output.HeaderFooter(headerFooter.ToString(), true, localName); } else if("oddFooter".Equals(localName) || "evenFooter".Equals(localName) || "firstFooter".Equals(localName)) { hfIsOpen = false; - output.headerFooter(headerFooter.ToString(), false, localName); + output.HeaderFooter(headerFooter.ToString(), false, localName); } } @@ -572,7 +572,7 @@ private void CheckForEmptyCellComments(EmptyCellCommentsCheckType type) private void OutputEmptyCellComment(CellAddress cellRef) { XSSFComment comment = commentsTable.FindCellComment(cellRef); - output.cell(cellRef.FormatAsString(), null, comment); + output.Cell(cellRef.FormatAsString(), null, comment); } private enum EmptyCellCommentsCheckType @@ -591,20 +591,20 @@ public interface SheetContentsHandler /// /// A row with the (zero based) row number has started */ /// - public void startRow(int rowNum); + public void StartRow(int rowNum); /// /// A row with the (zero based) row number has ended */ /// - public void endRow(int rowNum); + public void EndRow(int rowNum); /// /// A cell, with the given formatted value (may be null), /// and possibly a comment (may be null), was encountered */ /// - public void cell(String cellReference, String formattedValue, XSSFComment comment); + public void Cell(String cellReference, String formattedValue, XSSFComment comment); /// /// A header or footer has been encountered */ /// - public void headerFooter(String text, bool IsHeader, String tagName); + public void HeaderFooter(String text, bool IsHeader, String tagName); } } } diff --git a/ooxml/XSSF/Extractor/XSSFEventBasedExcelExtractor.cs b/ooxml/XSSF/Extractor/XSSFEventBasedExcelExtractor.cs index e7009b2e5..c0db763ab 100644 --- a/ooxml/XSSF/Extractor/XSSFEventBasedExcelExtractor.cs +++ b/ooxml/XSSF/Extractor/XSSFEventBasedExcelExtractor.cs @@ -1,7 +1,7 @@ /* ==================================================================== Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with - this work for Additional information regarding copyright ownership. + this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -14,110 +14,201 @@ the License. You may obtain a copy of the License at See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ -using NPOI.OpenXml4Net.OPC; + using System; -using NPOI.SS.UserModel; -using NPOI.XSSF.Model; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Text; + namespace NPOI.XSSF.Extractor { + using NPOI; + using NPOI.OpenXml4Net.Exceptions; + using NPOI.OpenXml4Net.OPC; + using NPOI.SS.UserModel; + using NPOI.SS.Extractor; + using NPOI.Util; + using NPOI.XSSF.EventUserModel; + using NPOI.XSSF.Model; + using NPOI.XSSF.UserModel; + using System.Globalization; + using NSAX; + using NSAX.AElfred; + using static NPOI.XSSF.EventUserModel.XSSFSheetXMLHandler; + using NPOI.OpenXml4Net; - /** - * Implementation of a text extractor from OOXML Excel - * files that uses SAX event based parsing. - */ - public class XSSFEventBasedExcelExtractor : POIXMLTextExtractor + /// + /// Implementation of a text extractor from OOXML Excel + /// files that uses SAX event based parsing. + /// + public class XSSFEventBasedExcelExtractor : POIXMLTextExtractor, IExcelExtractor { + + //private static POILogger LOGGER = POILogFactory.GetLogger(XSSFEventBasedExcelExtractor.class); + private OPCPackage container; private POIXMLProperties properties; - private Locale locale; + private CultureInfo locale; + private bool includeTextBoxes = true; private bool includeSheetNames = true; + private bool includeCellComments = false; + private bool includeHeadersFooters = true; private bool formulasNotResults = false; + private bool concatenatePhoneticRuns = true; public XSSFEventBasedExcelExtractor(String path) : this(OPCPackage.Open(path)) { - } - public XSSFEventBasedExcelExtractor(OPCPackage Container) + public XSSFEventBasedExcelExtractor(OPCPackage container) : base(null) { - this.container = Container; - properties = new POIXMLProperties(Container); + this.container = container; + + properties = new POIXMLProperties(container); + } + + public static void main(String[] args) + { + + if(args.Length < 1) + { + Console.WriteLine("Use:"); + Console.WriteLine(" XSSFEventBasedExcelExtractor "); + return; + } + var extractor = new XSSFEventBasedExcelExtractor(args[0]); + Console.WriteLine(extractor.Text); + extractor.Close(); } - /** - * Should sheet names be included? Default is true - */ - public void SetIncludeSheetNames(bool includeSheetNames) + /// + /// Get or Set should sheet names be included? Default is true + /// + public bool IncludeSheetNames { - this.includeSheetNames = includeSheetNames; + get + { + return includeSheetNames; + } + set + { + includeSheetNames = value; + } } - /** - * Should we return the formula itself, and not - * the result it produces? Default is false - */ - public void SetFormulasNotResults(bool formulasNotResults) + + + /// + /// Should we return the formula itself, and not + /// the result it produces? Default is false + /// + public bool FormulasNotResults { - this.formulasNotResults = formulasNotResults; + get { return formulasNotResults; } + set { formulasNotResults = value; } } - public void SetLocale(Locale locale) + /// + /// Should headers and footers be included? Default is true + /// + public bool IncludeHeadersFooters { - this.locale = locale; + get { return includeHeadersFooters; } + set { includeHeadersFooters = value; } } - /** - * Returns the opened OPCPackage Container. - */ - public OPCPackage GetPackage() + /// + /// Should text from textboxes be included? Default is true + /// + public bool IncludeTextBoxes { - return container; + get { return includeTextBoxes; } + set { includeTextBoxes = value; } + } + + /// + /// + /// whether cell comments should be included + /// + /// @since 3.16-beta3 + public bool IncludeCellComments + { + get { return includeCellComments; } + set { this.includeCellComments = value; } } - /** - * Returns the core document properties - */ + public bool AddTabEachEmptyCell { get; set; } = true; - public NPOI.POIXMLProperties.CoreProperties GetCoreProperties() + /// + /// Concatenate text from <rPh> text elements in SharedStringsTable + /// Default is true; + /// + /// concatenatePhoneticRuns + public void SetConcatenatePhoneticRuns(bool concatenatePhoneticRuns) { - return properties.GetCoreProperties(); + this.concatenatePhoneticRuns = concatenatePhoneticRuns; } - /** - * Returns the extended document properties - */ - public NPOI.POIXMLProperties.ExtendedProperties GetExtendedProperties() + /// CultureInfo + /// + public CultureInfo Locale + { + get { return locale; } + set { locale = value; } + } + /// + /// Returns the opened OPCPackage container. + /// + public OPCPackage GetPackage() { - return properties.GetExtendedProperties(); + return container; } - /** - * Returns the custom document properties - */ - public NPOI.POIXMLProperties.CustomProperties GetCustomProperties() + /// + /// Returns the core document properties + /// + public override CoreProperties GetCoreProperties() + { + return properties.CoreProperties; + } + /// + /// Returns the extended document properties + /// + public override ExtendedProperties GetExtendedProperties() + { + return properties.ExtendedProperties; + } + /// + /// Returns the custom document properties + /// + public override CustomProperties GetCustomProperties() { - return properties.GetCustomProperties(); + return properties.CustomProperties; } - /** - * Processes the given sheet - */ + + + /// + /// Processes the given sheet + /// public void ProcessSheet( SheetContentsHandler sheetContentsExtractor, StylesTable styles, + CommentsTable comments, ReadOnlySharedStringsTable strings, - InputStream sheetInputStream) + Stream sheetInputStream) + { + DataFormatter formatter; - if (locale == null) + if(locale == null) { formatter = new DataFormatter(); } @@ -127,94 +218,138 @@ public void ProcessSheet( } InputSource sheetSource = new InputSource(sheetInputStream); - SAXParserFactory saxFactory = SAXParserFactory.newInstance(); try { - SAXParser saxParser = saxFactory.newSAXParser(); - XMLReader sheetParser = saxParser.GetXMLReader(); - ContentHandler handler = new XSSFSheetXMLHandler( - styles, strings, sheetContentsExtractor, formatter, formulasNotResults); - sheetParser.SetContentHandler(handler); + SAXDriver sheetParser = new SAXDriver(); + IContentHandler handler = new XSSFSheetXMLHandler( + styles, comments, strings, sheetContentsExtractor, formatter, formulasNotResults); + sheetParser.ContentHandler = (handler); sheetParser.Parse(sheetSource); } - catch (ParserConfigurationException e) + catch(SAXException e) { - throw new RuntimeException("SAX Parser appears to be broken - " + e.GetMessage()); + throw new RuntimeException("SAX parser appears to be broken - " + e.Message); } } - /** - * Processes the file and returns the text - */ - public String GetText() + /// + /// Processes the file and returns the text + /// + public override String Text { - try + get { - ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(container); - XSSFReader xssfReader = new XSSFReader(container); - StylesTable styles = xssfReader.GetStylesTable(); - XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator)xssfReader.GetSheetsData(); + try + { + ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(container, concatenatePhoneticRuns); + XSSFReader xssfReader = new XSSFReader(container); + StylesTable styles = xssfReader.StylesTable; + XSSFReader.SheetIterator iter = (XSSFReader.SheetIterator) xssfReader.GetSheetsData(); - StringBuilder text = new StringBuilder(); - SheetTextExtractor sheetExtractor = new SheetTextExtractor(text); + StringBuilder text = new StringBuilder(); + SheetTextExtractor sheetExtractor = new SheetTextExtractor(this); - while (iter.HasNext()) - { - InputStream stream = iter.next(); - if (includeSheetNames) + while(iter.MoveNext()) { - text.Append(iter.GetSheetName()); - text.Append('\n'); + Stream stream = iter.Current; + if(includeSheetNames) + { + text.Append(iter.SheetName); + text.Append('\n'); + } + CommentsTable comments = includeCellComments ? iter.SheetComments : null; + ProcessSheet(sheetExtractor, styles, comments, strings, stream); + if(includeHeadersFooters) + { + sheetExtractor.AppendHeaderText(text); + } + sheetExtractor.AppendCellText(text); + if(includeTextBoxes) + { + ProcessShapes(iter.Shapes, text); + } + if(includeHeadersFooters) + { + sheetExtractor.AppendFooterText(text); + } + sheetExtractor.Reset(); + stream.Close(); } - ProcessSheet(sheetExtractor, styles, strings, stream); - stream.Close(); - } - return text.ToString(); + return text.ToString(); + } + catch(IOException) + { + //LOGGER.log(POILogger.WARN, e); + return null; + } + catch(SAXException) + { + //LOGGER.log(POILogger.WARN, se); + return null; + } + catch(OpenXml4NetException) + { + //LOGGER.log(POILogger.WARN, o4je); + return null; + } } - catch (IOException e) + + } + + static void ProcessShapes(List shapes, StringBuilder text) + { + if(shapes == null) { - System.err.println(e); - return null; + return; } - catch (OpenXML4NetException o4je) + foreach(XSSFShape shape in shapes) { - System.err.println(o4je); - return null; + if(shape is XSSFSimpleShape) + { + String sText = ((XSSFSimpleShape)shape).Text; + if(sText != null && sText.Length > 0) + { + text.Append(sText).Append('\n'); + } + } } } - public void Close() + public override void Close() { - if (container != null) + + if(container != null) { container.Close(); container = null; } - base.close(); + base.Close(); } + protected class SheetTextExtractor : SheetContentsHandler { - private StringBuilder output; - private bool firstCellOfRow = true; - - protected SheetTextExtractor(StringBuilder output) + private StringBuilder output; + private bool firstCellOfRow; + private Dictionary headerFooterMap; + private XSSFEventBasedExcelExtractor eb; + public SheetTextExtractor(XSSFEventBasedExcelExtractor eb) { - this.output = output; + this.eb = eb; + this.output = new StringBuilder(); + this.firstCellOfRow = true; + this.headerFooterMap = eb.IncludeHeadersFooters ? new Dictionary() : null; } - - public void startRow(int rowNum) + public void StartRow(int rowNum) { firstCellOfRow = true; } - - public void endRow() + public void EndRow(int rowNum) { output.Append('\n'); } - - public void cell(String cellRef, String formattedValue) + public void Cell(String cellRef, String formattedValue, XSSFComment comment) { - if (firstCellOfRow) + if(firstCellOfRow) { firstCellOfRow = false; } @@ -222,12 +357,118 @@ public void cell(String cellRef, String formattedValue) { output.Append('\t'); } - output.Append(formattedValue); + if(formattedValue != null) + { + eb.CheckMaxTextSize(output, formattedValue); + output.Append(formattedValue); + } + if(eb.IncludeCellComments && comment != null) + { + String commentText = comment.String.String.Replace('\n', ' '); + output.Append(formattedValue != null ? " Comment by " : "Comment by "); + eb.CheckMaxTextSize(output, commentText); + if(commentText.StartsWith(comment.Author + ": ")) + { + output.Append(commentText); + } + else + { + output.Append(comment.Author).Append(": ").Append(commentText); + } + } + } + public void HeaderFooter(String text, bool IsHeader, String tagName) + { + if(headerFooterMap != null) + { + headerFooterMap[tagName] = text; + } } - public void headerFooter(String text, bool IsHeader, String tagName) + /// + /// Append the text for the named header or footer if found. + /// + private void AppendHeaderFooterText(StringBuilder buffer, String name) { - // We don't include headers in the output yet, so ignore + String text = headerFooterMap.TryGetValue(name, out string value) ? value : null; + if(text != null && text.Length > 0) + { + // this is a naive way of handling the left, center, and right + // header and footer delimiters, but it seems to be as good as + // the method used by XSSFExcelExtractor + text = HandleHeaderFooterDelimiter(text, "&L"); + text = HandleHeaderFooterDelimiter(text, "&C"); + text = HandleHeaderFooterDelimiter(text, "&R"); + buffer.Append(text).Append('\n'); + } + } + /// + /// Remove the delimiter if its found at the beginning of the text, + /// or replace it with a tab if its in the middle. + /// + private static String HandleHeaderFooterDelimiter(String text, String delimiter) + { + int index = text.IndexOf(delimiter); + if(index == 0) + { + text = text.Substring(2); + } + else if(index > 0) + { + text = text.Substring(0, index) + "\t" + text.Substring(index + 2); + } + return text; + } + + + /// + /// Append the text for each header type in the same order + /// they are appended in XSSFExcelExtractor. + /// + /// + /// + public void AppendHeaderText(StringBuilder buffer) + { + AppendHeaderFooterText(buffer, "firstHeader"); + AppendHeaderFooterText(buffer, "oddHeader"); + AppendHeaderFooterText(buffer, "evenHeader"); + } + + /// + /// Append the text for each footer type in the same order + /// they are appended in XSSFExcelExtractor. + /// + /// + /// + public void AppendFooterText(StringBuilder buffer) + { + // append the text for each footer type in the same order + // they are appended in XSSFExcelExtractor + AppendHeaderFooterText(buffer, "firstFooter"); + AppendHeaderFooterText(buffer, "oddFooter"); + AppendHeaderFooterText(buffer, "evenFooter"); + } + + /// + /// Append the cell contents we have collected. + /// + public void AppendCellText(StringBuilder buffer) + { + eb.CheckMaxTextSize(buffer, output.ToString()); + buffer.Append(output); + } + + /// + /// Reset this SheetTextExtractor for the next sheet. + /// + public void Reset() + { + output.Length = 0; + firstCellOfRow = true; + if(headerFooterMap != null) + { + headerFooterMap.Clear(); + } } } } diff --git a/ooxml/XSSF/Extractor/XSSFExcelExtractor.cs b/ooxml/XSSF/Extractor/XSSFExcelExtractor.cs index a33762061..cd626ea8d 100644 --- a/ooxml/XSSF/Extractor/XSSFExcelExtractor.cs +++ b/ooxml/XSSF/Extractor/XSSFExcelExtractor.cs @@ -21,6 +21,7 @@ limitations under the License. using NPOI.SS.UserModel; using System.Globalization; using System.Collections.Generic; +using NPOI.Util; namespace NPOI.XSSF.Extractor { @@ -30,10 +31,10 @@ namespace NPOI.XSSF.Extractor public class XSSFExcelExtractor : POIXMLTextExtractor, NPOI.SS.Extractor.IExcelExtractor { public static XSSFRelation[] SUPPORTED_TYPES = new XSSFRelation[] { - XSSFRelation.WORKBOOK, XSSFRelation.MACRO_TEMPLATE_WORKBOOK, - XSSFRelation.MACRO_ADDIN_WORKBOOK, XSSFRelation.TEMPLATE_WORKBOOK, - XSSFRelation.MACROS_WORKBOOK - }; + XSSFRelation.WORKBOOK, XSSFRelation.MACRO_TEMPLATE_WORKBOOK, + XSSFRelation.MACRO_ADDIN_WORKBOOK, XSSFRelation.TEMPLATE_WORKBOOK, + XSSFRelation.MACROS_WORKBOOK + }; private readonly XSSFWorkbook workbook; private readonly DataFormatter dataFormatter; @@ -60,7 +61,7 @@ public XSSFExcelExtractor(XSSFWorkbook workbook) /// /// Should header and footer be included? Default is true /// - public bool IncludeHeaderFooter + public bool IncludeHeadersFooters { get { @@ -129,9 +130,12 @@ public bool IncludeTextBoxes includeTextBoxes = value; } } + public bool AddTabEachEmptyCell { get; set; } = true; /** * Should sheet names be included? Default is true */ + [Obsolete("use property IncludeSheetNames")] + [Removal(Version = "4.0")] public void SetIncludeSheetNames(bool includeSheetNames) { this.includeSheetNames = includeSheetNames; @@ -140,6 +144,8 @@ public void SetIncludeSheetNames(bool includeSheetNames) * Should we return the formula itself, and not * the result it produces? Default is false */ + [Obsolete("use property FormulasNotResults")] + [Removal(Version = "4.0")] public void SetFormulasNotResults(bool formulasNotResults) { this.formulasNotResults = formulasNotResults; @@ -147,6 +153,8 @@ public void SetFormulasNotResults(bool formulasNotResults) /** * Should cell comments be included? Default is false */ + [Obsolete("use property IncludeCellComments")] + [Removal(Version = "4.0")] public void SetIncludeCellComments(bool includeCellComments) { this.includeCellComments = includeCellComments; @@ -154,6 +162,8 @@ public void SetIncludeCellComments(bool includeCellComments) /** * Should headers and footers be included? Default is true */ + [Obsolete("use property IncludeHeadersFooters")] + [Removal(Version = "4.0")] public void SetIncludeHeadersFooters(bool includeHeadersFooters) { this.includeHeadersFooters = includeHeadersFooters; @@ -163,6 +173,8 @@ public void SetIncludeHeadersFooters(bool includeHeadersFooters) * Should text within textboxes be included? Default is true * @param includeTextBoxes */ + [Obsolete("use property IncludeTextBoxes")] + [Removal(Version = "4.0")] public void SetIncludeTextBoxes(bool includeTextBoxes) { this.includeTextBoxes = includeTextBoxes; @@ -223,13 +235,16 @@ public override string Text for (int j = 0; j < row.LastCellNum; j++) { // Add a tab delimiter for each empty cell. - if (!firsttime) + if(AddTabEachEmptyCell) { - text.Append("\t"); - } - else - { - firsttime = false; + if(!firsttime) + { + text.Append("\t"); + } + else + { + firsttime = false; + } } ICell cell = row.GetCell(j); diff --git a/testcases/ooxml/XSSF/Extractor/TestXSSFEventBasedExcelExtractor.cs b/testcases/ooxml/XSSF/Extractor/TestXSSFEventBasedExcelExtractor.cs new file mode 100644 index 000000000..76f2e8082 --- /dev/null +++ b/testcases/ooxml/XSSF/Extractor/TestXSSFEventBasedExcelExtractor.cs @@ -0,0 +1,435 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace TestCases.XSSF.Extractor +{ + + using NPOI; + using NPOI.HSSF; + using NPOI.HSSF.Extractor; + using NPOI.XSSF; + using NPOI.XSSF.Extractor; + using NUnit.Framework; + using NUnit.Framework.Legacy; + using System.Text.RegularExpressions; + using TestCases.HSSF; + + /// + /// Tests for + /// + [TestFixture] + public class TestXSSFEventBasedExcelExtractor + { + protected XSSFEventBasedExcelExtractor GetExtractor(String sampleName) + { + + return new XSSFEventBasedExcelExtractor(XSSFTestDataSamples.OpenSamplePackage(sampleName)); + } + + /// + /// Get text out of the simple file + /// + [Test] + public void TestGetSimpleText() + { + + // a very simple file + XSSFEventBasedExcelExtractor extractor = GetExtractor("sample.xlsx"); + var _ = extractor.Text; + + String text = extractor.Text; + ClassicAssert.IsTrue(text.Length > 0); + + // Check sheet names + POITestCase.AssertStartsWith(text, "Sheet1"); + POITestCase.AssertEndsWith(text, "Sheet3\n"); + + // Now without, will have text + extractor.IncludeSheetNames = (false); + text = extractor.Text; + String CHUNK1 = + "Lorem\t111\n" + + "ipsum\t222\n" + + "dolor\t333\n" + + "sit\t444\n" + + "amet\t555\n" + + "consectetuer\t666\n" + + "adipiscing\t777\n" + + "elit\t888\n" + + "Nunc\t999\n"; + String CHUNK2 = + "The quick brown fox jumps over the lazy dog\n" + + "hello, xssf hello, xssf\n" + + "hello, xssf hello, xssf\n" + + "hello, xssf hello, xssf\n" + + "hello, xssf hello, xssf\n"; + ClassicAssert.AreEqual( + CHUNK1 + + "at\t4995\n" + + CHUNK2 + , text); + + // Now Get formulas not their values + extractor.FormulasNotResults = (true); + text = extractor.Text; + ClassicAssert.AreEqual( + CHUNK1 + + "at\tSUM(B1:B9)\n" + + CHUNK2, text); + + // With sheet names too + extractor.IncludeSheetNames = (true); + text = extractor.Text; + ClassicAssert.AreEqual( + "Sheet1\n" + + CHUNK1 + + "at\tSUM(B1:B9)\n" + + "rich test\n" + + CHUNK2 + + "Sheet3\n" + , text); + + extractor.Close(); + } + + [Test] + public void TestGetComplexText() + { + + // A fairly complex file + XSSFEventBasedExcelExtractor extractor = GetExtractor("AverageTaxRates.xlsx"); + var _ = extractor.Text; + + String text = extractor.Text; + ClassicAssert.IsTrue(text.Length > 0); + + // Might not have all formatting it should do! + POITestCase.AssertStartsWith(text, + "Avgtxfull\n" + + "(iii) AVERAGE TAX RATES ON ANNUAL" + ); + + extractor.Close(); + } + + [Test] + public void TestInlineStrings() + { + + XSSFEventBasedExcelExtractor extractor = GetExtractor("InlineStrings.xlsx"); + extractor.FormulasNotResults = (true); + String text = extractor.Text; + + // Numbers + POITestCase.AssertContains(text, "43"); + POITestCase.AssertContains(text, "22"); + + // Strings + POITestCase.AssertContains(text, "ABCDE"); + POITestCase.AssertContains(text, "Long Text"); + + // Inline Strings + POITestCase.AssertContains(text, "1st Inline String"); + POITestCase.AssertContains(text, "And More"); + + // Formulas + POITestCase.AssertContains(text, "A2"); + POITestCase.AssertContains(text, "A5-A$2"); + + extractor.Close(); + } + + /// + /// Test that we return pretty much the same as + /// ExcelExtractor does, when we're both passed + /// the same file, just saved as xls and xlsx + /// + [Test] + public void TestComparedToOLE2() + { + + // A fairly simple file - ooxml + XSSFEventBasedExcelExtractor ooxmlExtractor = GetExtractor("SampleSS.xlsx"); + + ExcelExtractor ole2Extractor = + new ExcelExtractor(HSSFTestDataSamples.OpenSampleWorkbook("SampleSS.xls")); + + POITextExtractor[] extractors = + new POITextExtractor[] { ooxmlExtractor, ole2Extractor }; + foreach(POITextExtractor extractor in extractors) + { + String text = extractor.Text.Replace("\r", "").Replace("\t", ""); + POITestCase.AssertStartsWith(text, "First Sheet\nTest spreadsheet\n2nd row2nd row 2nd column\n"); + Regex pattern = new Regex(".*13(\\.0+)?\\s+Sheet3.*", RegexOptions.Compiled | RegexOptions.Singleline); + Match m = pattern.Match(text); + ClassicAssert.IsTrue(m.Success); + } + + ole2Extractor.Close(); + ooxmlExtractor.Close(); + } + + /// + /// Test text extraction from text box using GetShapes() + /// + /// Exception + [Test] + public void TestShapes() + { + XSSFEventBasedExcelExtractor ooxmlExtractor = GetExtractor("WithTextBox.xlsx"); + try + { + String text = ooxmlExtractor.Text; + StringAssert.Contains("Line 1", text); + StringAssert.Contains("Line 2", text); + StringAssert.Contains("Line 3", text); + } + finally + { + ooxmlExtractor.Close(); + } + } + + /// + /// Test that we return the same output for unstyled numbers as the + /// non-event-based XSSFExcelExtractor. + /// + [Test] + public void TestUnstyledNumbersComparedToNonEventBasedExtractor() + { + String expectedOutput = "Sheet1\n99.99\n"; + XSSFExcelExtractor extractor = new XSSFExcelExtractor( + XSSFTestDataSamples.OpenSampleWorkbook("56011.xlsx")); + try + { + ClassicAssert.AreEqual(expectedOutput, extractor.Text.Replace(",", ".")); + } + finally + { + extractor.Close(); + } + + XSSFEventBasedExcelExtractor fixture = + new XSSFEventBasedExcelExtractor( + XSSFTestDataSamples.OpenSamplePackage("56011.xlsx")); + try + { + ClassicAssert.AreEqual(expectedOutput, fixture.Text.Replace(",", ".")); + } + finally + { + fixture.Close(); + } + } + + /// + /// Test that we return the same output headers and footers as the + /// non-event-based XSSFExcelExtractor. + /// + [Test] + public void TestHeadersAndFootersComparedToNonEventBasedExtractor() + { + String expectedOutputWithHeadersAndFooters = + "Sheet1\n" + + "&\"Calibri,Regular\"&K000000top left\t&\"Calibri,Regular\"&K000000top center\t&\"Calibri,Regular\"&K000000top right\n" + + "abc\t123\n" + + "&\"Calibri,Regular\"&K000000bottom left\t&\"Calibri,Regular\"&K000000bottom center\t&\"Calibri,Regular\"&K000000bottom right\n"; + + String expectedOutputWithoutHeadersAndFooters = + "Sheet1\n" + + "abc\t123\n"; + + XSSFExcelExtractor extractor = new XSSFExcelExtractor( + XSSFTestDataSamples.OpenSampleWorkbook("headerFooterTest.xlsx")); + try + { + ClassicAssert.AreEqual(expectedOutputWithHeadersAndFooters, extractor.Text); + extractor.IncludeHeadersFooters = (false); + ClassicAssert.AreEqual(expectedOutputWithoutHeadersAndFooters, extractor.Text); + } + finally + { + extractor.Close(); + } + + XSSFEventBasedExcelExtractor fixture = + new XSSFEventBasedExcelExtractor( + XSSFTestDataSamples.OpenSamplePackage("headerFooterTest.xlsx")); + try + { + ClassicAssert.AreEqual(expectedOutputWithHeadersAndFooters, fixture.Text); + fixture.IncludeHeadersFooters = (false); + ClassicAssert.AreEqual(expectedOutputWithoutHeadersAndFooters, fixture.Text); + } + finally + { + fixture.Close(); + } + } + + /// + /// + /// Test that XSSFEventBasedExcelExtractor outputs comments when specified. + /// The output will contain two improvements over the output from + /// XSSFExcelExtractor in that (1) comments from empty cells will be + /// outputted, and (2) the author will not be outputted twice. + /// + /// + /// This test will need to be modified if these improvements are ported to + /// XSSFExcelExtractor. + /// + /// + [Test] + public void TestCommentsComparedToNonEventBasedExtractor() + { + String expectedOutputWithoutComments = + "Sheet1\n" + + "\n" + + "abc\n" + + "\n" + + "123\n" + + "\n" + + "\n" + + "\n"; + + String nonEventBasedExtractorOutputWithComments = + "Sheet1\n" + + "\n" + + "abc Comment by Shaun Kalley: Shaun Kalley: Comment A2\n" + + "\n" + + "123 Comment by Shaun Kalley: Shaun Kalley: Comment B4\n" + + "\n" + + "\n" + + "\n"; + + String eventBasedExtractorOutputWithComments = + "Sheet1\n" + + "Comment by Shaun Kalley: Comment A1\tComment by Shaun Kalley: Comment B1\n" + + "abc Comment by Shaun Kalley: Comment A2\tComment by Shaun Kalley: Comment B2\n" + + "Comment by Shaun Kalley: Comment A3\tComment by Shaun Kalley: Comment B3\n" + + "Comment by Shaun Kalley: Comment A4\t123 Comment by Shaun Kalley: Comment B4\n" + + "Comment by Shaun Kalley: Comment A5\tComment by Shaun Kalley: Comment B5\n" + + "Comment by Shaun Kalley: Comment A7\tComment by Shaun Kalley: Comment B7\n" + + "Comment by Shaun Kalley: Comment A8\tComment by Shaun Kalley: Comment B8\n"; + + XSSFExcelExtractor extractor = new XSSFExcelExtractor( + XSSFTestDataSamples.OpenSampleWorkbook("commentTest.xlsx")); + try + { + extractor.AddTabEachEmptyCell = false; + ClassicAssert.AreEqual(expectedOutputWithoutComments, extractor.Text); + extractor.IncludeCellComments = (true); + ClassicAssert.AreEqual(nonEventBasedExtractorOutputWithComments, extractor.Text); + } + finally + { + extractor.Close(); + } + + XSSFEventBasedExcelExtractor fixture = + new XSSFEventBasedExcelExtractor( + XSSFTestDataSamples.OpenSamplePackage("commentTest.xlsx")); + try + { + ClassicAssert.AreEqual(expectedOutputWithoutComments, fixture.Text); + fixture.IncludeCellComments = (true); + ClassicAssert.AreEqual(eventBasedExtractorOutputWithComments, fixture.Text); + } + finally + { + fixture.Close(); + } + } + + [Test] + public void TestFile56278_normal() + { + + // first with normal Text Extractor + POIXMLTextExtractor extractor = new XSSFExcelExtractor( + XSSFTestDataSamples.OpenSampleWorkbook("56278.xlsx")); + try + { + ClassicAssert.IsNotNull(extractor.Text); + } + finally + { + extractor.Close(); + } + } + + [Test] + public void TestFile56278_event() + { + + // then with event based one + POIXMLTextExtractor extractor = GetExtractor("56278.xlsx"); + try + { + ClassicAssert.IsNotNull(extractor.Text); + } + finally + { + extractor.Close(); + } + } + + [Test] + public void Test59021() + { + + XSSFEventBasedExcelExtractor ex = + new XSSFEventBasedExcelExtractor( + XSSFTestDataSamples.OpenSamplePackage("59021.xlsx")); + String text = ex.Text; + StringAssert.Contains("Abkhazia - Fixed", text); + StringAssert.Contains("10/02/2016", text); + ex.Close(); + } + + [Test] + public void Test51519() + { + + //default behavior: include phonetic runs + XSSFEventBasedExcelExtractor ex = + new XSSFEventBasedExcelExtractor( + XSSFTestDataSamples.OpenSamplePackage("51519.xlsx")); + String text = ex.Text; + StringAssert.Contains("\u65E5\u672C\u30AA\u30E9\u30AF\u30EB \u30CB\u30DB\u30F3", text); + ex.Close(); + + //now try turning them off + ex = new XSSFEventBasedExcelExtractor( + XSSFTestDataSamples.OpenSamplePackage("51519.xlsx")); + ex.SetConcatenatePhoneticRuns(false); + text = ex.Text; + ClassicAssert.IsFalse(text.Contains("\u65E5\u672C\u30AA\u30E9\u30AF\u30EB \u30CB\u30DB\u30F3"), + "should not be able to find appended phonetic run"); + ex.Close(); + + } + } +} + diff --git a/testcases/ooxml/XSSF/Extractor/TestXSSFExcelExtractor.cs b/testcases/ooxml/XSSF/Extractor/TestXSSFExcelExtractor.cs index 0c60d000d..23c580f5a 100644 --- a/testcases/ooxml/XSSF/Extractor/TestXSSFExcelExtractor.cs +++ b/testcases/ooxml/XSSF/Extractor/TestXSSFExcelExtractor.cs @@ -55,7 +55,7 @@ public void TestGetSimpleText() ClassicAssert.IsTrue(text.EndsWith("Sheet3\n")); // Now without, will have text - extractor.SetIncludeSheetNames(false); + extractor.IncludeSheetNames = false; text = extractor.Text; string CHUNK1 = "Lorem\t111\n" + @@ -80,7 +80,7 @@ public void TestGetSimpleText() , text); // Now Get formulas not their values - extractor.SetFormulasNotResults(true); + extractor.FormulasNotResults = true; text = extractor.Text; ClassicAssert.AreEqual( CHUNK1 + @@ -88,7 +88,7 @@ public void TestGetSimpleText() CHUNK2, text); // With sheet names too - extractor.SetIncludeSheetNames(true); + extractor.IncludeSheetNames = true; text = extractor.Text; ClassicAssert.AreEqual( "Sheet1\n" + @@ -184,7 +184,7 @@ public void TestComments() ClassicAssert.IsFalse(text.Contains("test phrase"), "Unable to find expected word in text\n" + text); // Turn on comment extraction, will then be - extractor.SetIncludeCellComments(true); + extractor.IncludeCellComments = true; text = extractor.Text; ClassicAssert.IsTrue(text.Contains("testdoc"), "Unable to find expected word in text\n" + text); ClassicAssert.IsTrue(text.Contains("test phrase"), "Unable to find expected word in text\n" + text); @@ -256,7 +256,7 @@ public void TestTextBoxes() XSSFExcelExtractor extractor = GetExtractor("WithTextBox.xlsx"); try { - extractor.SetFormulasNotResults(true); + extractor.FormulasNotResults = true; string text = extractor.Text; ClassicAssert.IsTrue(text.IndexOf("Line 1") > -1); ClassicAssert.IsTrue(text.IndexOf("Line 2") > -1);