Browse Source

Merge remote-tracking branch 'tealeg/master'

blackss2 10 years ago
parent
commit
2b26e0f34f
16 changed files with 599 additions and 98 deletions
  1. 1 1
      README.org
  2. 38 25
      cell.go
  3. 21 0
      cell_test.go
  4. 1 1
      file_test.go
  5. 244 0
      fuzzy_test.go
  6. 38 11
      lib.go
  7. 129 15
      lib_test.go
  8. 16 8
      sheet.go
  9. 28 1
      sheet_test.go
  10. 40 15
      style.go
  11. 4 4
      style_test.go
  12. BIN
      testdocs/badfile_noWorkbookRels.xlsx
  13. BIN
      testdocs/badfile_noWorksheets.xlsx
  14. 31 10
      xmlStyle.go
  15. 1 1
      xmlStyle_test.go
  16. 7 6
      xmlWorksheet.go

+ 1 - 1
README.org

@@ -98,7 +98,7 @@ This code is under a BSD style license:
   PROVIDED BY Geoffrey Teale ``AS IS'' AND ANY EXPRESS OR IMPLIED
   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-  DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE
+  DISCLAIMED. IN NO EVENT SHALL GEOFFREY TEALE OR CONTRIBUTORS BE
   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR

+ 38 - 25
cell.go

@@ -275,33 +275,34 @@ func (c *Cell) GetNumberFormat() string {
 	return c.numFmt
 }
 
-func (c *Cell) formatToFloat(format string) string {
+func (c *Cell) formatToFloat(format string) (string, error) {
 	f, err := strconv.ParseFloat(c.Value, 64)
 	if err != nil {
-		return err.Error()
+		return c.Value, err
 	}
-	return fmt.Sprintf(format, f)
+	return fmt.Sprintf(format, f), nil
 }
 
-func (c *Cell) formatToInt(format string) string {
+func (c *Cell) formatToInt(format string) (string, error) {
 	f, err := strconv.ParseFloat(c.Value, 64)
 	if err != nil {
-		return err.Error()
+		return c.Value, err
 	}
-	return fmt.Sprintf(format, int(f))
+	return fmt.Sprintf(format, int(f)), nil
 }
 
-// FormattedValue returns the formatted version of the value.
-// If it's a string type, c.Value will just be returned. Otherwise,
-// it will attempt to apply Excel formatting to the value.
-func (c *Cell) FormattedValue() string {
+// SafeFormattedValue returns a value, and possibly an error condition
+// from a Cell.  If it is possible to apply a format to the cell
+// value, it will do so, if not then an error will be returned, along
+// with the raw value of the Cell.
+func (c *Cell) SafeFormattedValue() (string, error) {
 	var numberFormat = c.GetNumberFormat()
 	if isTimeFormat(numberFormat) {
 		return parseTime(c)
 	}
 	switch numberFormat {
 	case builtInNumFmt[builtInNumFmtIndex_GENERAL], builtInNumFmt[builtInNumFmtIndex_STRING]:
-		return c.Value
+		return c.Value, nil
 	case builtInNumFmt[builtInNumFmtIndex_INT], "#,##0":
 		return c.formatToInt("%d")
 	case builtInNumFmt[builtInNumFmtIndex_FLOAT], "#,##0.00":
@@ -309,48 +310,60 @@ func (c *Cell) FormattedValue() string {
 	case "#,##0 ;(#,##0)", "#,##0 ;[red](#,##0)":
 		f, err := strconv.ParseFloat(c.Value, 64)
 		if err != nil {
-			return err.Error()
+			return c.Value, err
 		}
 		if f < 0 {
 			i := int(math.Abs(f))
-			return fmt.Sprintf("(%d)", i)
+			return fmt.Sprintf("(%d)", i), nil
 		}
 		i := int(f)
-		return fmt.Sprintf("%d", i)
+		return fmt.Sprintf("%d", i), nil
 	case "#,##0.00;(#,##0.00)", "#,##0.00;[red](#,##0.00)":
 		f, err := strconv.ParseFloat(c.Value, 64)
 		if err != nil {
-			return err.Error()
+			return c.Value, err
 		}
 		if f < 0 {
-			return fmt.Sprintf("(%.2f)", f)
+			return fmt.Sprintf("(%.2f)", f), nil
 		}
-		return fmt.Sprintf("%.2f", f)
+		return fmt.Sprintf("%.2f", f), nil
 	case "0%":
 		f, err := strconv.ParseFloat(c.Value, 64)
 		if err != nil {
-			return err.Error()
+			return c.Value, err
 		}
 		f = f * 100
-		return fmt.Sprintf("%d%%", int(f))
+		return fmt.Sprintf("%d%%", int(f)), nil
 	case "0.00%":
 		f, err := strconv.ParseFloat(c.Value, 64)
 		if err != nil {
-			return err.Error()
+			return c.Value, err
 		}
 		f = f * 100
-		return fmt.Sprintf("%.2f%%", f)
+		return fmt.Sprintf("%.2f%%", f), nil
 	case "0.00e+00", "##0.0e+0":
 		return c.formatToFloat("%e")
 	}
-	return c.Value
+	return c.Value, nil
+
+}
+
+// FormattedValue returns the formatted version of the value.
+// If it's a string type, c.Value will just be returned. Otherwise,
+// it will attempt to apply Excel formatting to the value.
+func (c *Cell) FormattedValue() string {
+	value, err := c.SafeFormattedValue()
+	if err != nil {
+		return err.Error()
+	}
+	return value
 }
 
 // parseTime returns a string parsed using time.Time
-func parseTime(c *Cell) string {
+func parseTime(c *Cell) (string, error) {
 	f, err := strconv.ParseFloat(c.Value, 64)
 	if err != nil {
-		return err.Error()
+		return c.Value, err
 	}
 	val := TimeFromExcelTime(f, c.date1904)
 	format := c.GetNumberFormat()
@@ -389,7 +402,7 @@ func parseTime(c *Cell) string {
 		format = strings.Replace(format, "[3]", "3", 1)
 		format = strings.Replace(format, "[15]", "15", 1)
 	}
-	return val.Format(format)
+	return val.Format(format), nil
 }
 
 // isTimeFormat checks whether an Excel format string represents

+ 21 - 0
cell_test.go

@@ -114,8 +114,29 @@ func (l *CellSuite) TestSetFloat(c *C) {
 	c.Assert(cell.Value, Equals, "37947.75334343")
 }
 
+// SafeFormattedValue returns an error for formatting errors
+func (l *CellSuite) TestSafeFormattedValueErrorsOnBadFormat(c *C) {
+	cell := Cell{Value: "Fudge Cake"}
+	cell.numFmt = "#,##0 ;(#,##0)"
+	value, err := cell.SafeFormattedValue()
+	c.Assert(value, Equals, "Fudge Cake")
+	c.Assert(err, NotNil)
+	c.Assert(err.Error(), Equals, "strconv.ParseFloat: parsing \"Fudge Cake\": invalid syntax")
+}
+
+// FormattedValue returns a string containing error text for formatting errors
+func (l *CellSuite) TestFormattedValueReturnsErrorAsValueForBadFormat(c *C) {
+	cell := Cell{Value: "Fudge Cake"}
+	cell.numFmt = "#,##0 ;(#,##0)"
+	value := cell.FormattedValue()
+	c.Assert(value, Equals, "strconv.ParseFloat: parsing \"Fudge Cake\": invalid syntax")
+}
+
 // We can return a string representation of the formatted data
 func (l *CellSuite) TestFormattedValue(c *C) {
+	// XXX TODO, this test should probably be split down, and made
+	// in terms of SafeFormattedValue, as FormattedValue wraps
+	// that function now.
 	cell := Cell{Value: "37947.7500001"}
 	negativeCell := Cell{Value: "-37947.7500001"}
 	smallCell := Cell{Value: "0.007"}

+ 1 - 1
file_test.go

@@ -659,7 +659,7 @@ func (l *FileSuite) TestMarshalFile(c *C) {
 	// For now we only allow simple string data in the
 	// spreadsheet.  Style support will follow.
 	expectedStyles := `<?xml version="1.0" encoding="UTF-8"?>
-<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="1"><font><sz val="12"/><name val="Verdana"/><family val="0"/><charset val="0"/></font></fonts><fills count="2"><fill><patternFill patternType="none"><fgColor rgb="FFFFFFFF"/><bgColor rgb="00000000"/></patternFill></fill><fill><patternFill patternType="lightGrey"/></fill></fills><borders count="1"><border><left style="none"/><right style="none"/><top style="none"/><bottom style="none"/></border></borders><cellStyleXfs count="1"><xf applyAlignment="0" applyBorder="0" applyFont="0" applyFill="0" applyNumberFormat="0" applyProtection="0" borderId="0" fillId="0" fontId="0" numFmtId="0"><alignment horizontal="" indent="0" shrinkToFit="0" textRotation="0" vertical="" wrapText="0"/></xf></cellStyleXfs><cellXfs count="1"><xf applyAlignment="0" applyBorder="0" applyFont="0" applyFill="0" applyNumberFormat="0" applyProtection="0" borderId="0" fillId="0" fontId="0" numFmtId="0"><alignment horizontal="" indent="0" shrinkToFit="0" textRotation="0" vertical="" wrapText="0"/></xf></cellXfs></styleSheet>`
+<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="1"><font><sz val="12"/><name val="Verdana"/><family val="0"/><charset val="0"/></font></fonts><fills count="2"><fill><patternFill patternType="none"><fgColor rgb="FFFFFFFF"/><bgColor rgb="00000000"/></patternFill></fill><fill><patternFill patternType="lightGray"/></fill></fills><borders count="1"><border><left style="none"></left><right style="none"></right><top style="none"></top><bottom style="none"></bottom></border></borders><cellStyleXfs count="1"><xf applyAlignment="0" applyBorder="0" applyFont="0" applyFill="0" applyNumberFormat="0" applyProtection="0" borderId="0" fillId="0" fontId="0" numFmtId="0"><alignment horizontal="" indent="0" shrinkToFit="0" textRotation="0" vertical="" wrapText="0"/></xf></cellStyleXfs><cellXfs count="1"><xf applyAlignment="0" applyBorder="0" applyFont="0" applyFill="0" applyNumberFormat="0" applyProtection="0" borderId="0" fillId="0" fontId="0" numFmtId="0"><alignment horizontal="" indent="0" shrinkToFit="0" textRotation="0" vertical="" wrapText="0"/></xf></cellXfs></styleSheet>`
 	c.Assert(parts["xl/styles.xml"], Equals, expectedStyles)
 }
 

+ 244 - 0
fuzzy_test.go

@@ -0,0 +1,244 @@
+// +build fuzzy
+
+package xlsx
+
+import (
+	"archive/zip"
+	"bytes"
+	"encoding/xml"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"math/rand"
+	"path/filepath"
+	"reflect"
+	"strconv"
+	"testing"
+	"time"
+
+	. "gopkg.in/check.v1"
+)
+
+type Fuzzy struct{}
+
+var _ = Suite(&Fuzzy{})
+var randseed *int64 = flag.Int64("test.seed", time.Now().Unix(), "Set the random seed of the test for repeatable results")
+
+type tokenchange struct {
+	file bytes.Buffer
+	old  xml.Token
+	new  xml.Token
+}
+
+type filechange struct {
+	File *zip.Reader
+	Name string
+	Old  xml.Token
+	New  xml.Token
+}
+
+var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+var numbers = []rune("0123456789")
+
+func randString(n int) []byte {
+	b := make([]rune, n)
+	for i := range b {
+		b[i] = letters[rand.Intn(len(letters))]
+	}
+	return []byte(string(b))
+}
+
+func randInt(n int) []byte {
+	b := make([]rune, n)
+	for i := range b {
+		b[i] = numbers[rand.Intn(len(numbers))]
+	}
+	return []byte(string(b))
+}
+
+//This function creates variations on tokens without regards as to positions in the file.
+func getTokenVariations(t xml.Token) []xml.Token {
+	var result []xml.Token = make([]xml.Token, 0)
+	switch t := t.(type) {
+	case xml.CharData:
+		{
+			//If the token is a number try some random number
+			if _, err := strconv.Atoi(string(t)); err == nil {
+				result = append(result, xml.CharData(randInt(rand.Intn(15))))
+			}
+
+			result = append(result, xml.CharData(randString(rand.Intn(100))))
+			return result
+		}
+	case xml.StartElement:
+		{
+			for k := range t.Attr {
+				if _, err := strconv.Atoi(string(t.Attr[k].Value)); err == nil {
+					start := xml.CopyToken(t).(xml.StartElement)
+					start.Attr[k].Value = string(randInt(rand.Intn(15)))
+					result = append(result, start)
+				}
+				start := xml.CopyToken(t).(xml.StartElement)
+				start.Attr[k].Value = string(randString(rand.Intn(100)))
+				result = append(result, start)
+			}
+			return result
+		}
+
+	default:
+		{
+			return make([]xml.Token, 0) // No variations on non char tokens yet
+		}
+	}
+}
+
+func variationsXML(f *zip.File) chan tokenchange {
+	result := make(chan tokenchange)
+	r, _ := f.Open()
+	xmlReader := xml.NewDecoder(r)
+	var tokenList []xml.Token
+	for {
+		if t, err := xmlReader.Token(); err == nil {
+			tokenList = append(tokenList, xml.CopyToken(t))
+		} else {
+			break
+		}
+	}
+
+	go func() {
+		//Over every token we want to break
+		for TokenToBreak, _ := range tokenList {
+			//Get the ways we can break that token
+			for _, brokenToken := range getTokenVariations(tokenList[TokenToBreak]) {
+				var buf bytes.Buffer
+				xmlWriter := xml.NewEncoder(&buf)
+				//Now create an xml file where one token is broken
+				for currentToken, t := range tokenList {
+					if currentToken == TokenToBreak {
+						xmlWriter.EncodeToken(brokenToken)
+					} else {
+						xmlWriter.EncodeToken(t)
+					}
+				}
+				xmlWriter.Flush()
+				result <- tokenchange{buf, tokenList[TokenToBreak], brokenToken}
+			}
+		}
+		close(result)
+	}()
+	return result
+}
+
+func generateBrokenFiles(r *zip.Reader) chan filechange {
+	result := make(chan filechange)
+	go func() {
+		count := 0
+		//For every file in the zip we want variation on
+		for breakIndex, fileToBreak := range r.File {
+			if filepath.Ext(fileToBreak.Name) != ".xml" {
+				continue //We cannot create variations on non-xml files
+			}
+
+			variationCount := 0
+			//For every broken version of that file
+			for changedFile := range variationsXML(fileToBreak) {
+				variationCount++
+				var buffer bytes.Buffer
+				//Create a new xlsx file in memory
+				outZip := zip.NewWriter(&buffer)
+				w, err := outZip.Create(fileToBreak.Name)
+				if err != nil {
+					log.Fatal(err)
+				}
+				//Add modified file to xlsx
+				_, err = changedFile.file.WriteTo(w)
+				if err != nil {
+					log.Fatal("changedFile.file.WriteTo", err)
+				}
+				//Add other, unchanged, files.
+				for otherIndex, otherFile := range r.File {
+					if breakIndex == otherIndex {
+						continue
+					}
+					to, err := outZip.Create(otherFile.Name)
+					if err != nil {
+						log.Fatal("Could not add new file to xlsx due to", err)
+					}
+					from, err := otherFile.Open()
+					if err != nil {
+						log.Fatal("Could not open original file from template xlsx due to", err)
+					}
+					io.Copy(to, from)
+					from.Close()
+				}
+				outZip.Close()
+
+				//Return this combination of broken files
+				b := buffer.Bytes()
+				var res filechange
+				res.File, _ = zip.NewReader(bytes.NewReader(b), int64(len(b)))
+				res.Name = fileToBreak.Name
+				res.Old = changedFile.old
+				res.New = changedFile.new
+				result <- res
+				count++
+			}
+		}
+		close(result)
+	}()
+	return result
+}
+
+func Raises(f func()) (err interface{}) {
+	defer func() {
+		err = recover()
+	}()
+	err = nil
+	f()
+	return
+}
+
+func tokenToString(t xml.Token) string {
+	switch t := t.(type) {
+	case xml.CharData:
+		{
+			return string(t)
+		}
+	default:
+		{
+			return fmt.Sprint(t)
+		}
+	}
+}
+
+func (f *Fuzzy) TestRandomBrokenParts(c *C) {
+	if testing.Short() {
+		c.Log("This test, tests many versions of an xlsx file and might take a while, it is being skipped")
+		c.SucceedNow()
+	}
+	log.Println("Fuzzy test is using this -test.seed=" + strconv.FormatInt(*randseed, 10))
+	rand.Seed(*randseed)
+	template, err := zip.OpenReader("./testdocs/testfile.xlsx")
+	c.Assert(err, IsNil)
+	defer template.Close()
+
+	count := 0
+
+	for brokenFile := range generateBrokenFiles(&template.Reader) {
+		count++
+		if testing.Verbose() {
+			//If the library panics fatally it would be nice to know why
+			log.Println("Testing change to ", brokenFile.Name, " on token ", tokenToString(brokenFile.Old), " of type ", reflect.TypeOf(brokenFile.Old), " to ", tokenToString(brokenFile.New))
+		}
+
+		if e := Raises(func() { ReadZipReader(brokenFile.File) }); e != nil {
+
+			c.Log("Some file with random changes did raise an exception instead of returning an error", e)
+			c.Log("Testing change to ", brokenFile.Name, " on token ", tokenToString(brokenFile.Old), " of type ", reflect.TypeOf(brokenFile.Old), " to ", tokenToString(brokenFile.New))
+			c.FailNow()
+		}
+
+	}
+	c.Succeed()
+}

+ 38 - 11
lib.go

@@ -17,8 +17,8 @@ type XLSXReaderError struct {
 	Err string
 }
 
-// String() returns a string value from an XLSXReaderError struct in
-// order that it might comply with the os.Error interface.
+// Error returns a string value from an XLSXReaderError struct in order
+// that it might comply with the builtin.error interface.
 func (e *XLSXReaderError) Error() string {
 	return e.Err
 }
@@ -251,13 +251,14 @@ func calculateMaxMinFromWorksheet(worksheet *xlsxWorksheet) (minx, miny, maxx, m
 // return an empty Row large enough to encompass that span and
 // populate it with empty cells.  All rows start from cell 1 -
 // regardless of the lower bound of the span.
-func makeRowFromSpan(spans string) *Row {
+func makeRowFromSpan(spans string, sheet *Sheet) *Row {
 	var error error
 	var upper int
 	var row *Row
 	var cell *Cell
 
 	row = new(Row)
+	row.Sheet = sheet
 	_, upper, error = getRangeFromString(spans)
 	if error != nil {
 		panic(error)
@@ -273,12 +274,13 @@ func makeRowFromSpan(spans string) *Row {
 }
 
 // makeRowFromRaw returns the Row representation of the xlsxRow.
-func makeRowFromRaw(rawrow xlsxRow) *Row {
+func makeRowFromRaw(rawrow xlsxRow, sheet *Sheet) *Row {
 	var upper int
 	var row *Row
 	var cell *Cell
 
 	row = new(Row)
+	row.Sheet = sheet
 	upper = -1
 
 	for _, rawcell := range rawrow.C {
@@ -305,9 +307,10 @@ func makeRowFromRaw(rawrow xlsxRow) *Row {
 	return row
 }
 
-func makeEmptyRow() *Row {
+func makeEmptyRow(sheet *Sheet) *Row {
 	row := new(Row)
 	row.Cells = make([]*Cell, 0)
+	row.Sheet = sheet
 	return row
 }
 
@@ -417,7 +420,7 @@ func fillCellData(rawcell xlsxC, reftable *RefTable, sharedFormulas map[int]shar
 // rows from a XSLXWorksheet, populates them with Cells and resolves
 // the value references from the reference table and stores them in
 // the rows and columns.
-func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File) ([]*Row, []*Col, int, int) {
+func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet) ([]*Row, []*Col, int, int) {
 	var rows []*Row
 	var cols []*Col
 	var row *Row
@@ -439,6 +442,7 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File) ([]*Row, []*Col, in
 	if err != nil {
 		panic(err.Error())
 	}
+
 	rowCount = maxRow + 1
 	colCount = maxCol + 1
 	rows = make([]*Row, rowCount)
@@ -473,23 +477,27 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File) ([]*Row, []*Col, in
 
 	// insert leading empty rows that is in front of minRow
 	for rowIndex := 0; rowIndex < minRow; rowIndex++ {
-		rows[rowIndex] = makeEmptyRow()
+		rows[rowIndex] = makeEmptyRow(sheet)
 	}
 
+	numRows := len(rows)
 	for rowIndex := 0; rowIndex < len(Worksheet.SheetData.Row); rowIndex++ {
 		rawrow := Worksheet.SheetData.Row[rowIndex]
 		// Some spreadsheets will omit blank rows from the
 		// stored data
 		for rawrow.R > (insertRowIndex + 1) {
 			// Put an empty Row into the array
-			rows[insertRowIndex-minRow] = makeEmptyRow()
+			index := insertRowIndex - minRow
+			if index < numRows {
+				rows[index] = makeEmptyRow(sheet)
+			}
 			insertRowIndex++
 		}
 		// range is not empty and only one range exist
 		if len(rawrow.Spans) != 0 && strings.Count(rawrow.Spans, ":") == 1 {
-			row = makeRowFromSpan(rawrow.Spans)
+			row = makeRowFromSpan(rawrow.Spans, sheet)
 		} else {
-			row = makeRowFromRaw(rawrow)
+			row = makeRowFromRaw(rawrow, sheet)
 		}
 
 		row.Hidden = rawrow.Hidden
@@ -559,6 +567,19 @@ func readSheetViews(xSheetViews xlsxSheetViews) []SheetView {
 // sheet and get the results back on the provided channel.
 func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *File, sheetXMLMap map[string]string) {
 	result := &indexedSheet{Index: index, Sheet: nil, Error: nil}
+	defer func() {
+		if e := recover(); e != nil {
+			switch e.(type) {
+			case error:
+				result.Error = e.(error)
+			default:
+				result.Error = errors.New("unexpected error")
+			}
+			// The only thing here, is if one close the channel. but its not the case
+			sc <- result
+		}
+	}()
+
 	worksheet, error := getWorksheetFromSheet(rsheet, fi.worksheets, sheetXMLMap)
 	if error != nil {
 		result.Error = error
@@ -567,7 +588,7 @@ func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *F
 	}
 	sheet := new(Sheet)
 	sheet.File = fi
-	sheet.Rows, sheet.Cols, sheet.MaxCol, sheet.MaxRow = readRowsFromSheet(worksheet, fi)
+	sheet.Rows, sheet.Cols, sheet.MaxCol, sheet.MaxRow = readRowsFromSheet(worksheet, fi, sheet)
 	sheet.Hidden = rsheet.State == sheetStateHidden || rsheet.State == sheetStateVeryHidden
 	sheet.SheetViews = readSheetViews(worksheet.SheetViews)
 
@@ -828,10 +849,16 @@ func ReadZipReader(r *zip.Reader) (*File, error) {
 			}
 		}
 	}
+	if workbookRels == nil {
+		return nil, fmt.Errorf("xl/_rels/workbook.xml.rels not found in input xlsx.")
+	}
 	sheetXMLMap, err = readWorkbookRelationsFromZipFile(workbookRels)
 	if err != nil {
 		return nil, err
 	}
+	if len(worksheets) == 0 {
+		return nil, fmt.Errorf("Input xlsx contains no worksheets.")
+	}
 	file.worksheets = worksheets
 	reftable, err = readSharedStringsFromZipFile(sharedStrings)
 	if err != nil {

+ 129 - 15
lib_test.go

@@ -13,6 +13,20 @@ type LibSuite struct{}
 
 var _ = Suite(&LibSuite{})
 
+// Attempting to open a file without workbook.xml.rels returns an error.
+func (l *LibSuite) TestReadZipReaderWithFileWithNoWorkbookRels(c *C) {
+	_, err := OpenFile("./testdocs/badfile_noWorkbookRels.xlsx")
+	c.Assert(err, NotNil)
+	c.Assert(err.Error(), Equals, "xl/_rels/workbook.xml.rels not found in input xlsx.")
+}
+
+// Attempting to open a file with no worksheets returns an error.
+func (l *LibSuite) TestReadZipReaderWithFileWithNoWorksheets(c *C) {
+	_, err := OpenFile("./testdocs/badfile_noWorksheets.xlsx")
+	c.Assert(err, NotNil)
+	c.Assert(err.Error(), Equals, "Input xlsx contains no worksheets.")
+}
+
 // which they are contained from the XLSX file, even when the
 // worksheet files have arbitrary, non-numeric names.
 func (l *LibSuite) TestReadWorkbookRelationsFromZipFileWithFunnyNames(c *C) {
@@ -225,18 +239,23 @@ func (l *LibSuite) TestMakeRowFromSpan(c *C) {
 	var rangeString string
 	var row *Row
 	var length int
+	var sheet *Sheet
+	sheet = new(Sheet)
 	rangeString = "1:3"
-	row = makeRowFromSpan(rangeString)
+	row = makeRowFromSpan(rangeString, sheet)
 	length = len(row.Cells)
 	c.Assert(length, Equals, 3)
+	c.Assert(row.Sheet, Equals, sheet)
 	rangeString = "5:7" // Note - we ignore lower bound!
-	row = makeRowFromSpan(rangeString)
+	row = makeRowFromSpan(rangeString, sheet)
 	length = len(row.Cells)
 	c.Assert(length, Equals, 7)
+	c.Assert(row.Sheet, Equals, sheet)
 	rangeString = "1:1"
-	row = makeRowFromSpan(rangeString)
+	row = makeRowFromSpan(rangeString, sheet)
 	length = len(row.Cells)
 	c.Assert(length, Equals, 1)
+	c.Assert(row.Sheet, Equals, sheet)
 }
 
 func (l *LibSuite) TestReadRowsFromSheet(c *C) {
@@ -300,10 +319,12 @@ func (l *LibSuite) TestReadRowsFromSheet(c *C) {
 	c.Assert(err, IsNil)
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
-	rows, cols, maxCols, maxRows := readRowsFromSheet(worksheet, file)
+	sheet := new(Sheet)
+	rows, cols, maxCols, maxRows := readRowsFromSheet(worksheet, file, sheet)
 	c.Assert(maxRows, Equals, 2)
 	c.Assert(maxCols, Equals, 2)
 	row := rows[0]
+	c.Assert(row.Sheet, Equals, sheet)
 	c.Assert(len(row.Cells), Equals, 2)
 	cell1 := row.Cells[0]
 	c.Assert(cell1.Value, Equals, "Foo")
@@ -321,6 +342,79 @@ func (l *LibSuite) TestReadRowsFromSheet(c *C) {
 	c.Assert(pane.YSplit, Equals, 1.0)
 }
 
+// An invalid value in the "r" attribute in a <row> was causing a panic
+// in readRowsFromSheet. This test is a copy of TestReadRowsFromSheet,
+// with the important difference of the value 1048576 below in <row r="1048576", which is
+// higher than the number of rows in the sheet. That number itself isn't significant;
+// it just happens to be the value found to trigger the error in a user's file.
+func (l *LibSuite) TestReadRowsFromSheetBadR(c *C) {
+	var sharedstringsXML = bytes.NewBufferString(`
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="4" uniqueCount="4">
+  <si>
+    <t>Foo</t>
+  </si>
+  <si>
+    <t>Bar</t>
+  </si>
+  <si>
+    <t xml:space="preserve">Baz </t>
+  </si>
+  <si>
+    <t>Quuk</t>
+  </si>
+</sst>`)
+	var sheetxml = bytes.NewBufferString(`
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
+           xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
+  <dimension ref="A1:B2"/>
+  <sheetViews>
+    <sheetView tabSelected="1" workbookViewId="0">
+      <selection activeCell="C2" sqref="C2"/>
+	  <pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/>
+    </sheetView>
+  </sheetViews>
+  <sheetFormatPr baseColWidth="10" defaultRowHeight="15"/>
+  <sheetData>
+    <row r="1" spans="1:2">
+      <c r="A1" t="s">
+        <v>0</v>
+      </c>
+      <c r="B1" t="s">
+        <v>1</v>
+      </c>
+    </row>
+    <row r="1048576" spans="1:2">
+      <c r="A2" t="s">
+        <v>2</v>
+      </c>
+      <c r="B2" t="s">
+        <v>3</v>
+      </c>
+    </row>
+  </sheetData>
+  <pageMargins left="0.7" right="0.7"
+               top="0.78740157499999996"
+               bottom="0.78740157499999996"
+               header="0.3"
+               footer="0.3"/>
+</worksheet>`)
+	worksheet := new(xlsxWorksheet)
+	err := xml.NewDecoder(sheetxml).Decode(worksheet)
+	c.Assert(err, IsNil)
+	sst := new(xlsxSST)
+	err = xml.NewDecoder(sharedstringsXML).Decode(sst)
+	c.Assert(err, IsNil)
+	file := new(File)
+	file.referenceTable = MakeSharedStringRefTable(sst)
+
+	sheet := new(Sheet)
+	// Discarding all return values; this test is a regression for
+	// a panic due to an "index out of range."
+	readRowsFromSheet(worksheet, file, sheet)
+}
+
 func (l *LibSuite) TestReadRowsFromSheetWithLeadingEmptyRows(c *C) {
 	var sharedstringsXML = bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2"><si><t>ABC</t></si><si><t>DEF</t></si></sst>`)
@@ -363,7 +457,8 @@ func (l *LibSuite) TestReadRowsFromSheetWithLeadingEmptyRows(c *C) {
 
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
-	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file)
+	sheet := new(Sheet)
+	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file, sheet)
 	c.Assert(maxRows, Equals, 5)
 	c.Assert(maxCols, Equals, 1)
 
@@ -420,7 +515,8 @@ func (l *LibSuite) TestReadRowsFromSheetWithLeadingEmptyCols(c *C) {
 
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
-	rows, cols, maxCols, maxRows := readRowsFromSheet(worksheet, file)
+	sheet := new(Sheet)
+	rows, cols, maxCols, maxRows := readRowsFromSheet(worksheet, file, sheet)
 	c.Assert(maxRows, Equals, 2)
 	c.Assert(maxCols, Equals, 4)
 
@@ -526,11 +622,13 @@ func (l *LibSuite) TestReadRowsFromSheetWithEmptyCells(c *C) {
 	c.Assert(err, IsNil)
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
-	rows, cols, maxCols, maxRows := readRowsFromSheet(worksheet, file)
+	sheet := new(Sheet)
+	rows, cols, maxCols, maxRows := readRowsFromSheet(worksheet, file, sheet)
 	c.Assert(maxRows, Equals, 3)
 	c.Assert(maxCols, Equals, 3)
 
 	row := rows[2]
+	c.Assert(row.Sheet, Equals, sheet)
 	c.Assert(len(row.Cells), Equals, 3)
 
 	cell1 := row.Cells[0]
@@ -568,11 +666,13 @@ func (l *LibSuite) TestReadRowsFromSheetWithTrailingEmptyCells(c *C) {
 
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
-	rows, _, maxCol, maxRow := readRowsFromSheet(worksheet, file)
+	sheet := new(Sheet)
+	rows, _, maxCol, maxRow := readRowsFromSheet(worksheet, file, sheet)
 	c.Assert(maxCol, Equals, 4)
 	c.Assert(maxRow, Equals, 8)
 
 	row = rows[0]
+	c.Assert(row.Sheet, Equals, sheet)
 	c.Assert(len(row.Cells), Equals, 4)
 
 	cell1 = row.Cells[0]
@@ -588,6 +688,7 @@ func (l *LibSuite) TestReadRowsFromSheetWithTrailingEmptyCells(c *C) {
 	c.Assert(cell4.Value, Equals, "D")
 
 	row = rows[1]
+	c.Assert(row.Sheet, Equals, sheet)
 	c.Assert(len(row.Cells), Equals, 4)
 
 	cell1 = row.Cells[0]
@@ -675,10 +776,12 @@ func (l *LibSuite) TestReadRowsFromSheetWithMultipleSpans(c *C) {
 	c.Assert(err, IsNil)
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
-	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file)
+	sheet := new(Sheet)
+	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file, sheet)
 	c.Assert(maxRows, Equals, 2)
 	c.Assert(maxCols, Equals, 4)
 	row := rows[0]
+	c.Assert(row.Sheet, Equals, sheet)
 	c.Assert(len(row.Cells), Equals, 4)
 	cell1 := row.Cells[0]
 	c.Assert(cell1.Value, Equals, "Foo")
@@ -748,10 +851,12 @@ func (l *LibSuite) TestReadRowsFromSheetWithMultipleTypes(c *C) {
 	c.Assert(err, IsNil)
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
-	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file)
+	sheet := new(Sheet)
+	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file, sheet)
 	c.Assert(maxRows, Equals, 1)
 	c.Assert(maxCols, Equals, 6)
 	row := rows[0]
+	c.Assert(row.Sheet, Equals, sheet)
 	c.Assert(len(row.Cells), Equals, 6)
 
 	cell1 := row.Cells[0]
@@ -815,10 +920,12 @@ func (l *LibSuite) TestReadRowsFromSheetWithHiddenColumn(c *C) {
 	c.Assert(err, IsNil)
 	file := new(File)
 	file.referenceTable = MakeSharedStringRefTable(sst)
-	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file)
+	sheet := new(Sheet)
+	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file, sheet)
 	c.Assert(maxRows, Equals, 1)
 	c.Assert(maxCols, Equals, 2)
 	row := rows[0]
+	c.Assert(row.Sheet, Equals, sheet)
 	c.Assert(len(row.Cells), Equals, 2)
 
 	cell1 := row.Cells[0]
@@ -842,9 +949,11 @@ func (l *LibSuite) TestReadRowFromRaw(c *C) {
 	cell = xlsxC{R: "A1"}
 	cell = xlsxC{R: "A2"}
 	rawRow.C = append(rawRow.C, cell)
-	row = makeRowFromRaw(rawRow)
+	sheet := new(Sheet)
+	row = makeRowFromRaw(rawRow, sheet)
 	c.Assert(row, NotNil)
 	c.Assert(row.Cells, HasLen, 1)
+	c.Assert(row.Sheet, Equals, sheet)
 }
 
 // When a cell claims it is at a position greater than its ordinal
@@ -859,9 +968,11 @@ func (l *LibSuite) TestReadRowFromRawWithMissingCells(c *C) {
 	rawRow.C = append(rawRow.C, cell)
 	cell = xlsxC{R: "E1"}
 	rawRow.C = append(rawRow.C, cell)
-	row = makeRowFromRaw(rawRow)
+	sheet := new(Sheet)
+	row = makeRowFromRaw(rawRow, sheet)
 	c.Assert(row, NotNil)
 	c.Assert(row.Cells, HasLen, 5)
+	c.Assert(row.Sheet, Equals, sheet)
 }
 
 // We can cope with missing coordinate references
@@ -879,9 +990,11 @@ func (l *LibSuite) TestReadRowFromRawWithPartialCoordinates(c *C) {
 	rawRow.C = append(rawRow.C, cell)
 	cell = xlsxC{}
 	rawRow.C = append(rawRow.C, cell)
-	row = makeRowFromRaw(rawRow)
+	sheet := new(Sheet)
+	row = makeRowFromRaw(rawRow, sheet)
 	c.Assert(row, NotNil)
 	c.Assert(row.Cells, HasLen, 27)
+	c.Assert(row.Sheet, Equals, sheet)
 }
 
 func (l *LibSuite) TestSharedFormulas(c *C) {
@@ -935,7 +1048,8 @@ func (l *LibSuite) TestSharedFormulas(c *C) {
 	c.Assert(err, IsNil)
 
 	file := new(File)
-	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file)
+	sheet := new(Sheet)
+	rows, _, maxCols, maxRows := readRowsFromSheet(worksheet, file, sheet)
 	c.Assert(maxCols, Equals, 3)
 	c.Assert(maxRows, Equals, 2)
 

+ 16 - 8
sheet.go

@@ -54,8 +54,7 @@ func (s *Sheet) maybeAddCol(cellCount int) {
 			Min:       cellCount,
 			Max:       cellCount,
 			Hidden:    false,
-			Collapsed: false,
-			Width:     ColWidth}
+			Collapsed: false}
 		s.Cols = append(s.Cols, col)
 		s.MaxCol = cellCount
 	}
@@ -110,6 +109,11 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 	maxRow := 0
 	maxCell := 0
 
+	if s.SheetFormat.DefaultRowHeight != 0 {
+		worksheet.SheetFormatPr.DefaultRowHeight = s.SheetFormat.DefaultRowHeight
+	}
+	worksheet.SheetFormatPr.DefaultColWidth = s.SheetFormat.DefaultColWidth
+
 	colsXfIdList := make([]int, len(s.Cols))
 	worksheet.Cols = xlsxCols{Col: []xlsxCol{}}
 	for c, col := range s.Cols {
@@ -123,16 +127,20 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 		}
 		colsXfIdList[c] = XfId
 
+		var customWidth int
 		if col.Width == 0 {
 			col.Width = ColWidth
+		} else {
+			customWidth = 1
 		}
 		worksheet.Cols.Col = append(worksheet.Cols.Col,
 			xlsxCol{Min: col.Min,
-				Max:       col.Max,
-				Hidden:    col.Hidden,
-				Width:     col.Width,
-				Collapsed: col.Collapsed,
-				Style:     XfId,
+				Max:         col.Max,
+				Hidden:      col.Hidden,
+				Width:       col.Width,
+				CustomWidth: customWidth,
+				Collapsed:   col.Collapsed,
+				Style:       XfId,
 			})
 	}
 
@@ -233,7 +241,7 @@ func handleStyleForXLSX(style *Style, NumFmtId int, styles *xlsxStyleSheet) (XfI
 
 	// HACK - adding light grey fill, as in OO and Google
 	greyfill := xlsxFill{}
-	greyfill.PatternFill.PatternType = "lightGrey"
+	greyfill.PatternFill.PatternType = "lightGray"
 	styles.addFill(greyfill)
 
 	borderId := styles.addBorder(xBorder)

File diff suppressed because it is too large
+ 28 - 1
sheet_test.go


+ 40 - 15
style.go

@@ -18,9 +18,9 @@ type Style struct {
 // Return a new Style structure initialised with the default values.
 func NewStyle() *Style {
 	return &Style{
-		Font:   *DefaulFont(),
-		Border: *DefaulBorder(),
-		Fill:   *DefaulFill(),
+		Font:   *DefaultFont(),
+		Border: *DefaultBorder(),
+		Fill:   *DefaultFill(),
 	}
 }
 
@@ -56,10 +56,22 @@ func (style *Style) makeXLSXStyleElements() (xFont xlsxFont, xFill xlsxFill, xBo
 	xPatternFill.FgColor.RGB = style.Fill.FgColor
 	xPatternFill.BgColor.RGB = style.Fill.BgColor
 	xFill.PatternFill = xPatternFill
-	xBorder.Left = xlsxLine{Style: style.Border.Left}
-	xBorder.Right = xlsxLine{Style: style.Border.Right}
-	xBorder.Top = xlsxLine{Style: style.Border.Top}
-	xBorder.Bottom = xlsxLine{Style: style.Border.Bottom}
+	xBorder.Left = xlsxLine{
+		Style: style.Border.Left,
+		Color: xlsxColor{RGB: style.Border.LeftColor},
+	}
+	xBorder.Right = xlsxLine{
+		Style: style.Border.Right,
+		Color: xlsxColor{RGB: style.Border.RightColor},
+	}
+	xBorder.Top = xlsxLine{
+		Style: style.Border.Top,
+		Color: xlsxColor{RGB: style.Border.TopColor},
+	}
+	xBorder.Bottom = xlsxLine{
+		Style: style.Border.Bottom,
+		Color: xlsxColor{RGB: style.Border.BottomColor},
+	}
 	xCellXf = makeXLSXCellElement()
 	xCellXf.ApplyBorder = style.ApplyBorder
 	xCellXf.ApplyFill = style.ApplyFill
@@ -83,14 +95,27 @@ func makeXLSXCellElement() (xCellXf xlsxXf) {
 // Border is a high level structure intended to provide user access to
 // the contents of Border Style within an Sheet.
 type Border struct {
-	Left   string
-	Right  string
-	Top    string
-	Bottom string
+	Left        string
+	LeftColor   string
+	Right       string
+	RightColor  string
+	Top         string
+	TopColor    string
+	Bottom      string
+	BottomColor string
 }
 
 func NewBorder(left, right, top, bottom string) *Border {
-	return &Border{Left: left, Right: right, Top: top, Bottom: bottom}
+	return &Border{
+		Left:        left,
+		LeftColor:   "",
+		Right:       right,
+		RightColor:  "",
+		Top:         top,
+		TopColor:    "",
+		Bottom:      bottom,
+		BottomColor: "",
+	}
 }
 
 // Fill is a high level structure intended to provide user access to
@@ -138,15 +163,15 @@ func SetDefaultFont(size int, name string) {
 	defaultFontName = name
 }
 
-func DefaulFont() *Font {
+func DefaultFont() *Font {
 	return NewFont(defaultFontSize, defaultFontName)
 }
 
-func DefaulFill() *Fill {
+func DefaultFill() *Fill {
 	return NewFill("none", "FFFFFFFF", "00000000")
 
 }
 
-func DefaulBorder() *Border {
+func DefaultBorder() *Border {
 	return NewBorder("none", "none", "none", "none")
 }

+ 4 - 4
style_test.go

@@ -13,11 +13,11 @@ func (s *StyleSuite) TestNewStyle(c *C) {
 	c.Assert(style, NotNil)
 }
 
-func (s *StyleSuite) TestNewStyleDefaults(c *C) {
+func (s *StyleSuite) TestNewStyleDefaultts(c *C) {
 	style := NewStyle()
-	c.Assert(style.Font, Equals, *DefaulFont())
-	c.Assert(style.Fill, Equals, *DefaulFill())
-	c.Assert(style.Border, Equals, *DefaulBorder())
+	c.Assert(style.Font, Equals, *DefaultFont())
+	c.Assert(style.Fill, Equals, *DefaultFill())
+	c.Assert(style.Border, Equals, *DefaultBorder())
 }
 
 func (s *StyleSuite) TestMakeXLSXStyleElements(c *C) {

BIN
testdocs/badfile_noWorkbookRels.xlsx


BIN
testdocs/badfile_noWorksheets.xlsx


+ 31 - 10
xmlStyle.go

@@ -59,10 +59,10 @@ var builtInNumFmt = map[int]string{
 
 const (
 	builtInNumFmtIndex_GENERAL = int(0)
-	builtInNumFmtIndex_INT = int(1)
-	builtInNumFmtIndex_FLOAT = int(2)
-	builtInNumFmtIndex_DATE = int(14)
-	builtInNumFmtIndex_STRING = int(49)
+	builtInNumFmtIndex_INT     = int(1)
+	builtInNumFmtIndex_FLOAT   = int(2)
+	builtInNumFmtIndex_DATE    = int(14)
+	builtInNumFmtIndex_STRING  = int(49)
 )
 
 // xlsxStyle directly maps the styleSheet element in the namespace
@@ -137,9 +137,13 @@ func (styles *xlsxStyleSheet) getStyle(styleIndex int) (style *Style) {
 			var border xlsxBorder
 			border = styles.Borders.Border[xf.BorderId]
 			style.Border.Left = border.Left.Style
+			style.Border.LeftColor = border.Left.Color.RGB
 			style.Border.Right = border.Right.Style
+			style.Border.RightColor = border.Right.Color.RGB
 			style.Border.Top = border.Top.Style
+			style.Border.TopColor = border.Top.Color.RGB
 			style.Border.Bottom = border.Bottom.Style
+			style.Border.BottomColor = border.Bottom.Color.RGB
 		}
 
 		if xf.FillId > -1 && xf.FillId < styles.Fills.Count {
@@ -698,19 +702,35 @@ func (border *xlsxBorder) Marshal() (result string, err error) {
 	subparts := ""
 	if border.Left.Style != "" {
 		emit = true
-		subparts += fmt.Sprintf(`<left style="%s"/>`, border.Left.Style)
+		subparts += fmt.Sprintf(`<left style="%s">`, border.Left.Style)
+		if border.Left.Color.RGB != "" {
+			subparts += fmt.Sprintf(`<color rgb="%s"/>`, border.Left.Color.RGB)
+		}
+		subparts += `</left>`
 	}
 	if border.Right.Style != "" {
 		emit = true
-		subparts += fmt.Sprintf(`<right style="%s"/>`, border.Right.Style)
+		subparts += fmt.Sprintf(`<right style="%s">`, border.Right.Style)
+		if border.Right.Color.RGB != "" {
+			subparts += fmt.Sprintf(`<color rgb="%s"/>`, border.Right.Color.RGB)
+		}
+		subparts += `</right>`
 	}
 	if border.Top.Style != "" {
 		emit = true
-		subparts += fmt.Sprintf(`<top style="%s"/>`, border.Top.Style)
+		subparts += fmt.Sprintf(`<top style="%s">`, border.Top.Style)
+		if border.Top.Color.RGB != "" {
+			subparts += fmt.Sprintf(`<color rgb="%s"/>`, border.Top.Color.RGB)
+		}
+		subparts += `</top>`
 	}
 	if border.Bottom.Style != "" {
 		emit = true
-		subparts += fmt.Sprintf(`<bottom style="%s"/>`, border.Bottom.Style)
+		subparts += fmt.Sprintf(`<bottom style="%s">`, border.Bottom.Style)
+		if border.Bottom.Color.RGB != "" {
+			subparts += fmt.Sprintf(`<color rgb="%s"/>`, border.Bottom.Color.RGB)
+		}
+		subparts += `</bottom>`
 	}
 	if emit {
 		result += `<border>`
@@ -725,11 +745,12 @@ func (border *xlsxBorder) Marshal() (result string, err error) {
 // currently I have not checked it for completeness - it does as much
 // as I need.
 type xlsxLine struct {
-	Style string `xml:"style,attr,omitempty"`
+	Style string    `xml:"style,attr,omitempty"`
+	Color xlsxColor `xml:"color,omitempty"`
 }
 
 func (line *xlsxLine) Equals(other xlsxLine) bool {
-	return line.Style == other.Style
+	return line.Style == other.Style && line.Color.Equals(other.Color)
 }
 
 // xlsxCellStyleXfs directly maps the cellStyleXfs element in the

+ 1 - 1
xmlStyle_test.go

@@ -71,7 +71,7 @@ func (x *XMLStyleSuite) TestMarshalXlsxStyleSheetWithABorder(c *C) {
 	styles.Borders.Border[0] = border
 
 	expected := `<?xml version="1.0" encoding="UTF-8"?>
-<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><borders count="1"><border><left style="solid"/><top style="none"/></border></borders></styleSheet>`
+<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><borders count="1"><border><left style="solid"></left><top style="none"></top></border></borders></styleSheet>`
 	result, err := styles.Marshal()
 	c.Assert(err, IsNil)
 	c.Assert(string(result), Equals, expected)

+ 7 - 6
xmlWorksheet.go

@@ -192,12 +192,13 @@ type xlsxCols struct {
 // currently I have not checked it for completeness - it does as much
 // as I need.
 type xlsxCol struct {
-	Collapsed bool    `xml:"collapsed,attr"`
-	Hidden    bool    `xml:"hidden,attr"`
-	Max       int     `xml:"max,attr"`
-	Min       int     `xml:"min,attr"`
-	Style     int     `xml:"style,attr"`
-	Width     float64 `xml:"width,attr"`
+	Collapsed   bool    `xml:"collapsed,attr"`
+	Hidden      bool    `xml:"hidden,attr"`
+	Max         int     `xml:"max,attr"`
+	Min         int     `xml:"min,attr"`
+	Style       int     `xml:"style,attr"`
+	Width       float64 `xml:"width,attr"`
+	CustomWidth int     `xml:"customWidth,attr,omitempty"`
 }
 
 // xlsxDimension directly maps the dimension element in the namespace

Some files were not shown because too many files changed in this diff