wordpress hit counter
XSL transformation of SpreadsheetML to generic XML - OpenXML Developer - Blog - OpenXML Developer
Goodbye and Hello

OpenXmlDeveloper.org is Shutting Down

There is a time for all good things to come to an end, and the time has come to shut down OpenXmlDeveloper.org.

Screen-casts and blog posts: Content on OpenXmlDeveloper.org will be moving to EricWhite.com.

Forums: We are moving the forums to EricWhite.com and StackOverflow.com. Please do not post in the forums on OpenXmlDeveloper.org. Instead, please post in the forums at EricWhite.com or at StackOverflow.com.

Please see this blog post for more information about my plans moving forward.  Cheers, Eric

XSL transformation of SpreadsheetML to generic XML

XSL transformation of SpreadsheetML to generic XML

Rate This
  • Comments 6

By MuthuKumar Arjunan of Sonata Software Limited

 

This article explains how a SpreadsheetML document (.xlsx file) can be converted into a generic xml data source file that can be used by heterogeneous systems.  It then binds this generic xml data source to an ASP.NET data grid to display the data in a browser.

 

XSL Transformation of Spreadsheet ML into a generic XML data source file:

 

The Spreadsheet ML document is first converted to a generic xml file format using XSL transformation.  This generic xml file format is used as a data source for data grid through which the data contained in the spreadsheet ML is shown in browser.

 

Steps to achieve it:

  • The ASP Net application (downloadable) merges the Spreadsheet ML files like Sheet1.xml, Sharedstrings.xml, Styles.xml.

 

§         Sharedstrings.xml :  This Spreadsheet ML file holds all the string data (data type: string) typed into the excel file.

§         SheetX.xml : This Spreadsheet ML file holds the numeric and date values (Date stored as Julian integer).It refers to Sharedstrings.xml on encountering string data type.

§         Styles.xml : This Spreadsheet ML file holds the formatting and style information.

§         alphabets.xml : Besides all the Spreadsheet ML files mentioned above, another utility file called “alphabets.xml” is used by the XSL internally.

This file holds a list of alphabets.

  

  • It then uses the XSL (shown below) and transforms the merged Spreadsheet ML files into a common xml file format that can be used as data source for various heterogeneous applications.

 

The XSL used for Conversion is:

 

<xsl:stylesheet

  xmlns:sps="http://schemas.openxmlformats.org/spreadsheetml/2006/5/main"

  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"

  xmlns:ConvJulianDate="http://mycompany.com/mynamespace"

  xmlns:msxsl="urn:schemas-microsoft-com:xslt"

  extension-element-prefixes="msxsl"

  exclude-result-prefixes="ConvJulianDate"

  version="1.0" >

  <xsl:output method="xml" />

  <xsl:param name="strDateFormatParam">dd/mm/yyyy</xsl:param>

  <msxsl:script language="c#" implements-prefix="ConvJulianDate">

 

    public string getDateFromJulianInteger(string strJulianNumber,string strDateFormatParam,string formatCodeValue)

    {

    string strReturnDate = "1/1/2001";

    try

    {

    int nSerialDate = Convert.ToInt32(strJulianNumber);

    int nDay = 0;

    int nMonth = 0;

    int nYear = 0;

 

    // Excel/Lotus 123 have a bug with 29-02-1900. 1900 is not a

    // leap year, but Excel/Lotus 123 think it is...

    if (nSerialDate == 60)

    {

    nDay = 29;

    nMonth = 2;

    nYear = 1900;

    strReturnDate = nMonth.ToString() + "/" + nDay.ToString() + "/" + nYear.ToString();

 

    }

    else

    {

    if (nSerialDate &lt; 60)

    {

    // Because of the 29-02-1900 bug, any serial date

    // under 60 is one off... Compensate.

    nSerialDate++;

    }

 

    // Modified Julian to DMY calculation with an addition of 2415019

    int l = nSerialDate + 68569 + 2415019;

    int n = (int)((4 * l) / 146097);

    l = l - (int)((146097 * n + 3) / 4);

    int i = (int)((4000 * (l + 1)) / 1461001);

    l = l - (int)((1461 * i) / 4) + 31;

    int j = (int)((80 * l) / 2447);

    nDay = l - (int)((2447 * j) / 80);

    l = (int)(j / 11);

    nMonth = j + 2 - (12 * l);

    nYear = 100 * (n - 49) + i + l;

 

    strReturnDate = nMonth.ToString() + "/" + nDay.ToString() + "/" + nYear.ToString();

 

    string strMonth = nMonth.ToString();

    string strDate = nDay.ToString();

    string strYear = nYear.ToString();

    if (strDateFormatParam.IndexOf("yyyy") > -1)

    {

    if (strDateFormatParam.IndexOf("mm") > -1)

    {

    if (strMonth.Length &lt; 2)

    {

    strMonth = "0" + strMonth.Trim();

    }

    }

    if(strDateFormatParam.IndexOf("dd") >  -1)

    {

    if (strDate.Length &lt; 2)

    {

    strDate = "0" + strDate.Trim();

    }

    }

    strDateFormatParam = strDateFormatParam.Replace("mm", strMonth);

    strDateFormatParam = strDateFormatParam.Replace("dd", strDate);

    strDateFormatParam = strDateFormatParam.Replace("yyyy", strYear);

    strReturnDate =strDateFormatParam;

 

    }

    else if (strDateFormatParam.IndexOf("yy") > -1)

    {

    strYear = strYear.Substring(strYear.Length - 2, 2);

 

    if (strDateFormatParam.IndexOf("mm") > -1)

    {

    if (strMonth.Length &lt; 2)

    {

    strMonth = "0" + strMonth.Trim();

    }

    }

    if (strDateFormatParam.IndexOf("dd") > -1)

    {

    if (strDate.Length &lt; 2)

    {

    strDate = "0" + strDate.Trim();

    }

    }

    strDateFormatParam = strDateFormatParam.Replace("mm", strMonth);

    strDateFormatParam = strDateFormatParam.Replace("dd", strDate);

    strDateFormatParam = strDateFormatParam.Replace("yy", strYear);

    strReturnDate = strDateFormatParam;

 

    }

    else

    {

    strReturnDate = nMonth.ToString() + "/" + nDay.ToString() + "/" + nYear.ToString();

    }

 

    }

    }

    catch (Exception ex)

    {

    strReturnDate = ex.Message.ToString();

    }

 

 

 

    return strReturnDate;

 

    }

  </msxsl:script>

 

  <xsl:variable name="ColNodeList" select="//sps:sheetData//sps:c"></xsl:variable>

  <xsl:variable name="RowNodeList" select="//sps:sheetData/sps:row"></xsl:variable>

  <xsl:variable name="SharedStringNodeList" select="//sps:si"></xsl:variable>

  <xsl:variable name="NumFmtNodeList" select="//sps:numFmts/sps:numFmt"></xsl:variable>

  <xsl:variable name="StyleNodeList" select="//sps:cellXfs/sps:xf"></xsl:variable>

  <xsl:variable name="AlphabetNodeList" select=" //sps:ALPHABET/sps:LETTER"></xsl:variable>

  <xsl:variable name ="AlphabetCount" select="count(//sps:ALPHABET/sps:LETTER)"></xsl:variable>

  <xsl:variable name="XLMaxNumCols">

    <xsl:call-template name="nodeWithMaxChildren">

      <xsl:with-param name="theNodes" select="//sps:row"/>

      <xsl:with-param name="startIndexVal" select="1"/>

 

    </xsl:call-template>

  </xsl:variable>

 

 

  <xsl:template match="/">

   

    <root>

      <xsl:for-each select="//sps:sheetData/sps:row[sps:c[sps:v]]">

        <XLRow>

          <xsl:call-template name="GenerateEmptyCellsforPadding">

            <xsl:with-param name="RowNumber" select="@r"/>

            <xsl:with-param name="AlphabetIndex">1</xsl:with-param>

          </xsl:call-template>

 

 

          <xsl:for-each select="sps:c[sps:v]">

            <xsl:variable name="AlphabetNodeName">

              <xsl:call-template  name="GetLetters">

                <xsl:with-param name="CheckString" select ="./@r"/>

              </xsl:call-template>

            </xsl:variable>

           

                           

            <xsl:if test="count(sps:v)=1">

 

              <xsl:choose>

 

                <xsl:when test="count(self::*[@t='s'])=1">

                  <!--@t='s' This should be a string

                        Hence we take it from   sharedstring.xml  -->

                  <xsl:element name="{$AlphabetNodeName}">

                  <xsl:call-template name="GetSharedStringValue">

                    <xsl:with-param name="StringIndex" select="."/>

                    <xsl:with-param name="xfIndexValue" select="self::node()/@s"/>

                  </xsl:call-template>

                  </xsl:element>

                </xsl:when>

 

                <xsl:when test="count(self::node()/@s)=1">

                  <!--@s This has a reference to style

                        Hence we look for its style inside styles.xml-->

                  <xsl:element name="{$AlphabetNodeName}">

                  <xsl:call-template name="GetStyleFromStyleXml">

                    <xsl:with-param name="CellValue" select="."/>

                    <xsl:with-param name="xfIndexValue" select="self::node()/@s"/>

                  </xsl:call-template>

                  </xsl:element>

                </xsl:when>

                <xsl:when test="count(sps:v)&gt; 0">

                  <xsl:element name="{$AlphabetNodeName}">

                    <!--Value is directly written into Sheet.xml(Usually Numeric) -->

                    <!--Numeric Val -->

                    <xsl:value-of select="sps:v"/>

                    <!--<xsl:copy-of select="node()" />-->

                  </xsl:element>

                </xsl:when>

                <xsl:otherwise>

                  <xsl:element name="{$AlphabetNodeName}">

                    <!--blank cell-->

                    -

                  </xsl:element>

                </xsl:otherwise>

 

              </xsl:choose>

 

            </xsl:if>

            <xsl:choose>

              <xsl:when test="position()=last()">

                <xsl:if test="position()&lt; number($XLMaxNumCols)">

                  <!--Check if the start cell has value-if not, add an empty cell-->

 

                  <xsl:call-template name="GenerateEmptyCellsTillMaxColumns">

                    <xsl:with-param name="CapitalAlphabetValue" >

                      <xsl:call-template  name="GetLetters">

                        <xsl:with-param name="CheckString" select ="./@r"/>

                      </xsl:call-template>

                    </xsl:with-param>

                  </xsl:call-template>

                </xsl:if>

              </xsl:when>

              <xsl:otherwise>

                <!--Check if the next cell is present,if not,then create an empty cell-->

 

                <xsl:call-template name="GenerateConsequetiveEmptyCells">

                  <xsl:with-param name="CapitalAlphabetValue" >

                    <xsl:call-template  name="GetLetters">

                      <xsl:with-param name="CheckString" select ="./@r"/>

                    </xsl:call-template>

                  </xsl:with-param>

                  <xsl:with-param name="RowNumberValue" >

                    <xsl:call-template  name="GetNumbers">

                      <xsl:with-param name="CheckString" select ="./@r"/>

                    </xsl:call-template>

                  </xsl:with-param>

                 

                </xsl:call-template>

 

              </xsl:otherwise>

            </xsl:choose>

           

          </xsl:for-each>

 

        </XLRow>

 

      </xsl:for-each>

    </root>

  </xsl:template>

 

  <xsl:template name="GetStyleFromStyleXml">

    <xsl:param name="CellValue"/>

    <xsl:param name="xfIndexValue"/>

  

 

 

      <xsl:for-each select="$StyleNodeList[position()=(number($xfIndexValue)+1)]">

       

        <xsl:call-template name="GetformatCodeFromNumFmtInStyleXml">

          <xsl:with-param name="CellValue" select="$CellValue"/>

          <xsl:with-param name="numFmtIdValue" select="@numFmtId"/>

        </xsl:call-template>

    

      </xsl:for-each>

  

  </xsl:template>

 

  <xsl:template name="GetformatCodeFromNumFmtInStyleXml">

    <xsl:param name="CellValue"/>

    <xsl:param name="numFmtIdValue"/>

  

    <xsl:choose>

      <xsl:when test="count($NumFmtNodeList[@numFmtId=$numFmtIdValue]/@formatCode) &gt; 0">

        <!--Count is gt 0-->

        <xsl:call-template name="GetDateFromJulianInteger">

          <xsl:with-param name="JulianInteger" select="$CellValue"/>

          <xsl:with-param name="formatCodeValue" select="$NumFmtNodeList[@numFmtId=$numFmtIdValue]/@formatCode"/>

        </xsl:call-template>

      </xsl:when>

      <xsl:otherwise>

        <xsl:choose>

          <xsl:when test="number($numFmtIdValue)=14">

            <xsl:call-template name="GetDateFromJulianInteger">

              <xsl:with-param name="JulianInteger" select="$CellValue"/>

              <xsl:with-param name="formatCodeValue" >mm/dd/yyyy</xsl:with-param>

            </xsl:call-template>

          </xsl:when>

          <xsl:when test="number($numFmtIdValue)=17">

            <xsl:call-template name="GetDateFromJulianInteger">

              <xsl:with-param name="JulianInteger" select="$CellValue"/>

              <xsl:with-param name="formatCodeValue" >mm/dd/yyyy</xsl:with-param>

            </xsl:call-template>

          </xsl:when>

          <xsl:otherwise>

            <xsl:value-of select="$CellValue"></xsl:value-of>

          </xsl:otherwise>

        </xsl:choose>

 

      </xsl:otherwise>

    </xsl:choose>

  </xsl:template>

 

 

  <xsl:template name="GetDateFromJulianInteger">

    <xsl:param name="JulianInteger"/>

    <xsl:param name="formatCodeValue"/>

    <!--Getting Julian date-->

       <xsl:choose>

      <xsl:when test="contains($formatCodeValue,'yy')">

        <xsl:value-of select="ConvJulianDate:getDateFromJulianInteger($JulianInteger,$strDateFormatParam,$formatCodeValue)"/>

      </xsl:when>

      <xsl:otherwise>

        <xsl:value-of select="$JulianInteger"></xsl:value-of>

      </xsl:otherwise>

    </xsl:choose>

  </xsl:template>

 

 

 

  <xsl:template name="GetSharedStringValue">

    <xsl:param name="StringIndex"/>

    <xsl:param name="xfIndexValue"/>

      <xsl:for-each select="//sps:si">

        <xsl:if test="position()=(number($StringIndex)+1)">

         <xsl:apply-templates></xsl:apply-templates>

        </xsl:if>

      </xsl:for-each>

    <!--</xsl:element>-->

  </xsl:template>

  <xsl:template match="sps:t">

 

    <xsl:value-of select="text()"/>

  </xsl:template>

  <xsl:template name="GetLetters">

    <xsl:param name="CheckString"/>

    <xsl:choose>

      <xsl:when test="number(string-length($CheckString)) &gt; 0">

        <xsl:choose>

          <xsl:when test="string(number(substring($CheckString,1,1)))='NaN'">

            <xsl:value-of select="substring($CheckString,1,1)"/>

          </xsl:when>

          <xsl:otherwise>

 

          </xsl:otherwise>

        </xsl:choose>

 

        <xsl:call-template name="GetLetters">

          <xsl:with-param name="CheckString" select="substring($CheckString,2,number(string-length($CheckString))-1)" />

 

        </xsl:call-template>

      </xsl:when>

      <xsl:otherwise>

      </xsl:otherwise>

    </xsl:choose>

 

 

  </xsl:template>

  <xsl:template name="GetNumbers">

    <xsl:param name="CheckString"/>

    <xsl:choose>

      <xsl:when test="number(string-length($CheckString)) &gt; 0">

        <xsl:choose>

          <xsl:when test="string(number(substring($CheckString,1,1)))='NaN'">

 

          </xsl:when>

          <xsl:otherwise>

            <xsl:value-of select="substring($CheckString,1,1)"/>

          </xsl:otherwise>

        </xsl:choose>

 

        <xsl:call-template name="GetNumbers">

          <xsl:with-param name="CheckString" select="substring($CheckString,2,number(string-length($CheckString))-1)" />

 

        </xsl:call-template>

      </xsl:when>

      <xsl:otherwise>

      </xsl:otherwise>

    </xsl:choose>

 

 

  </xsl:template>

 

  <xsl:template name="GenerateEmptyCellsforPadding">

    <xsl:param name="RowNumber"/>

    <xsl:param name="AlphabetIndex"/>

    <xsl:if test="$AlphabetIndex &lt;$AlphabetCount">

    <xsl:variable name="Alphabet" select="$AlphabetNodeList[number($AlphabetIndex)]"></xsl:variable>

    <xsl:if test="count($RowNodeList/sps:c[@r=concat($Alphabet,$RowNumber)])=0">

      <xsl:element name="{$Alphabet}">-</xsl:element>

       

      <xsl:call-template name="GenerateEmptyCellsforPadding">

        <xsl:with-param name="RowNumber" select="$RowNumber"/>

        <xsl:with-param name="AlphabetIndex" select="number($AlphabetIndex)+1"></xsl:with-param>

      </xsl:call-template>

    </xsl:if>

    </xsl:if>

  </xsl:template>

  <xsl:template name="GenerateAlphabetNodes">

    <xsl:param name="CapitalAlphabetValue"/>

  

    <xsl:for-each select="$AlphabetNodeList[.=$CapitalAlphabetValue]">

      <xsl:if test="(number(count($AlphabetNodeList[.=$CapitalAlphabetValue]/preceding-sibling::*)) + 1  &lt; $XLMaxNumCols)">

        <xsl:element name="{$CapitalAlphabetValue}">

          <!--blank--> -

        </xsl:element>

 

        <xsl:call-template name="GenerateAlphabetNodes">

          <xsl:with-param name="CapitalAlphabetValue" select="following-sibling::*[1]"/>

        </xsl:call-template>

      </xsl:if>

    </xsl:for-each>

 

  </xsl:template>

 

 

  <xsl:template name="GenerateEmptyCellsTillMaxColumns">

    <xsl:param name="CapitalAlphabetValue"/>

  

    <xsl:for-each select="$AlphabetNodeList[.=$CapitalAlphabetValue]">

      <xsl:if test="(number(count($AlphabetNodeList[.=$CapitalAlphabetValue]/preceding-sibling::*)) + 1  &lt; $XLMaxNumCols)">

 

        <xsl:call-template name="GenerateAlphabetNodes">

          <xsl:with-param name="CapitalAlphabetValue" select="following-sibling::*[1]"/>

        </xsl:call-template>

      </xsl:if>

    </xsl:for-each>

 

  </xsl:template>

 

   <xsl:template name="GenerateConsequetiveEmptyCells">

    <xsl:param name="CapitalAlphabetValue"/>

    <xsl:param name="RowNumberValue"/>

    <xsl:if test="count($ColNodeList[@r=string(concat($CapitalAlphabetValue,$RowNumberValue))]/sps:v)=0">

      <xsl:for-each select="$AlphabetNodeList[.=$CapitalAlphabetValue]">

       

        <xsl:if test="(number(count($AlphabetNodeList[.=$CapitalAlphabetValue]/preceding-sibling::*)) + 1  &lt; $XLMaxNumCols)">

          <xsl:element name="{$CapitalAlphabetValue}">

            <!--blank--> -

          </xsl:element>

          <xsl:call-template name="GenerateEmptyCellsTillMaxColumns">

            <xsl:with-param name="CapitalAlphabetValue" select="following-sibling::*[1]"/>

           

          </xsl:call-template>

        </xsl:if>

      </xsl:for-each>

    </xsl:if>

 

  </xsl:template>

  <xsl:template name="CheckNextCellToGenerateEmptyCell">

    <xsl:param name="CapitalAlphabetValue"/>

    <xsl:param name="RowNumberValue"/>

 

    <xsl:for-each select="$AlphabetNodeList[.=$CapitalAlphabetValue]">

      <xsl:call-template name="CheckNGenerateEmptyCell">

        <xsl:with-param name="CapitalAlphabetValue" select="following-sibling::*[1]"/>

        <xsl:with-param name="RowNumberValue" select="$RowNumberValue"/>

      </xsl:call-template>

 

    </xsl:for-each>

 

  </xsl:template>

 

  <xsl:template name="CheckNGenerateEmptyCell">

    <xsl:param name="CapitalAlphabetValue"/>

    <xsl:param name="RowNumberValue"/>

    <xsl:if test="count($ColNodeList[@r=string(concat($CapitalAlphabetValue,$RowNumberValue))]/sps:v)=0">

      <xsl:element name="{$CapitalAlphabetValue}">

        -

      </xsl:element>

 

      <xsl:for-each select="$AlphabetNodeList[.=$CapitalAlphabetValue]">

        <xsl:call-template name="CheckNGenerateEmptyCell">

          <xsl:with-param name="CapitalAlphabetValue" select="following-sibling::*[1]"/>

          <xsl:with-param name="RowNumberValue" select="$RowNumberValue"/>

        </xsl:call-template>

      </xsl:for-each>

    </xsl:if>

 

  </xsl:template>

 

  <xsl:template name="CheckNGenerateEmptyRows">

    <xsl:param name="RowNumberValue"/>

    <xsl:choose>

      <xsl:when test="count($RowNodeList[@r=$RowNumberValue])=0">

        <XLRow>

          <xsl:for-each select="$AlphabetNodeList">

            <xsl:if test="position()&lt; number($XLMaxNumCols)">

              <xsl:element name="{$RowNumberValue}">

                <!--blankRow--> -

           </xsl:element>

            </xsl:if>

          </xsl:for-each>

        </XLRow>

        <xsl:call-template name="CheckNGenerateEmptyRows">

          <xsl:with-param name="RowNumberValue" select="number($RowNumberValue)+1"/>

        </xsl:call-template>

      </xsl:when>

      <xsl:otherwise>

 

      </xsl:otherwise>

    </xsl:choose>

  </xsl:template>

 

  <xsl:template name="nodeWithMaxChildren">

    <xsl:param name="theNodes"/>

    <xsl:param name="startIndexVal" />

 

    <xsl:if test="$theNodes[$startIndexVal]">

      <xsl:variable name="myNumChildren"

        select="count($theNodes[$startIndexVal]/sps:c)"/>

      <xsl:choose>

        <xsl:when test="$theNodes[number($startIndexVal)+1]">

 

          <xsl:variable name="max">

            <xsl:call-template name="nodeWithMaxChildren">

              <xsl:with-param name="theNodes"

                              select="$theNodes" />

              <xsl:with-param name="startIndexVal"

                              select="number($startIndexVal)+1" />

 

            </xsl:call-template>

          </xsl:variable>

 

          <xsl:choose>

            <xsl:when test="$max &gt; $myNumChildren">

              <xsl:value-of select="$max" />

            </xsl:when>

            <xsl:otherwise>

              <xsl:value-of select="$myNumChildren" />

            </xsl:otherwise>

          </xsl:choose>

 

        </xsl:when>

        <xsl:otherwise>

          <xsl:value-of select="$myNumChildren" />

        </xsl:otherwise>

      </xsl:choose>

    </xsl:if>

  </xsl:template>

</xsl:stylesheet>

 

 

 

  • Date Conversion in XSL:

The dates in Spreadsheet ML are stored as Julian integer. The date formats are specified as an input param (strDateFormatParam) to XSL. The optional format values for strDateFormatParam are “dd/mm/yy”,”dd/mm/yyyy”,”mm/dd/yy”...Etc.

 

 

 

  • The generic xml file output after transformation using the XSL (above) is like this:

  

<?xml version="1.0" encoding="utf-8" ?>

<root xmlns:sps="http://schemas.openxmlformats.org/spreadsheetml/2006/5/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">

<XLRow>

      <A>dataA1</A>

      <B>dataB1</B>

      <C> 01/01/05 </C>

</XLRow>

<XLRow>

<A>dataA2</A>

      <B>dataB2</B>

      <C> 01/01/06</C>

 

</XLRow>

………………

……………..

</root>

 

 

 

  • Binding the generic xml data source file (above) to ASP Net Data grid.

 

The ASP Net application (downloadable) then binds the xml data source file generated (above) to a data grid.

This data grid which contains the information stored in the Spreadsheet ML document is displayed in browser.

 

 

 

  • Spreadsheet ML data displayed in AspNet data grid:

 

 

The ASPNet application GUI (downloadable) :

 

 

  

Browse to select the Spreadsheet ML document.  Click on “Convert to XML Format” button to view the xml file (which can be used as data source for other heterogeneous applications).  Click on “Convert2XML and Display in Data Grid” button to generate the xml file (data source), internally and bind it to a datagrid which is then displayed in a browser.

 

Note that the XSL has to be slightly tuned for other heterogeneous applications. The xml file generated after transformation of SpreadsheetML can be used to display the data from the SpreadsheetML document in a GUI, demonstrating one of the ways that SpreadsheetML is interoperable across heterogeneous platforms.

 

 

 

 

 

Attachment: Downloads.zip
Page 1 of 1 (6 items)