├── .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 |
65 |
68 | 0
69 |
70 |
73 | 1
74 |
75 |
76 |
83 |
86 | 2
87 |
88 |
91 | 3
92 |
93 |
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 | 0123
1
1
1
1
1
1
1
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 |
--------------------------------------------------------------------------------