├── .gitignore ├── testfile.xlsx ├── testrels.xlsx ├── macExcelTest.xlsx ├── googleDocsTest.xlsx ├── macNumbersTest.xlsx ├── wpsBlankLineTest.xlsx ├── common_test.go ├── doc.go ├── googleDocsExcel_test.go ├── macNumbers_test.go ├── macExcel_test.go ├── wpsBlankLine_test.go ├── sharedstrings_test.go ├── sharedstrings.go ├── README.org ├── worksheet.go ├── workbook_test.go ├── style.go ├── worksheet_test.go ├── workbook.go ├── lib_test.go └── lib.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /testfile.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/goxlsx/master/testfile.xlsx -------------------------------------------------------------------------------- /testrels.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/goxlsx/master/testrels.xlsx -------------------------------------------------------------------------------- /macExcelTest.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/goxlsx/master/macExcelTest.xlsx -------------------------------------------------------------------------------- /googleDocsTest.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/goxlsx/master/googleDocsTest.xlsx -------------------------------------------------------------------------------- /macNumbersTest.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/goxlsx/master/macNumbersTest.xlsx -------------------------------------------------------------------------------- /wpsBlankLineTest.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/goxlsx/master/wpsBlankLineTest.xlsx -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | "testing" 6 | ) 7 | 8 | func Test(t *testing.T) { TestingT(t) } 9 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // xslx is a package designed to help with reading data from 2 | // spreadsheets stored in the XLSX format used in recent versions of 3 | // Microsoft's Excel spreadsheet. 4 | // 5 | // For a concise example of how to use this library why not check out 6 | // the source for xlsx2csv here: https://github.com/tealeg/xlsx2csv 7 | 8 | package xlsx 9 | -------------------------------------------------------------------------------- /googleDocsExcel_test.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import . "gopkg.in/check.v1" 4 | 5 | type GoogleDocsExcelSuite struct{} 6 | 7 | var _ = Suite(&GoogleDocsExcelSuite{}) 8 | 9 | // Test that we can successfully read an XLSX file generated by 10 | // Google Docs. 11 | func (g *GoogleDocsExcelSuite) TestGoogleDocsExcel(c *C) { 12 | xlsxFile, err := OpenFile("googleDocsTest.xlsx") 13 | c.Assert(err, IsNil) 14 | c.Assert(xlsxFile, NotNil) 15 | } 16 | -------------------------------------------------------------------------------- /macNumbers_test.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | ) 6 | 7 | type MacNumbersSuite struct{} 8 | 9 | var _ = Suite(&MacNumbersSuite{}) 10 | 11 | // Test that we can successfully read an XLSX file generated by 12 | // Numbers for Mac. 13 | func (m *MacNumbersSuite) TestMacNumbers(c *C) { 14 | xlsxFile, err := OpenFile("macNumbersTest.xlsx") 15 | c.Assert(err, IsNil) 16 | c.Assert(xlsxFile, NotNil) 17 | s := xlsxFile.Sheets[0].Cell(0, 0).String() 18 | c.Assert(s, Equals, "编号") 19 | } 20 | -------------------------------------------------------------------------------- /macExcel_test.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | ) 6 | 7 | type MacExcelSuite struct{} 8 | 9 | var _ = Suite(&MacExcelSuite{}) 10 | 11 | // Test that we can successfully read an XLSX file generated by 12 | // Microsoft Excel for Mac. In particular this requires that we 13 | // respect the contents of workbook.xml.rels, which maps the sheet IDs 14 | // to their internal file names. 15 | func (m *MacExcelSuite) TestMacExcel(c *C) { 16 | xlsxFile, err := OpenFile("macExcelTest.xlsx") 17 | c.Assert(err, IsNil) 18 | c.Assert(xlsxFile, NotNil) 19 | s := xlsxFile.Sheets[0].Cell(0, 0).String() 20 | c.Assert(s, Equals, "编号") 21 | } 22 | -------------------------------------------------------------------------------- /wpsBlankLine_test.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | . "gopkg.in/check.v1" 5 | ) 6 | 7 | type WpsBlankLineSuite struct{} 8 | 9 | var _ = Suite(&WorksheetSuite{}) 10 | 11 | // Test that we can successfully read an XLSX file generated by 12 | // Wps on windows. you can download it freely from http://www.wps.cn/ 13 | func (w *WpsBlankLineSuite) TestWpsBlankLine(c *C) { 14 | xlsxFile, err := OpenFile("wpsBlankLineTest.xlsx") 15 | c.Assert(err, IsNil) 16 | c.Assert(xlsxFile, NotNil) 17 | sheet := xlsxFile.Sheets[0] 18 | row := sheet.Rows[0] 19 | cell := row.Cells[0] 20 | s := cell.String() 21 | expected := "编号" 22 | c.Assert(s, Equals, expected) 23 | 24 | row = sheet.Rows[2] 25 | cell = row.Cells[0] 26 | s = cell.String() 27 | c.Assert(s, Equals, expected) 28 | 29 | row = sheet.Rows[4] 30 | cell = row.Cells[1] 31 | s = cell.String() 32 | c.Assert(s, Equals, "") 33 | 34 | s = sheet.Rows[4].Cells[2].String() 35 | c.Assert(s, Equals, expected) 36 | } 37 | -------------------------------------------------------------------------------- /sharedstrings_test.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | type SharedStringsSuite struct { 10 | SharedStringsXML *bytes.Buffer 11 | } 12 | 13 | var _ = Suite(&SharedStringsSuite{}) 14 | 15 | func (s *SharedStringsSuite) SetUpTest(c *C) { 16 | s.SharedStringsXML = bytes.NewBufferString( 17 | ` 18 | 21 | 22 | Foo 23 | 24 | 25 | Bar 26 | 27 | 28 | Baz 29 | 30 | 31 | Quuk 32 | 33 | `) 34 | } 35 | 36 | // Test we can correctly convert a xlsxSST into a reference table 37 | // using xlsx.MakeSharedStringRefTable(). 38 | func (s *SharedStringsSuite) TestMakeSharedStringRefTable(c *C) { 39 | sst := new(xlsxSST) 40 | err := xml.NewDecoder(s.SharedStringsXML).Decode(sst) 41 | c.Assert(err, IsNil) 42 | reftable := MakeSharedStringRefTable(sst) 43 | c.Assert(len(reftable), Equals, 4) 44 | c.Assert(reftable[0], Equals, "Foo") 45 | c.Assert(reftable[1], Equals, "Bar") 46 | } 47 | 48 | // Test we can correctly resolve a numeric reference in the reference table to a string value using xlsx.ResolveSharedString(). 49 | func (s *SharedStringsSuite) TestResolveSharedString(c *C) { 50 | sst := new(xlsxSST) 51 | err := xml.NewDecoder(s.SharedStringsXML).Decode(sst) 52 | c.Assert(err, IsNil) 53 | reftable := MakeSharedStringRefTable(sst) 54 | c.Assert(ResolveSharedString(reftable, 0), Equals, "Foo") 55 | } 56 | 57 | // Test we can correctly unmarshal an the sharedstrings.xml file into 58 | // an xlsx.xlsxSST struct and it's associated children. 59 | func (s *SharedStringsSuite) TestUnmarshallSharedStrings(c *C) { 60 | sst := new(xlsxSST) 61 | err := xml.NewDecoder(s.SharedStringsXML).Decode(sst) 62 | c.Assert(err, IsNil) 63 | c.Assert(sst.Count, Equals, "4") 64 | c.Assert(sst.UniqueCount, Equals, "4") 65 | c.Assert(sst.SI, HasLen, 4) 66 | si := sst.SI[0] 67 | c.Assert(si.T, Equals, "Foo") 68 | } 69 | -------------------------------------------------------------------------------- /sharedstrings.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | // xlsxSST directly maps the sst element from the namespace 4 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main currently 5 | // I have not checked this for completeness - it does as much as I need. 6 | type xlsxSST struct { 7 | Count string `xml:"count,attr"` 8 | UniqueCount string `xml:"uniqueCount,attr"` 9 | SI []xlsxSI `xml:"si"` 10 | } 11 | 12 | // xlsxSI directly maps the si element from the namespace 13 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 14 | // currently I have not checked this for completeness - it does as 15 | // much as I need. 16 | type xlsxSI struct { 17 | T string `xml:"t"` 18 | R []xlsxR `xml:"r"` 19 | } 20 | 21 | // xlsxR directly maps the r element from the namespace 22 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 23 | // currently I have not checked this for completeness - it does as 24 | // much as I need. 25 | type xlsxR struct { 26 | T string `xml:"t"` 27 | } 28 | 29 | // // xlsxT directly maps the t element from the namespace 30 | // // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 31 | // // currently I have not checked this for completeness - it does as 32 | // // much as I need. 33 | // type xlsxT struct { 34 | // Data string `xml:"chardata"` 35 | // } 36 | 37 | // MakeSharedStringRefTable() takes an xlsxSST struct and converts 38 | // it's contents to an slice of strings used to refer to string values 39 | // by numeric index - this is the model used within XLSX worksheet (a 40 | // numeric reference is stored to a shared cell value). 41 | func MakeSharedStringRefTable(source *xlsxSST) []string { 42 | reftable := make([]string, len(source.SI)) 43 | for i, si := range source.SI { 44 | if len(si.R) > 0 { 45 | for j := 0; j < len(si.R); j++ { 46 | reftable[i] = reftable[i] + si.R[j].T 47 | } 48 | } else { 49 | reftable[i] = si.T 50 | } 51 | } 52 | return reftable 53 | } 54 | 55 | // ResolveSharedString() looks up a string value by numeric index from 56 | // a provided reference table (just a slice of strings in the correct 57 | // order). This function only exists to provide clarity or purpose 58 | // via it's name. 59 | func ResolveSharedString(reftable []string, index int) string { 60 | return reftable[index] 61 | } 62 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * XSLX 2 | ** Introduction 3 | xlsx is a library to simplify reading the XML format used by recent 4 | version of Microsoft Excel in Go programs. 5 | 6 | Some, minimal, writing of XLSX files is now planned, but not yet 7 | completed. 8 | 9 | ** Usage 10 | Here is a minimal example usage that will dump all cell data in a 11 | given XLSX file. A more complete example of this kind of 12 | functionality is contained in [[https://github.com/tealeg/xlsx2csv][the XLSX2CSV program]]: 13 | 14 | #+BEGIN_SRC go 15 | 16 | import ( 17 | "fmt" 18 | "github.com/tealeg/xlsx" 19 | ) 20 | 21 | func main() { 22 | excelFileName := "/home/tealeg/foo.xlsx" 23 | xlFile, error := xlsx.OpenFile(excelFileName) 24 | if error != nil { 25 | ... 26 | } 27 | for _, sheet := range xlFile.Sheets { 28 | for _, row := range sheet.Rows { 29 | for _, cell := range row.Cells { 30 | fmt.Printf("%s\n", cell.String()) 31 | } 32 | } 33 | } 34 | } 35 | 36 | #+END_SRC 37 | 38 | Some additional information is available from the cell (for example, 39 | style information). For more details see the godoc output for this 40 | package. 41 | 42 | ** License 43 | This code is under a BSD style license: 44 | 45 | #+BEGIN_EXAMPLE 46 | 47 | Copyright 2011-2013 Geoffrey Teale. All rights reserved. 48 | 49 | Redistribution and use in source and binary forms, with or without 50 | modification, are permitted provided that the following conditions are 51 | met: 52 | 53 | Redistributions of source code must retain the above copyright notice, 54 | this list of conditions and the following disclaimer. Redistributions 55 | in binary form must reproduce the above copyright notice, this list of 56 | conditions and the following disclaimer in the documentation and/or 57 | other materials provided with the distribution. THIS SOFTWARE IS 58 | PROVIDED BY Geoffrey Teale ``AS IS'' AND ANY EXPRESS OR IMPLIED 59 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 60 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 61 | DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE 62 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 63 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 64 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 65 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 66 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 67 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 68 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 69 | 70 | #+END_EXAMPLE 71 | 72 | Eat a peach - Geoff 73 | -------------------------------------------------------------------------------- /worksheet.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | // xlsxWorksheet directly maps the worksheet element in the namespace 4 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 5 | // currently I have not checked it for completeness - it does as much 6 | // as I need. 7 | type xlsxWorksheet struct { 8 | SheetFormatPr xlsxSheetFormatPr `xml:"sheetFormatPr"` 9 | Dimension xlsxDimension `xml:"dimension"` 10 | SheetData xlsxSheetData `xml:"sheetData"` 11 | SheetProtection xlsxSheetProtection `xml:"sheetProtection"` 12 | } 13 | 14 | type xlsxSheetFormatPr struct { 15 | DefaultRowHeight float64 `xml:"defaultRowHeight,attr"` 16 | } 17 | 18 | // xlsxDimension directly maps the dimension element in the namespace 19 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 20 | // currently I have not checked it for completeness - it does as much 21 | // as I need. 22 | type xlsxDimension struct { 23 | Ref string `xml:"ref,attr"` 24 | } 25 | 26 | // xlsxSheetData directly maps the sheetData element in the namespace 27 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 28 | // currently I have not checked it for completeness - it does as much 29 | // as I need. 30 | type xlsxSheetData struct { 31 | Row []xlsxRow `xml:"row"` 32 | } 33 | 34 | // xlsxRow directly maps the row element in the namespace 35 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 36 | // currently I have not checked it for completeness - it does as much 37 | // as I need. 38 | type xlsxRow struct { 39 | R int `xml:"r,attr"` 40 | Spans string `xml:"spans,attr"` 41 | C []xlsxC `xml:"c"` 42 | Ht float64 `xml:"ht,attr"` 43 | CustomHeight int `xml:"customHeight,attr"` 44 | } 45 | 46 | // xlsxSheetProtection directly maps the sheetProtection element in the namespace 47 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 48 | // currently I have not checked it for completeness - it does as much 49 | // as I need. 50 | type xlsxSheetProtection struct { 51 | Sheet bool `xml:"sheet,attr"` 52 | } 53 | 54 | type xlsxSharedFormula struct { 55 | F string 56 | Ref string 57 | cellX int 58 | cellY int 59 | } 60 | 61 | type xlsxF struct { 62 | F string `xml:",innerxml"` 63 | Si string `xml:"si,attr"` 64 | Ref string `xml:"ref,attr"` 65 | T string `xml:"t,attr"` 66 | } 67 | 68 | // xlsxC directly maps the c element in the namespace 69 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 70 | // currently I have not checked it for completeness - it does as much 71 | // as I need. 72 | type xlsxC struct { 73 | F xlsxF `xml:"f"` 74 | R string `xml:"r,attr"` 75 | S int `xml:"s,attr"` 76 | T string `xml:"t,attr"` 77 | V string `xml:"v"` 78 | } 79 | 80 | // get cell 81 | func (sh *Sheet) Cell(row, col int) *Cell { 82 | 83 | cell, ok := sh.Cells[CellCoord{col, row}] 84 | if ok { 85 | return &cell 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /workbook_test.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | type WorkbookSuite struct{} 10 | 11 | var _ = Suite(&WorkbookSuite{}) 12 | 13 | // Test we can succesfully unmarshal the workbook.xml file from within 14 | // an XLSX file and return a xlsxWorkbook struct (and associated 15 | // children). 16 | func (w *WorkbookSuite) TestUnmarshallWorkbookXML(c *C) { 17 | var buf = bytes.NewBufferString( 18 | ` 21 | 23 | 27 | 28 | 29 | 33 | 34 | 35 | 38 | 41 | 44 | 45 | 46 | Sheet1!$A$1533 48 | 49 | 50 | `) 51 | var workbook *xlsxWorkbook 52 | workbook = new(xlsxWorkbook) 53 | err := xml.NewDecoder(buf).Decode(workbook) 54 | c.Assert(err, IsNil) 55 | c.Assert(workbook.FileVersion.AppName, Equals, "xl") 56 | c.Assert(workbook.FileVersion.LastEdited, Equals, "4") 57 | c.Assert(workbook.FileVersion.LowestEdited, Equals, "4") 58 | c.Assert(workbook.FileVersion.RupBuild, Equals, "4506") 59 | c.Assert(workbook.WorkbookPr.DefaultThemeVersion, Equals, "124226") 60 | c.Assert(workbook.BookViews.WorkBookView, HasLen, 1) 61 | workBookView := workbook.BookViews.WorkBookView[0] 62 | c.Assert(workBookView.XWindow, Equals, "120") 63 | c.Assert(workBookView.YWindow, Equals, "75") 64 | c.Assert(workBookView.WindowWidth, Equals, "15135") 65 | c.Assert(workBookView.WindowHeight, Equals, "7620") 66 | c.Assert(workbook.Sheets.Sheet, HasLen, 3) 67 | sheet := workbook.Sheets.Sheet[0] 68 | c.Assert(sheet.Id, Equals, "rId1") 69 | c.Assert(sheet.Name, Equals, "Sheet1") 70 | c.Assert(sheet.SheetId, Equals, "1") 71 | c.Assert(workbook.DefinedNames.DefinedName, HasLen, 1) 72 | dname := workbook.DefinedNames.DefinedName[0] 73 | c.Assert(dname.Data, Equals, "Sheet1!$A$1533") 74 | c.Assert(dname.LocalSheetID, Equals, "0") 75 | c.Assert(dname.Name, Equals, "monitors") 76 | c.Assert(workbook.CalcPr.CalcId, Equals, "125725") 77 | } 78 | -------------------------------------------------------------------------------- /style.go: -------------------------------------------------------------------------------- 1 | // xslx is a package designed to help with reading data from 2 | // spreadsheets stored in the XLSX format used in recent versions of 3 | // Microsoft's Excel spreadsheet. 4 | // 5 | // For a concise example of how to use this library why not check out 6 | // the source for xlsx2csv here: https://github.com/tealeg/xlsx2csv 7 | 8 | package xlsx 9 | 10 | // xlsxStyle directly maps the style element in the namespace 11 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 12 | // currently I have not checked it for completeness - it does as much 13 | // as I need. 14 | type xlsxStyles struct { 15 | Fonts []xlsxFont `xml:"fonts>font"` 16 | Fills []xlsxFill `xml:"fills>fill"` 17 | Borders []xlsxBorder `xml:"borders>border"` 18 | CellStyleXfs []xlsxXf `xml:"cellStyleXfs>xf"` 19 | CellXfs []xlsxXf `xml:"cellXfs>xf"` 20 | } 21 | 22 | // xlsxFont directly maps the font element in the namespace 23 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 24 | // currently I have not checked it for completeness - it does as much 25 | // as I need. 26 | type xlsxFont struct { 27 | Sz xlsxVal `xml:"sz"` 28 | Name xlsxVal `xml:"name"` 29 | Family xlsxVal `xml:"family"` 30 | Charset xlsxVal `xml:"charset"` 31 | } 32 | 33 | // xlsxVal directly maps the val element in the namespace 34 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 35 | // currently I have not checked it for completeness - it does as much 36 | // as I need. 37 | type xlsxVal struct { 38 | Val string `xml:"val,attr"` 39 | } 40 | 41 | // xlsxFill directly maps the fill element in the namespace 42 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 43 | // currently I have not checked it for completeness - it does as much 44 | // as I need. 45 | type xlsxFill struct { 46 | PatternFill xlsxPatternFill `xml:"patternFill"` 47 | } 48 | 49 | // xlsxPatternFill directly maps the patternFill element in the namespace 50 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 51 | // currently I have not checked it for completeness - it does as much 52 | // as I need. 53 | type xlsxPatternFill struct { 54 | PatternType string `xml:"patternType,attr"` 55 | FgColor xlsxColor `xml:"fgColor"` 56 | BgColor xlsxColor `xml:"bgColor"` 57 | } 58 | 59 | // xlsxColor is a common mapping used for both the fgColor and bgColor 60 | // elements in the namespace 61 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 62 | // currently I have not checked it for completeness - it does as much 63 | // as I need. 64 | type xlsxColor struct { 65 | RGB string `xml:"rgb,attr"` 66 | } 67 | 68 | // xlsxBorder directly maps the border element in the namespace 69 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 70 | // currently I have not checked it for completeness - it does as much 71 | // as I need. 72 | type xlsxBorder struct { 73 | Left xlsxLine `xml:"left"` 74 | Right xlsxLine `xml:"right"` 75 | Top xlsxLine `xml:"top"` 76 | Bottom xlsxLine `xml:"bottom"` 77 | } 78 | 79 | // xlsxLine directly maps the line style element in the namespace 80 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 81 | // currently I have not checked it for completeness - it does as much 82 | // as I need. 83 | type xlsxLine struct { 84 | Style string `xml:"style,attr"` 85 | } 86 | 87 | // xlsxXf directly maps the xf element in the namespace 88 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 89 | // currently I have not checked it for completeness - it does as much 90 | // as I need. 91 | type xlsxXf struct { 92 | ApplyAlignment bool `xml:"applyAlignment,attr"` 93 | ApplyBorder bool `xml:"applyBorder,attr"` 94 | ApplyFont bool `xml:"applyFont,attr"` 95 | ApplyFill bool `xml:"applyFill,attr"` 96 | ApplyProtection bool `xml:"applyProtection,attr"` 97 | BorderId int `xml:"borderId,attr"` 98 | FillId int `xml:"fillId,attr"` 99 | FontId int `xml:"fontId,attr"` 100 | NumFmtId int `xml:"numFmtId,attr"` 101 | alignment xlsxAlignment `xml:"alignement"` 102 | } 103 | 104 | type xlsxAlignment struct { 105 | Horizontal string `xml:"horizontal,attr"` 106 | Indent int `xml:"indent,attr"` 107 | ShrinkToFit bool `xml:"shrinkToFit,attr"` 108 | TextRotation int `xml:"textRotation,attr"` 109 | Vertical string `xml:"vertical,attr"` 110 | WrapText bool `xml:"wrapText,attr"` 111 | } 112 | -------------------------------------------------------------------------------- /worksheet_test.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | . "gopkg.in/check.v1" 7 | ) 8 | 9 | type WorksheetSuite struct{} 10 | 11 | var _ = Suite(&WorksheetSuite{}) 12 | 13 | // Test we can succesfully unmarshal the sheetN.xml files within and 14 | // XLSX file into an xlsxWorksheet struct (and it's related children). 15 | func (w *WorksheetSuite) TestUnmarshallWorksheet(c *C) { 16 | var sheetxml = bytes.NewBufferString( 17 | ` 18 | 20 | 21 | 22 | 23 | 24 | 25 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 56 | 57 | 58 | 76 | 94 | 95 | 100 | 106 | 121 | 123 | 124 | 125 | 126 | 127 | 128 | `) 129 | worksheet := new(xlsxWorksheet) 130 | err := xml.NewDecoder(sheetxml).Decode(worksheet) 131 | c.Assert(err, IsNil) 132 | c.Assert(worksheet.Dimension.Ref, Equals, "A1:B2") 133 | c.Assert(worksheet.SheetData.Row, HasLen, 2) 134 | row := worksheet.SheetData.Row[0] 135 | c.Assert(row.R, Equals, 1) 136 | c.Assert(row.C, HasLen, 2) 137 | cell := row.C[0] 138 | c.Assert(cell.R, Equals, "A1") 139 | c.Assert(cell.T, Equals, "s") 140 | c.Assert(cell.V, Equals, "0") 141 | } 142 | -------------------------------------------------------------------------------- /workbook.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | "archive/zip" 5 | "encoding/xml" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | // xmlxWorkbookRels contains xmlxWorkbookRelations 11 | // which maps sheet id and sheet XML 12 | type xlsxWorkbookRels struct { 13 | Relationships []xlsxWorkbookRelation `xml:"Relationship"` 14 | } 15 | 16 | // xmlxWorkbookRelation maps sheet id and xl/worksheets/sheet%d.xml 17 | type xlsxWorkbookRelation struct { 18 | Id string `xml:",attr"` 19 | Target string `xml:",attr"` 20 | } 21 | 22 | // xlsxWorkbook directly maps the workbook element from the namespace 23 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 24 | // currently I have not checked it for completeness - it does as much 25 | // as I need. 26 | type xlsxWorkbook struct { 27 | FileVersion xlsxFileVersion `xml:"fileVersion"` 28 | WorkbookPr xlsxWorkbookPr `xml:"workbookPr"` 29 | BookViews xlsxBookViews `xml:"bookViews"` 30 | Sheets xlsxSheets `xml:"sheets"` 31 | DefinedNames xlsxDefinedNames `xml:"definedNames"` 32 | CalcPr xlsxCalcPr `xml:"calcPr"` 33 | WorkbookProtection xlsxWorkbookProtection `xml:"workbookProtection"` 34 | } 35 | 36 | // xlsxFileVersion directly maps the fileVersion element from the 37 | // namespace http://schemas.openxmlformats.org/spreadsheetml/2006/main 38 | // - currently I have not checked it for completeness - it does as 39 | // much as I need. 40 | type xlsxFileVersion struct { 41 | AppName string `xml:"appName,attr"` 42 | LastEdited string `xml:"lastEdited,attr"` 43 | LowestEdited string `xml:"lowestEdited,attr"` 44 | RupBuild string `xml:"rupBuild,attr"` 45 | } 46 | 47 | // xlsxWorkbookPr directly maps the workbookPr element from the 48 | // namespace http://schemas.openxmlformats.org/spreadsheetml/2006/main 49 | // - currently I have not checked it for completeness - it does as 50 | // much as I need. 51 | type xlsxWorkbookPr struct { 52 | DefaultThemeVersion string `xml:"defaultThemeVersion,attr"` 53 | } 54 | 55 | // xlsxBookViews directly maps the bookViews element from the 56 | // namespace http://schemas.openxmlformats.org/spreadsheetml/2006/main 57 | // - currently I have not checked it for completeness - it does as 58 | // much as I need. 59 | type xlsxBookViews struct { 60 | WorkBookView []xlsxWorkBookView `xml:"workbookView"` 61 | } 62 | 63 | // xlsxWorkBookView directly maps the workbookView element from the 64 | // namespace http://schemas.openxmlformats.org/spreadsheetml/2006/main 65 | // - currently I have not checked it for completeness - it does as 66 | // much as I need. 67 | type xlsxWorkBookView struct { 68 | XWindow string `xml:"xWindow,attr"` 69 | YWindow string `xml:"yWindow,attr"` 70 | WindowWidth string `xml:"windowWidth,attr"` 71 | WindowHeight string `xml:"windowHeight,attr"` 72 | } 73 | 74 | // xlsxSheets directly maps the sheets element from the namespace 75 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 76 | // currently I have not checked it for completeness - it does as much 77 | // as I need. 78 | type xlsxSheets struct { 79 | Sheet []xlsxSheet `xml:"sheet"` 80 | } 81 | 82 | // xlsxSheet directly maps the sheet element from the namespace 83 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 84 | // currently I have not checked it for completeness - it does as much 85 | // as I need. 86 | type xlsxSheet struct { 87 | Name string `xml:"name,attr"` 88 | SheetId string `xml:"sheetId,attr"` 89 | Id string `xml:"id,attr"` 90 | } 91 | 92 | // xlsxDefinedNames directly maps the definedNames element from the 93 | // namespace http://schemas.openxmlformats.org/spreadsheetml/2006/main 94 | // - currently I have not checked it for completeness - it does as 95 | // much as I need. 96 | type xlsxDefinedNames struct { 97 | DefinedName []xlsxDefinedName `xml:"definedName"` 98 | } 99 | 100 | // xlsxDefinedName directly maps the definedName element from the 101 | // namespace http://schemas.openxmlformats.org/spreadsheetml/2006/main 102 | // - currently I have not checked it for completeness - it does as 103 | // much as I need. 104 | type xlsxDefinedName struct { 105 | Data string `xml:",chardata"` 106 | Name string `xml:"name,attr"` 107 | LocalSheetID string `xml:"localSheetId,attr"` 108 | } 109 | 110 | // xlsxCalcPr directly maps the calcPr element from the namespace 111 | // http://schemas.openxmlformats.org/spreadsheetml/2006/main - 112 | // currently I have not checked it for completeness - it does as much 113 | // as I need. 114 | type xlsxCalcPr struct { 115 | CalcId string `xml:"calcId,attr"` 116 | } 117 | 118 | // xlsxWorkbookProtection directly maps the workbookProtection element from the 119 | // namespace http://schemas.openxmlformats.org/spreadsheetml/2006/main 120 | // - currently I have not checked it for completeness - it does as 121 | // much as I need. 122 | type xlsxWorkbookProtection struct { 123 | LockRevision bool `xml:"lockRevision,attr"` 124 | LockStructure bool `xml:"lockStructure,attr"` 125 | LockWindows bool `xml:"lockWindows,attr"` 126 | } 127 | 128 | // getWorksheetFromSheet() is an internal helper function to open a 129 | // sheetN.xml file, refered to by an xlsx.xlsxSheet struct, from the XLSX 130 | // file and unmarshal it an xlsx.xlsxWorksheet struct 131 | func getWorksheetFromSheet(sheet xlsxSheet, worksheets map[string]*zip.File, sheetXMLMap map[string]string) (*xlsxWorksheet, error) { 132 | var rc io.ReadCloser 133 | var decoder *xml.Decoder 134 | var worksheet *xlsxWorksheet 135 | var error error 136 | var sheetName string 137 | worksheet = new(xlsxWorksheet) 138 | 139 | sheetName, ok := sheetXMLMap[sheet.Id] 140 | if !ok { 141 | if sheet.SheetId != "" { 142 | sheetName = fmt.Sprintf("sheet%s", sheet.SheetId) 143 | } else { 144 | sheetName = fmt.Sprintf("sheet%s", sheet.Id) 145 | } 146 | } 147 | f := worksheets[sheetName] 148 | rc, error = f.Open() 149 | if error != nil { 150 | return nil, error 151 | } 152 | decoder = xml.NewDecoder(rc) 153 | error = decoder.Decode(worksheet) 154 | if error != nil { 155 | return nil, error 156 | } 157 | return worksheet, nil 158 | } 159 | -------------------------------------------------------------------------------- /lib_test.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | // "strconv" 7 | . "gopkg.in/check.v1" 8 | "strings" 9 | ) 10 | 11 | type LibSuite struct{} 12 | 13 | var _ = Suite(&LibSuite{}) 14 | 15 | // Test we can correctly open a XSLX file and return a xlsx.File 16 | // struct. 17 | func (l *LibSuite) TestOpenFile(c *C) { 18 | var xlsxFile *File 19 | var error error 20 | 21 | xlsxFile, error = OpenFile("testfile.xlsx") 22 | c.Assert(error, IsNil) 23 | c.Assert(xlsxFile, NotNil) 24 | 25 | } 26 | 27 | // Test we can create a File object from scratch 28 | func (l *LibSuite) TestCreateFile(c *C) { 29 | var xlsxFile *File 30 | 31 | xlsxFile = NewFile() 32 | c.Assert(xlsxFile, NotNil) 33 | } 34 | 35 | // Test that when we open a real XLSX file we create xlsx.Sheet 36 | // objects for the sheets inside the file and that these sheets are 37 | // themselves correct. 38 | func (l *LibSuite) TestCreateSheet(c *C) { 39 | var xlsxFile *File 40 | var err error 41 | var sheet *Sheet 42 | var row *Row 43 | xlsxFile, err = OpenFile("testfile.xlsx") 44 | c.Assert(err, IsNil) 45 | c.Assert(xlsxFile, NotNil) 46 | sheetLen := len(xlsxFile.Sheets) 47 | c.Assert(sheetLen, Equals, 3) 48 | sheet = xlsxFile.Sheets[0] 49 | rowLen := len(sheet.Rows) 50 | c.Assert(rowLen, Equals, 2) 51 | row = sheet.Rows[0] 52 | c.Assert(len(row.Cells), Equals, 2) 53 | cell := row.Cells[0] 54 | cellstring := cell.String() 55 | c.Assert(cellstring, Equals, "Foo") 56 | } 57 | 58 | // Test that GetStyle correctly converts the xlsxStyle.Fonts. 59 | func (l *LibSuite) TestGetStyleWithFonts(c *C) { 60 | var cell *Cell 61 | var style *Style 62 | var xStyles *xlsxStyles 63 | var fonts []xlsxFont 64 | var cellXfs []xlsxXf 65 | 66 | fonts = make([]xlsxFont, 1) 67 | fonts[0] = xlsxFont{ 68 | Sz: xlsxVal{Val: "10"}, 69 | Name: xlsxVal{Val: "Calibra"}} 70 | 71 | cellXfs = make([]xlsxXf, 1) 72 | cellXfs[0] = xlsxXf{ApplyFont: true, FontId: 0} 73 | 74 | xStyles = &xlsxStyles{Fonts: fonts, CellXfs: cellXfs} 75 | 76 | cell = &Cell{Value: "123", styleIndex: 1, styles: xStyles} 77 | style = cell.GetStyle() 78 | c.Assert(style, NotNil) 79 | c.Assert(style.Font.Size, Equals, 10) 80 | c.Assert(style.Font.Name, Equals, "Calibra") 81 | } 82 | 83 | // Test that GetStyle correctly converts the xlsxStyle.Fills. 84 | func (l *LibSuite) TestGetStyleWithFills(c *C) { 85 | var cell *Cell 86 | var style *Style 87 | var xStyles *xlsxStyles 88 | var fills []xlsxFill 89 | var cellXfs []xlsxXf 90 | 91 | fills = make([]xlsxFill, 1) 92 | fills[0] = xlsxFill{ 93 | PatternFill: xlsxPatternFill{ 94 | PatternType: "solid", 95 | FgColor: xlsxColor{RGB: "FF000000"}, 96 | BgColor: xlsxColor{RGB: "00FF0000"}}} 97 | cellXfs = make([]xlsxXf, 1) 98 | cellXfs[0] = xlsxXf{ApplyFill: true, FillId: 0} 99 | 100 | xStyles = &xlsxStyles{Fills: fills, CellXfs: cellXfs} 101 | 102 | cell = &Cell{Value: "123", styleIndex: 1, styles: xStyles} 103 | style = cell.GetStyle() 104 | fill := style.Fill 105 | c.Assert(fill.PatternType, Equals, "solid") 106 | c.Assert(fill.BgColor, Equals, "00FF0000") 107 | c.Assert(fill.FgColor, Equals, "FF000000") 108 | } 109 | 110 | // Test that GetStyle correctly converts the xlsxStyle.Borders. 111 | func (l *LibSuite) TestGetStyleWithBorders(c *C) { 112 | var cell *Cell 113 | var style *Style 114 | var xStyles *xlsxStyles 115 | var borders []xlsxBorder 116 | var cellXfs []xlsxXf 117 | 118 | borders = make([]xlsxBorder, 1) 119 | borders[0] = xlsxBorder{ 120 | Left: xlsxLine{Style: "thin"}, 121 | Right: xlsxLine{Style: "thin"}, 122 | Top: xlsxLine{Style: "thin"}, 123 | Bottom: xlsxLine{Style: "thin"}} 124 | 125 | cellXfs = make([]xlsxXf, 1) 126 | cellXfs[0] = xlsxXf{ApplyBorder: true, BorderId: 0} 127 | 128 | xStyles = &xlsxStyles{Borders: borders, CellXfs: cellXfs} 129 | 130 | cell = &Cell{Value: "123", styleIndex: 1, styles: xStyles} 131 | style = cell.GetStyle() 132 | border := style.Border 133 | c.Assert(border.Left, Equals, "thin") 134 | c.Assert(border.Right, Equals, "thin") 135 | c.Assert(border.Top, Equals, "thin") 136 | c.Assert(border.Bottom, Equals, "thin") 137 | } 138 | 139 | // Test that we can correctly extract a reference table from the 140 | // sharedStrings.xml file embedded in the XLSX file and return a 141 | // reference table of string values from it. 142 | func (l *LibSuite) TestReadSharedStringsFromZipFile(c *C) { 143 | var xlsxFile *File 144 | var err error 145 | xlsxFile, err = OpenFile("testfile.xlsx") 146 | c.Assert(err, IsNil) 147 | c.Assert(xlsxFile.referenceTable, NotNil) 148 | } 149 | 150 | // Helper function used to test contents of a given xlsxXf against 151 | // expectations. 152 | func testXf(c *C, result, expected *xlsxXf) { 153 | c.Assert(result.ApplyAlignment, Equals, expected.ApplyAlignment) 154 | c.Assert(result.ApplyBorder, Equals, expected.ApplyBorder) 155 | c.Assert(result.ApplyFont, Equals, expected.ApplyFont) 156 | c.Assert(result.ApplyFill, Equals, expected.ApplyFill) 157 | c.Assert(result.ApplyProtection, Equals, expected.ApplyProtection) 158 | c.Assert(result.BorderId, Equals, expected.BorderId) 159 | c.Assert(result.FillId, Equals, expected.FillId) 160 | c.Assert(result.FontId, Equals, expected.FontId) 161 | c.Assert(result.NumFmtId, Equals, expected.NumFmtId) 162 | } 163 | 164 | // We can correctly extract a style table from the style.xml file 165 | // embedded in the XLSX file and return a styles struct from it. 166 | func (l *LibSuite) TestReadStylesFromZipFile(c *C) { 167 | var xlsxFile *File 168 | var err error 169 | var fontCount, fillCount, borderCount, cellStyleXfCount, cellXfCount int 170 | var font xlsxFont 171 | var fill xlsxFill 172 | var border xlsxBorder 173 | var xf xlsxXf 174 | 175 | xlsxFile, err = OpenFile("testfile.xlsx") 176 | c.Assert(err, IsNil) 177 | c.Assert(xlsxFile.styles, NotNil) 178 | 179 | fontCount = len(xlsxFile.styles.Fonts) 180 | c.Assert(fontCount, Equals, 4) 181 | 182 | font = xlsxFile.styles.Fonts[0] 183 | c.Assert(font.Sz.Val, Equals, "11") 184 | c.Assert(font.Name.Val, Equals, "Calibri") 185 | 186 | fillCount = len(xlsxFile.styles.Fills) 187 | c.Assert(fillCount, Equals, 3) 188 | 189 | fill = xlsxFile.styles.Fills[2] 190 | c.Assert(fill.PatternFill.PatternType, Equals, "solid") 191 | 192 | borderCount = len(xlsxFile.styles.Borders) 193 | c.Assert(borderCount, Equals, 2) 194 | 195 | border = xlsxFile.styles.Borders[1] 196 | c.Assert(border.Left.Style, Equals, "thin") 197 | c.Assert(border.Right.Style, Equals, "thin") 198 | c.Assert(border.Top.Style, Equals, "thin") 199 | c.Assert(border.Bottom.Style, Equals, "thin") 200 | 201 | cellStyleXfCount = len(xlsxFile.styles.CellStyleXfs) 202 | c.Assert(cellStyleXfCount, Equals, 20) 203 | 204 | xf = xlsxFile.styles.CellStyleXfs[0] 205 | expectedXf := &xlsxXf{ 206 | ApplyAlignment: true, 207 | ApplyBorder: true, 208 | ApplyFont: true, 209 | ApplyFill: false, 210 | ApplyProtection: true, 211 | BorderId: 0, 212 | FillId: 0, 213 | FontId: 0, 214 | NumFmtId: 164} 215 | testXf(c, &xf, expectedXf) 216 | 217 | cellXfCount = len(xlsxFile.styles.CellXfs) 218 | c.Assert(cellXfCount, Equals, 3) 219 | 220 | xf = xlsxFile.styles.CellXfs[0] 221 | expectedXf = &xlsxXf{ 222 | ApplyAlignment: false, 223 | ApplyBorder: false, 224 | ApplyFont: false, 225 | ApplyFill: false, 226 | ApplyProtection: false, 227 | BorderId: 0, 228 | FillId: 0, 229 | FontId: 0, 230 | NumFmtId: 164} 231 | testXf(c, &xf, expectedXf) 232 | } 233 | 234 | // We can correctly extract a map of relationship Ids to the worksheet files in 235 | // which they are contained from the XLSX file. 236 | func (l *LibSuite) TestReadWorkbookRelationsFromZipFile(c *C) { 237 | var xlsxFile *File 238 | var err error 239 | 240 | xlsxFile, err = OpenFile("testfile.xlsx") 241 | c.Assert(err, IsNil) 242 | sheetCount := len(xlsxFile.Sheet) 243 | c.Assert(sheetCount, Equals, 3) 244 | } 245 | 246 | // which they are contained from the XLSX file, even when the 247 | // worksheet files have arbitrary, non-numeric names. 248 | func (l *LibSuite) TestReadWorkbookRelationsFromZipFileWithFunnyNames(c *C) { 249 | var xlsxFile *File 250 | var err error 251 | 252 | xlsxFile, err = OpenFile("testrels.xlsx") 253 | c.Assert(err, IsNil) 254 | sheetCount := len(xlsxFile.Sheet) 255 | c.Assert(sheetCount, Equals, 2) 256 | bob := xlsxFile.Sheet["Bob"] 257 | row1 := bob.Rows[0] 258 | cell1 := row1.Cells[0] 259 | c.Assert(cell1.String(), Equals, "I am Bob") 260 | } 261 | 262 | func (l *LibSuite) TestLettersToNumeric(c *C) { 263 | cases := map[string]int{"A": 0, "G": 6, "z": 25, "AA": 26, "Az": 51, 264 | "BA": 52, "Bz": 77, "ZA": 26*26 + 0, "ZZ": 26*26 + 25, 265 | "AAA": 26*26 + 26 + 0, "AMI": 1022} 266 | for input, ans := range cases { 267 | output := lettersToNumeric(input) 268 | c.Assert(output, Equals, ans) 269 | } 270 | } 271 | 272 | func (l *LibSuite) TestLetterOnlyMapFunction(c *C) { 273 | var input string = "ABC123" 274 | var output string = strings.Map(letterOnlyMapF, input) 275 | c.Assert(output, Equals, "ABC") 276 | input = "abc123" 277 | output = strings.Map(letterOnlyMapF, input) 278 | c.Assert(output, Equals, "ABC") 279 | } 280 | 281 | func (l *LibSuite) TestIntOnlyMapFunction(c *C) { 282 | var input string = "ABC123" 283 | var output string = strings.Map(intOnlyMapF, input) 284 | c.Assert(output, Equals, "123") 285 | } 286 | 287 | func (l *LibSuite) TestGetCoordsFromCellIDString(c *C) { 288 | var cellIDString string = "A3" 289 | var x, y int 290 | var err error 291 | x, y, err = getCoordsFromCellIDString(cellIDString) 292 | c.Assert(err, IsNil) 293 | c.Assert(x, Equals, 0) 294 | c.Assert(y, Equals, 2) 295 | } 296 | 297 | func (l *LibSuite) TestGetMaxMinFromDimensionRef(c *C) { 298 | var dimensionRef string = "A1:B2" 299 | var minx, miny, maxx, maxy int 300 | var err error 301 | minx, miny, maxx, maxy, err = getMaxMinFromDimensionRef(dimensionRef) 302 | c.Assert(err, IsNil) 303 | c.Assert(minx, Equals, 0) 304 | c.Assert(miny, Equals, 0) 305 | c.Assert(maxx, Equals, 1) 306 | c.Assert(maxy, Equals, 1) 307 | } 308 | 309 | func (l *LibSuite) TestCalculateMaxMinFromWorksheet(c *C) { 310 | var sheetxml = bytes.NewBufferString(` 311 | 312 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 0 328 | 329 | 330 | 1 331 | 332 | 333 | 334 | 335 | 2 336 | 337 | 338 | 3 339 | 340 | 341 | 342 | 343 | `) 344 | worksheet := new(xlsxWorksheet) 345 | err := xml.NewDecoder(sheetxml).Decode(worksheet) 346 | c.Assert(err, IsNil) 347 | minx, miny, maxx, maxy, err := calculateMaxMinFromWorksheet(worksheet) 348 | c.Assert(err, IsNil) 349 | c.Assert(minx, Equals, 0) 350 | c.Assert(miny, Equals, 0) 351 | c.Assert(maxx, Equals, 1) 352 | c.Assert(maxy, Equals, 1) 353 | } 354 | 355 | func (l *LibSuite) TestGetRangeFromString(c *C) { 356 | var rangeString string 357 | var lower, upper int 358 | var err error 359 | rangeString = "1:3" 360 | lower, upper, err = getRangeFromString(rangeString) 361 | c.Assert(err, IsNil) 362 | c.Assert(lower, Equals, 1) 363 | c.Assert(upper, Equals, 3) 364 | } 365 | 366 | func (l *LibSuite) TestMakeRowFromSpan(c *C) { 367 | var rangeString string 368 | var row *Row 369 | var length int 370 | rangeString = "1:3" 371 | row = makeRowFromSpan(rangeString) 372 | length = len(row.Cells) 373 | c.Assert(length, Equals, 3) 374 | rangeString = "5:7" // Note - we ignore lower bound! 375 | row = makeRowFromSpan(rangeString) 376 | length = len(row.Cells) 377 | c.Assert(length, Equals, 7) 378 | rangeString = "1:1" 379 | row = makeRowFromSpan(rangeString) 380 | length = len(row.Cells) 381 | c.Assert(length, Equals, 1) 382 | } 383 | 384 | func (l *LibSuite) TestReadRowsFromSheet(c *C) { 385 | var sharedstringsXML = bytes.NewBufferString(` 386 | 387 | 388 | 389 | Foo 390 | 391 | 392 | Bar 393 | 394 | 395 | Baz 396 | 397 | 398 | Quuk 399 | 400 | `) 401 | var sheetxml = bytes.NewBufferString(` 402 | 403 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 0 416 | 417 | 418 | 1 419 | 420 | 421 | 422 | 423 | 2 424 | 425 | 426 | 3 427 | 428 | 429 | 430 | 435 | `) 436 | worksheet := new(xlsxWorksheet) 437 | err := xml.NewDecoder(sheetxml).Decode(worksheet) 438 | c.Assert(err, IsNil) 439 | sst := new(xlsxSST) 440 | err = xml.NewDecoder(sharedstringsXML).Decode(sst) 441 | c.Assert(err, IsNil) 442 | file := new(File) 443 | file.referenceTable = MakeSharedStringRefTable(sst) 444 | rows, maxCols, maxRows := readRowsFromSheet(worksheet, file) 445 | c.Assert(maxRows, Equals, 2) 446 | c.Assert(maxCols, Equals, 2) 447 | row := rows[0] 448 | c.Assert(len(row.Cells), Equals, 2) 449 | cell1 := row.Cells[0] 450 | c.Assert(cell1.String(), Equals, "Foo") 451 | cell2 := row.Cells[1] 452 | c.Assert(cell2.String(), Equals, "Bar") 453 | } 454 | 455 | func (l *LibSuite) TestReadRowsFromSheetWithLeadingEmptyRows(c *C) { 456 | var sharedstringsXML = bytes.NewBufferString(` 457 | ABCDEF`) 458 | var sheetxml = bytes.NewBufferString(` 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 0 471 | 472 | 473 | 474 | 475 | 1 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | `) 488 | worksheet := new(xlsxWorksheet) 489 | err := xml.NewDecoder(sheetxml).Decode(worksheet) 490 | c.Assert(err, IsNil) 491 | sst := new(xlsxSST) 492 | err = xml.NewDecoder(sharedstringsXML).Decode(sst) 493 | c.Assert(err, IsNil) 494 | 495 | file := new(File) 496 | file.referenceTable = MakeSharedStringRefTable(sst) 497 | _, maxCols, maxRows := readRowsFromSheet(worksheet, file) 498 | c.Assert(maxRows, Equals, 2) 499 | c.Assert(maxCols, Equals, 1) 500 | } 501 | 502 | func (l *LibSuite) TestReadRowsFromSheetWithEmptyCells(c *C) { 503 | var sharedstringsXML = bytes.NewBufferString(` 504 | 505 | 506 | 507 | Bob 508 | 509 | 510 | Alice 511 | 512 | 513 | Sue 514 | 515 | 516 | Yes 517 | 518 | 519 | No 520 | 521 | 522 | `) 523 | var sheetxml = bytes.NewBufferString(` 524 | 525 | 526 | 527 | 528 | 529 | 530 | 0 531 | 532 | 533 | 534 | 535 | 1 536 | 537 | 538 | 539 | 540 | 2 541 | 542 | 543 | 544 | 545 | 546 | 547 | 3 548 | 549 | 550 | 551 | 552 | 4 553 | 554 | 555 | 556 | 557 | 3 558 | 559 | 560 | 561 | 562 | 563 | 564 | 4 565 | 566 | 567 | 568 | 569 | 3 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | `) 578 | worksheet := new(xlsxWorksheet) 579 | err := xml.NewDecoder(sheetxml).Decode(worksheet) 580 | c.Assert(err, IsNil) 581 | sst := new(xlsxSST) 582 | err = xml.NewDecoder(sharedstringsXML).Decode(sst) 583 | c.Assert(err, IsNil) 584 | file := new(File) 585 | file.referenceTable = MakeSharedStringRefTable(sst) 586 | rows, maxCols, maxRows := readRowsFromSheet(worksheet, file) 587 | c.Assert(maxRows, Equals, 3) 588 | c.Assert(maxCols, Equals, 3) 589 | 590 | row := rows[2] 591 | c.Assert(len(row.Cells), Equals, 3) 592 | 593 | cell1 := row.Cells[0] 594 | c.Assert(cell1.String(), Equals, "No") 595 | 596 | cell2 := row.Cells[1] 597 | c.Assert(cell2.String(), Equals, "") 598 | 599 | cell3 := row.Cells[2] 600 | c.Assert(cell3.String(), Equals, "Yes") 601 | } 602 | 603 | func (l *LibSuite) TestReadRowsFromSheetWithTrailingEmptyCells(c *C) { 604 | var row *Row 605 | var cell1, cell2, cell3, cell4 *Cell 606 | var sharedstringsXML = bytes.NewBufferString(` 607 | 608 | ABCD`) 609 | var sheetxml = bytes.NewBufferString(` 610 | 611 | 01231111111 612 | `) 613 | worksheet := new(xlsxWorksheet) 614 | err := xml.NewDecoder(sheetxml).Decode(worksheet) 615 | c.Assert(err, IsNil) 616 | 617 | sst := new(xlsxSST) 618 | err = xml.NewDecoder(sharedstringsXML).Decode(sst) 619 | c.Assert(err, IsNil) 620 | 621 | file := new(File) 622 | file.referenceTable = MakeSharedStringRefTable(sst) 623 | rows, maxCol, maxRow := readRowsFromSheet(worksheet, file) 624 | c.Assert(maxCol, Equals, 4) 625 | c.Assert(maxRow, Equals, 8) 626 | 627 | row = rows[0] 628 | c.Assert(len(row.Cells), Equals, 4) 629 | 630 | cell1 = row.Cells[0] 631 | c.Assert(cell1.String(), Equals, "A") 632 | 633 | cell2 = row.Cells[1] 634 | c.Assert(cell2.String(), Equals, "B") 635 | 636 | cell3 = row.Cells[2] 637 | c.Assert(cell3.String(), Equals, "C") 638 | 639 | cell4 = row.Cells[3] 640 | c.Assert(cell4.String(), Equals, "D") 641 | 642 | row = rows[1] 643 | c.Assert(len(row.Cells), Equals, 4) 644 | 645 | cell1 = row.Cells[0] 646 | c.Assert(cell1.String(), Equals, "1") 647 | 648 | cell2 = row.Cells[1] 649 | c.Assert(cell2.String(), Equals, "") 650 | 651 | cell3 = row.Cells[2] 652 | c.Assert(cell3.String(), Equals, "") 653 | 654 | cell4 = row.Cells[3] 655 | c.Assert(cell4.String(), Equals, "") 656 | } 657 | -------------------------------------------------------------------------------- /lib.go: -------------------------------------------------------------------------------- 1 | package xlsx 2 | 3 | import ( 4 | "archive/zip" 5 | "encoding/xml" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | type CellFilter func(cell Cell) bool 14 | 15 | // XLSXReaderError is the standard error type for otherwise undefined 16 | // errors in the XSLX reading process. 17 | type XLSXReaderError struct { 18 | Err string 19 | } 20 | 21 | // String() returns a string value from an XLSXReaderError struct in 22 | // order that it might comply with the os.Error interface. 23 | func (e *XLSXReaderError) Error() string { 24 | return e.Err 25 | } 26 | 27 | // Cell is a high level structure intended to provide user access to 28 | // the contents of Cell within an xlsx.Row. 29 | type Cell struct { 30 | Value string 31 | formula string 32 | styleIndex int 33 | styles *xlsxStyles 34 | } 35 | 36 | // CellInterface defines the public API of the Cell. 37 | type CellInterface interface { 38 | String() string 39 | } 40 | 41 | // String returns the value of a Cell as a string. 42 | func (c *Cell) String() string { 43 | return c.Value 44 | } 45 | 46 | // String returns the formula of a Cell as a string. 47 | func (c *Cell) Formula() string { 48 | return c.formula 49 | } 50 | 51 | // GetStyle returns the Style associated with a Cell 52 | func (c *Cell) GetStyle() *Style { 53 | style := &Style{} 54 | 55 | if c.styleIndex > 0 && c.styleIndex <= len(c.styles.CellXfs) { 56 | xf := c.styles.CellXfs[c.styleIndex-1] 57 | if xf.ApplyBorder { 58 | var border Border 59 | border.Left = c.styles.Borders[xf.BorderId].Left.Style 60 | border.Right = c.styles.Borders[xf.BorderId].Right.Style 61 | border.Top = c.styles.Borders[xf.BorderId].Top.Style 62 | border.Bottom = c.styles.Borders[xf.BorderId].Bottom.Style 63 | style.Border = border 64 | } 65 | if xf.ApplyFill { 66 | var fill Fill 67 | fill.PatternType = c.styles.Fills[xf.FillId].PatternFill.PatternType 68 | fill.BgColor = c.styles.Fills[xf.FillId].PatternFill.BgColor.RGB 69 | fill.FgColor = c.styles.Fills[xf.FillId].PatternFill.FgColor.RGB 70 | style.Fill = fill 71 | } 72 | if xf.ApplyFont { 73 | font := c.styles.Fonts[xf.FontId] 74 | style.Font = Font{} 75 | style.Font.Size, _ = strconv.Atoi(font.Sz.Val) 76 | style.Font.Name = font.Name.Val 77 | style.Font.Family, _ = strconv.Atoi(font.Family.Val) 78 | style.Font.Charset, _ = strconv.Atoi(font.Charset.Val) 79 | } 80 | } 81 | return style 82 | } 83 | 84 | // Row is a high level structure indended to provide user access to a 85 | // row within a xlsx.Sheet. An xlsx.Row contains a slice of xlsx.Cell. 86 | type Row struct { 87 | Height float64 88 | } 89 | 90 | // zero-based cell index 91 | type CellCoord struct { 92 | X int 93 | Y int 94 | } 95 | 96 | // Sheet is a high level structure intended to provide user access to 97 | // the contents of a particular sheet within an XLSX file. 98 | type Sheet struct { 99 | Name string 100 | Cells map[CellCoord]Cell 101 | Rows map[int]Row 102 | MaxRow int 103 | MaxCol int 104 | DefaultRowHeight float64 105 | Protected bool 106 | WorkbookLockRevision bool 107 | WorkbookLockStructure bool 108 | WorkbookLockWindows bool 109 | } 110 | 111 | // Style is a high level structure intended to provide user access to 112 | // the contents of Style within an XLSX file. 113 | type Style struct { 114 | Border Border 115 | Fill Fill 116 | Font Font 117 | } 118 | 119 | // Border is a high level structure intended to provide user access to 120 | // the contents of Border Style within an Sheet. 121 | type Border struct { 122 | Left string 123 | Right string 124 | Top string 125 | Bottom string 126 | } 127 | 128 | // Fill is a high level structure intended to provide user access to 129 | // the contents of background and foreground color index within an Sheet. 130 | type Fill struct { 131 | PatternType string 132 | BgColor string 133 | FgColor string 134 | } 135 | 136 | type Font struct { 137 | Size int 138 | Name string 139 | Family int 140 | Charset int 141 | } 142 | 143 | // File is a high level structure providing a slice of Sheet structs 144 | // to the user. 145 | type File struct { 146 | worksheets map[string]*zip.File 147 | referenceTable []string 148 | styles *xlsxStyles 149 | Sheets []*Sheet // sheet access by index 150 | Sheet map[string]*Sheet // sheet access by name 151 | } 152 | 153 | // getRangeFromString is an internal helper function that converts 154 | // XLSX internal range syntax to a pair of integers. For example, 155 | // the range string "1:3" yield the upper and lower intergers 1 and 3. 156 | func getRangeFromString(rangeString string) (lower int, upper int, error error) { 157 | var parts []string 158 | parts = strings.SplitN(rangeString, ":", 2) 159 | if parts[0] == "" { 160 | error = errors.New(fmt.Sprintf("Invalid range '%s'\n", rangeString)) 161 | } 162 | if parts[1] == "" { 163 | error = errors.New(fmt.Sprintf("Invalid range '%s'\n", rangeString)) 164 | } 165 | lower, error = strconv.Atoi(parts[0]) 166 | if error != nil { 167 | error = errors.New(fmt.Sprintf("Invalid range (not integer in lower bound) %s\n", rangeString)) 168 | } 169 | upper, error = strconv.Atoi(parts[1]) 170 | if error != nil { 171 | error = errors.New(fmt.Sprintf("Invalid range (not integer in upper bound) %s\n", rangeString)) 172 | } 173 | return lower, upper, error 174 | } 175 | 176 | // lettersToNumeric is used to convert a character based column 177 | // reference to a zero based numeric column identifier. 178 | func lettersToNumeric(letters string) int { 179 | sum, mul, n := 0, 1, 0 180 | for i := len(letters) - 1; i >= 0; i, mul, n = i-1, mul*26, 1 { 181 | c := letters[i] 182 | switch { 183 | case 'A' <= c && c <= 'Z': 184 | n += int(c - 'A') 185 | case 'a' <= c && c <= 'z': 186 | n += int(c - 'a') 187 | } 188 | sum += n * mul 189 | } 190 | return sum 191 | } 192 | 193 | // letterOnlyMapF is used in conjunction with strings.Map to return 194 | // only the characters A-Z and a-z in a string 195 | func letterOnlyMapF(rune rune) rune { 196 | switch { 197 | case 'A' <= rune && rune <= 'Z': 198 | return rune 199 | case 'a' <= rune && rune <= 'z': 200 | return rune - 32 201 | } 202 | return -1 203 | } 204 | 205 | // intOnlyMapF is used in conjunction with strings.Map to return only 206 | // the numeric portions of a string. 207 | func intOnlyMapF(rune rune) rune { 208 | if rune >= 48 && rune < 58 { 209 | return rune 210 | } 211 | return -1 212 | } 213 | 214 | // getCoordsFromCellIDString returns the zero based cartesian 215 | // coordinates from a cell name in Excel format, e.g. the cellIDString 216 | // "A1" returns 0, 0 and the "B3" return 1, 2. 217 | func getCoordsFromCellIDString(cellIDString string) (x, y int, error error) { 218 | var letterPart string = strings.Map(letterOnlyMapF, cellIDString) 219 | y, error = strconv.Atoi(strings.Map(intOnlyMapF, cellIDString)) 220 | if error != nil { 221 | return x, y, error 222 | } 223 | y -= 1 // Zero based 224 | x = lettersToNumeric(letterPart) 225 | return x, y, error 226 | } 227 | 228 | // getCoordsFromCellIDString returns the zero based cartesian 229 | // coordinates from a cell name in Excel format, e.g. the cellIDString 230 | // "A1" returns 0, 0 and the "B3" return 1, 2. 231 | func getCoordsFromCellIDRunes(cellIDString []rune) (x, y int, error error) { 232 | for i, v := range cellIDString { 233 | if !(v >= 'A' && v <= 'Z') { 234 | if i == 0 { 235 | return 0, 0, errors.New("no alphanum in rune array") 236 | } 237 | x := lettersToNumeric(string(cellIDString[:i])) 238 | y, error := strconv.Atoi(string(cellIDString[i:])) 239 | if error != nil { 240 | return x, y, error 241 | } 242 | y -= 1 // Zero based 243 | return x, y, nil 244 | } 245 | } 246 | return 0, 0, errors.New("no number in rune array") 247 | } 248 | 249 | func reverseRunesInPlace(runes []rune) { 250 | for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { 251 | near := runes[j] 252 | far := runes[i] 253 | runes[i] = near 254 | runes[j] = far 255 | } 256 | } 257 | 258 | func coordsToCellIDRunes(x, y int) []rune { 259 | var itoa []rune 260 | y += 1 261 | for { 262 | itoa = append(itoa, rune('0'+y%10)) 263 | y /= 10 264 | if y == 0 { 265 | break 266 | } 267 | } 268 | reverseRunesInPlace(itoa) 269 | var retval []rune 270 | x += 1 271 | for x > 0 { 272 | rem := (x - 1) % 26 273 | retval = append(retval, rune('A'+rem)) 274 | x -= rem 275 | x /= 26 276 | } 277 | reverseRunesInPlace(retval) 278 | return append(retval, itoa...) 279 | } 280 | 281 | // getMaxMinFromDimensionRef return the zero based cartesian maximum 282 | // and minimum coordinates from the dimension reference embedded in a 283 | // XLSX worksheet. For example, the dimension reference "A1:B2" 284 | // returns "0,0", "1,1". 285 | func getMaxMinFromDimensionRef(ref string) (minx, miny, maxx, maxy int, err error) { 286 | var parts []string 287 | parts = strings.Split(ref, ":") 288 | minx, miny, err = getCoordsFromCellIDString(parts[0]) 289 | if err != nil { 290 | return -1, -1, -1, -1, err 291 | } 292 | if len(parts) == 1 { 293 | maxx, maxy = minx, miny 294 | return 295 | } 296 | maxx, maxy, err = getCoordsFromCellIDString(parts[1]) 297 | if err != nil { 298 | return -1, -1, -1, -1, err 299 | } 300 | return 301 | } 302 | 303 | // calculateMaxMinFromWorkSheet works out the dimensions of a spreadsheet 304 | // that doesn't have a DimensionRef set. The only case currently 305 | // known where this is true is with XLSX exported from Google Docs. 306 | func calculateMaxMinFromWorksheet(worksheet *xlsxWorksheet) (minx, miny, maxx, maxy int, err error) { 307 | // Note, this method could be very slow for large spreadsheets. 308 | var x, y int 309 | minx = 0 310 | miny = 0 311 | maxy = 0 312 | maxx = 0 313 | for _, row := range worksheet.SheetData.Row { 314 | for _, cell := range row.C { 315 | x, y, err = getCoordsFromCellIDString(cell.R) 316 | if err != nil { 317 | return -1, -1, -1, -1, err 318 | } 319 | if x < minx { 320 | minx = x 321 | } 322 | if x > maxx { 323 | maxx = x 324 | } 325 | if y < miny { 326 | miny = y 327 | } 328 | if y > maxy { 329 | maxy = y 330 | } 331 | } 332 | } 333 | return 334 | } 335 | 336 | // getValueFromCellData attempts to extract a valid value, usable in CSV form from the raw cell value. 337 | // Note - this is not actually general enough - we should support retaining tabs and newlines. 338 | func getValueFromCellData(rawcell xlsxC, reftable []string) string { 339 | var value string = "" 340 | var vval string = rawcell.V 341 | if len(vval) > 0 { 342 | if rawcell.T == "s" { 343 | ref, error := strconv.Atoi(vval) 344 | if error != nil { 345 | panic(error) 346 | } 347 | value = reftable[ref] 348 | } else { 349 | value = vval 350 | } 351 | } 352 | return value 353 | } 354 | func isDelimiter(v rune) bool { 355 | if v >= 'A' && v <= 'Z' { 356 | return false 357 | } 358 | if v >= '0' && v <= '9' { 359 | return false 360 | } 361 | if v >= 'a' && v <= 'z' { 362 | return false 363 | } 364 | return true 365 | } 366 | 367 | func fixupFormulaToken(candidate []rune, dx int, dy int) []rune { 368 | candidateX, candidateY, error := getCoordsFromCellIDRunes(candidate) 369 | if error != nil { 370 | return candidate 371 | } 372 | candidateX += dx 373 | candidateY += dy 374 | return coordsToCellIDRunes(candidateX, candidateY) 375 | } 376 | 377 | func updateFormula(sharedFormula xlsxSharedFormula, cellX int, cellY int) string { 378 | dx := cellX - sharedFormula.cellX 379 | dy := cellY - sharedFormula.cellY 380 | var formula []rune 381 | var candidate []rune 382 | quoted := false 383 | for _, v := range sharedFormula.F { 384 | if isDelimiter(v) { 385 | if len(candidate) > 0 { 386 | formula = append(formula, fixupFormulaToken(candidate, dx, dy)...) 387 | candidate = candidate[0:0] 388 | } 389 | formula = append(formula, v) 390 | } else if quoted { 391 | formula = append(formula, v) 392 | } else { 393 | candidate = append(candidate, v) 394 | } 395 | if v == '"' { 396 | quoted = !quoted 397 | } 398 | } 399 | 400 | return string(append(formula, fixupFormulaToken(candidate, dx, dy)...)) 401 | } 402 | 403 | func getFormulaFromCellData(rawcell xlsxC, cellX int, cellY int, si map[string]xlsxSharedFormula) string { 404 | var value string = "" 405 | var fval string = rawcell.F.F 406 | if len(fval) > 0 { 407 | value = fval 408 | } 409 | if len(rawcell.F.Si) > 0 && rawcell.F.T == "shared" { 410 | fvalSi := rawcell.F.Si 411 | if len(fval) > 0 { 412 | si[fvalSi] = xlsxSharedFormula{fval, rawcell.F.Ref, cellX, cellY} 413 | } else { 414 | sharedFormula, ok := si[fvalSi] 415 | if ok { 416 | value = updateFormula(sharedFormula, cellX, cellY) 417 | } 418 | } 419 | } 420 | return value 421 | } 422 | 423 | // readRowsFromSheet is an internal helper function that extracts the 424 | // rows from a XSLXWorksheet, poulates them with Cells and resolves 425 | // the value references from the reference table and stores them in 426 | func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, si map[string]xlsxSharedFormula, cellFilter CellFilter) Sheet { 427 | var maxCol, maxRow, colCount, rowCount int 428 | var reftable []string 429 | var err error 430 | var insertRowIndex, insertColIndex int 431 | 432 | reftable = file.referenceTable 433 | if len(Worksheet.Dimension.Ref) > 0 { 434 | _, _, maxCol, maxRow, err = getMaxMinFromDimensionRef(Worksheet.Dimension.Ref) 435 | } else { 436 | _, _, maxCol, maxRow, err = calculateMaxMinFromWorksheet(Worksheet) 437 | } 438 | if err != nil { 439 | panic(err.Error()) 440 | } 441 | rowCount = maxRow + 1 442 | colCount = maxCol + 1 443 | cells := make(map[CellCoord]Cell) 444 | rows := make(map[int]Row) 445 | insertRowIndex = 0 446 | for rowIndex := 0; rowIndex < len(Worksheet.SheetData.Row); rowIndex++ { 447 | rawrow := Worksheet.SheetData.Row[rowIndex] 448 | // Some spreadsheets will omit blank rows from the 449 | // stored data 450 | if insertRowIndex < rawrow.R { 451 | insertRowIndex = rawrow.R - 1 452 | } 453 | if rawrow.CustomHeight != 0 { 454 | rows[insertRowIndex] = Row{rawrow.Ht} 455 | } 456 | // range is not empty 457 | insertColIndex = 0 458 | for _, rawcell := range rawrow.C { 459 | x, _, error := getCoordsFromCellIDString(rawcell.R) 460 | if error == nil { 461 | insertColIndex = x 462 | } 463 | var cell Cell 464 | cell.Value = getValueFromCellData(rawcell, reftable) 465 | cell.formula = getFormulaFromCellData(rawcell, insertColIndex, insertRowIndex, si) 466 | cell.styleIndex = rawcell.S 467 | cell.styles = file.styles 468 | if cellFilter(cell) { 469 | cells[CellCoord{insertColIndex, insertRowIndex}] = cell 470 | } 471 | insertColIndex++ 472 | } 473 | insertRowIndex++ 474 | } 475 | var sheet Sheet 476 | sheet.Cells = cells 477 | sheet.Rows = rows 478 | sheet.MaxRow = rowCount 479 | sheet.MaxCol = colCount 480 | sheet.DefaultRowHeight = Worksheet.SheetFormatPr.DefaultRowHeight 481 | sheet.Protected = Worksheet.SheetProtection.Sheet 482 | return sheet 483 | } 484 | 485 | type indexedSheet struct { 486 | Index int 487 | Sheet *Sheet 488 | Error error 489 | } 490 | 491 | // readSheetFromFile is the logic of converting a xlsxSheet struct 492 | // into a Sheet struct. This work can be done in parallel and so 493 | // readSheetsFromZipFile will spawn an instance of this function per 494 | // sheet and get the results back on the provided channel. 495 | func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *File, sheetXMLMap map[string]string, cellFilter CellFilter) { 496 | result := &indexedSheet{Index: index, Sheet: nil, Error: nil} 497 | worksheet, error := getWorksheetFromSheet(rsheet, fi.worksheets, sheetXMLMap) 498 | if error != nil { 499 | result.Error = error 500 | sc <- result 501 | return 502 | } 503 | siIndex := make(map[string]xlsxSharedFormula) 504 | sheet := readRowsFromSheet(worksheet, fi, siIndex, cellFilter) 505 | result.Sheet = &sheet 506 | sc <- result 507 | } 508 | 509 | // readSheetsFromZipFile is an internal helper function that loops 510 | // over the Worksheets defined in the XSLXWorkbook and loads them into 511 | // Sheet objects stored in the Sheets slice of a xlsx.File struct. 512 | func readSheetsFromZipFile(f *zip.File, file *File, sheetXMLMap map[string]string, cellFilter CellFilter) ([]*Sheet, error) { 513 | var workbook *xlsxWorkbook 514 | var error error 515 | var rc io.ReadCloser 516 | var decoder *xml.Decoder 517 | var sheetCount int 518 | workbook = new(xlsxWorkbook) 519 | rc, error = f.Open() 520 | if error != nil { 521 | return nil, error 522 | } 523 | decoder = xml.NewDecoder(rc) 524 | error = decoder.Decode(workbook) 525 | if error != nil { 526 | return nil, error 527 | } 528 | sheetCount = len(workbook.Sheets.Sheet) 529 | sheets := make([]*Sheet, sheetCount) 530 | sheetChan := make(chan *indexedSheet, sheetCount) 531 | for i, rawsheet := range workbook.Sheets.Sheet { 532 | go readSheetFromFile(sheetChan, i, rawsheet, file, sheetXMLMap, cellFilter) 533 | } 534 | for j := 0; j < sheetCount; j++ { 535 | sheet := <-sheetChan 536 | if sheet.Error != nil { 537 | return nil, sheet.Error 538 | } 539 | sheet.Sheet.Name = workbook.Sheets.Sheet[sheet.Index].Name 540 | sheet.Sheet.WorkbookLockRevision = workbook.WorkbookProtection.LockRevision 541 | sheet.Sheet.WorkbookLockStructure = workbook.WorkbookProtection.LockStructure 542 | sheet.Sheet.WorkbookLockWindows = workbook.WorkbookProtection.LockWindows 543 | sheets[sheet.Index] = sheet.Sheet 544 | } 545 | return sheets, nil 546 | } 547 | 548 | // readSharedStringsFromZipFile() is an internal helper function to 549 | // extract a reference table from the sharedStrings.xml file within 550 | // the XLSX zip file. 551 | func readSharedStringsFromZipFile(f *zip.File) ([]string, error) { 552 | if f == nil { 553 | return []string{}, nil 554 | } 555 | var sst *xlsxSST 556 | var error error 557 | var rc io.ReadCloser 558 | var decoder *xml.Decoder 559 | var reftable []string 560 | rc, error = f.Open() 561 | if error != nil { 562 | return nil, error 563 | } 564 | sst = new(xlsxSST) 565 | decoder = xml.NewDecoder(rc) 566 | error = decoder.Decode(sst) 567 | if error != nil { 568 | return nil, error 569 | } 570 | reftable = MakeSharedStringRefTable(sst) 571 | return reftable, nil 572 | } 573 | 574 | // readStylesFromZipFile() is an internal helper function to 575 | // extract a style table from the style.xml file within 576 | // the XLSX zip file. 577 | func readStylesFromZipFile(f *zip.File) (*xlsxStyles, error) { 578 | var style *xlsxStyles 579 | var error error 580 | var rc io.ReadCloser 581 | var decoder *xml.Decoder 582 | rc, error = f.Open() 583 | if error != nil { 584 | return nil, error 585 | } 586 | style = new(xlsxStyles) 587 | decoder = xml.NewDecoder(rc) 588 | error = decoder.Decode(style) 589 | if error != nil { 590 | return nil, error 591 | } 592 | return style, nil 593 | } 594 | 595 | // readWorkbookRelationsFromZipFile is an internal helper function to 596 | // extract a map of relationship ID strings to the name of the 597 | // worksheet.xml file they refer to. The resulting map can be used to 598 | // reliably derefence the worksheets in the XLSX file. 599 | func readWorkbookRelationsFromZipFile(workbookRels *zip.File) (map[string]string, error) { 600 | var sheetXMLMap map[string]string 601 | var wbRelationships *xlsxWorkbookRels 602 | var rc io.ReadCloser 603 | var decoder *xml.Decoder 604 | var err error 605 | 606 | rc, err = workbookRels.Open() 607 | if err != nil { 608 | return nil, err 609 | } 610 | decoder = xml.NewDecoder(rc) 611 | wbRelationships = new(xlsxWorkbookRels) 612 | err = decoder.Decode(wbRelationships) 613 | if err != nil { 614 | return nil, err 615 | } 616 | sheetXMLMap = make(map[string]string) 617 | for _, rel := range wbRelationships.Relationships { 618 | if strings.HasSuffix(rel.Target, ".xml") && strings.HasPrefix(rel.Target, "worksheets/") { 619 | sheetXMLMap[rel.Id] = strings.Replace(rel.Target[len("worksheets/"):], ".xml", "", 1) 620 | } 621 | } 622 | return sheetXMLMap, nil 623 | } 624 | 625 | func HashT(cell Cell) bool { 626 | return true 627 | } 628 | 629 | // OpenFile() take the name of an XLSX file and returns a populated 630 | // xlsx.File struct for it. 631 | func OpenFileFilter(filename string, cellFilter CellFilter) (*File, error) { 632 | var f *zip.ReadCloser 633 | f, err := zip.OpenReader(filename) 634 | if err != nil { 635 | return nil, err 636 | } 637 | return ReadZip(f, cellFilter) 638 | } 639 | 640 | func OpenFile(filename string) (*File, error) { 641 | return OpenFileFilter(filename, HashT) 642 | } 643 | 644 | // ReadZip() takes a pointer to a zip.ReadCloser and returns a 645 | // xlsx.File struct populated with its contents. In most cases 646 | // ReadZip is not used directly, but is called internally by OpenFile. 647 | func ReadZip(f *zip.ReadCloser, cellFilter CellFilter) (*File, error) { 648 | defer f.Close() 649 | return ReadZipReader(&f.Reader, cellFilter) 650 | } 651 | 652 | // ReadZipReader() can be used to read xlsx in memory without touch filesystem. 653 | func ReadZipReader(r *zip.Reader, cellFilter CellFilter) (*File, error) { 654 | var err error 655 | var file *File 656 | var reftable []string 657 | var sharedStrings *zip.File 658 | var sheetMap map[string]*Sheet 659 | var sheetXMLMap map[string]string 660 | var sheets []*Sheet 661 | var style *xlsxStyles 662 | var styles *zip.File 663 | var v *zip.File 664 | var workbook *zip.File 665 | var workbookRels *zip.File 666 | var worksheets map[string]*zip.File 667 | 668 | file = new(File) 669 | worksheets = make(map[string]*zip.File, len(r.File)) 670 | for _, v = range r.File { 671 | switch v.Name { 672 | case "xl/sharedStrings.xml": 673 | sharedStrings = v 674 | case "xl/workbook.xml": 675 | workbook = v 676 | case "xl/_rels/workbook.xml.rels": 677 | workbookRels = v 678 | case "xl/styles.xml": 679 | styles = v 680 | default: 681 | if len(v.Name) > 14 { 682 | if v.Name[0:13] == "xl/worksheets" { 683 | worksheets[v.Name[14:len(v.Name)-4]] = v 684 | } 685 | } 686 | } 687 | } 688 | sheetXMLMap, err = readWorkbookRelationsFromZipFile(workbookRels) 689 | if err != nil { 690 | return nil, err 691 | } 692 | file.worksheets = worksheets 693 | reftable, err = readSharedStringsFromZipFile(sharedStrings) 694 | if err != nil { 695 | return nil, err 696 | } 697 | if reftable == nil { 698 | readerErr := new(XLSXReaderError) 699 | readerErr.Err = "No valid sharedStrings.xml found in XLSX file" 700 | return nil, readerErr 701 | } 702 | file.referenceTable = reftable 703 | style, err = readStylesFromZipFile(styles) 704 | if err != nil { 705 | return nil, err 706 | } 707 | file.styles = style 708 | sheets, err = readSheetsFromZipFile(workbook, file, sheetXMLMap, cellFilter) 709 | if err != nil { 710 | return nil, err 711 | } 712 | if sheets == nil { 713 | readerErr := new(XLSXReaderError) 714 | readerErr.Err = "No sheets found in XLSX File" 715 | return nil, readerErr 716 | } 717 | file.Sheets = sheets 718 | sheetMap = make(map[string]*Sheet, len(sheets)) 719 | for i := 0; i < len(sheets); i++ { 720 | sheetMap[sheets[i].Name] = sheets[i] 721 | } 722 | file.Sheet = sheetMap 723 | return file, nil 724 | } 725 | 726 | func NewFile() *File { 727 | return &File{} 728 | } 729 | --------------------------------------------------------------------------------