Browse Source

Cell type and style support

This version breaks no previous tests and will not break any code using the original version of the library.
It only Extends the API with support for different styles and cell data types.
DamianSzkuat 6 years ago
parent
commit
f91e520126
8 changed files with 664 additions and 3 deletions
  1. 16 0
      row.go
  2. 41 0
      stream_cell.go
  3. 160 3
      stream_file.go
  4. 125 0
      stream_file_builder.go
  5. 115 0
      stream_style.go
  6. 169 0
      stream_style_test.go
  7. 23 0
      style.go
  8. 15 0
      write.go

+ 16 - 0
row.go

@@ -1,5 +1,7 @@
 package xlsx
 
+import "strconv"
+
 type Row struct {
 	Cells        []*Cell
 	Hidden       bool
@@ -25,3 +27,17 @@ func (r *Row) AddCell() *Cell {
 	r.Sheet.maybeAddCol(len(r.Cells))
 	return cell
 }
+
+// AddStreamCell takes as input a StreamCell, creates a new Cell from it,
+// and appends the new cell to the row.
+func (r *Row) AddStreamCell(streamCell StreamCell) {
+	cell := NewCell(r)
+	cell.Value = streamCell.cellData
+	cell.style = streamCell.cellStyle.style
+	cell.NumFmt = strconv.Itoa(streamCell.cellStyle.xNumFmtId)
+	cell.cellType = streamCell.cellType
+	r.Cells = append(r.Cells, cell)
+	r.Sheet.maybeAddCol(len(r.Cells))
+}
+
+

+ 41 - 0
stream_cell.go

@@ -0,0 +1,41 @@
+package xlsx
+
+import "strconv"
+
+// StreamCell holds the data, style and type of cell for streaming
+type StreamCell struct {
+	cellData  string
+	cellStyle StreamStyle
+	cellType  CellType
+}
+
+// NewStreamCell creates a new StreamCell
+func NewStreamCell(cellData string, cellStyle StreamStyle, cellType CellType) StreamCell{
+	return StreamCell{
+		cellData:  cellData,
+		cellStyle: cellStyle,
+		cellType:  cellType,
+	}
+}
+
+// MakeStringStreamCell creates a new cell that holds string data, is of type string and uses general formatting
+func MakeStringStreamCell(cellData string) StreamCell{
+	return NewStreamCell(cellData, Strings, CellTypeString)
+}
+
+// MakeStyledStringStreamCell creates a new cell that holds a string and is styled according to the given style
+func MakeStyledStringStreamCell(cellData string, cellStyle StreamStyle) StreamCell {
+	return NewStreamCell(cellData, cellStyle, CellTypeString)
+}
+
+// MakeIntegerStreamCell creates a new cell that holds an integer value (represented as string),
+// is formatted as a standard integer and is of type numeric.
+func MakeIntegerStreamCell(cellData int) StreamCell {
+	return NewStreamCell(strconv.Itoa(cellData), Integers, CellTypeNumeric)
+}
+
+// MakeStyledIntegerStreamCell created a new cell that holds an integer value (represented as string)
+// and is styled according to the given style.
+func MakeStyledIntegerStreamCell(cellData int, cellStyle StreamStyle) StreamCell {
+	return NewStreamCell(strconv.Itoa(cellData), cellStyle, CellTypeNumeric)
+}

+ 160 - 3
stream_file.go

@@ -15,6 +15,7 @@ type StreamFile struct {
 	zipWriter      *zip.Writer
 	currentSheet   *streamSheet
 	styleIds       [][]int
+	styleIdMap	   map[StreamStyle]int
 	err            error
 }
 
@@ -31,9 +32,10 @@ type streamSheet struct {
 }
 
 var (
-	NoCurrentSheetError     = errors.New("no Current Sheet")
-	WrongNumberOfRowsError  = errors.New("invalid number of cells passed to Write. All calls to Write on the same sheet must have the same number of cells")
-	AlreadyOnLastSheetError = errors.New("NextSheet() called, but already on last sheet")
+	NoCurrentSheetError      = errors.New("no Current Sheet")
+	WrongNumberOfRowsError   = errors.New("invalid number of cells passed to Write. All calls to Write on the same sheet must have the same number of cells")
+	AlreadyOnLastSheetError  = errors.New("NextSheet() called, but already on last sheet")
+	UnsupportedCellTypeError = errors.New("the given cell type is not supported")
 )
 
 // Write will write a row of cells to the current sheet. Every call to Write on the same sheet must contain the
@@ -51,6 +53,22 @@ func (sf *StreamFile) Write(cells []string) error {
 	return sf.zipWriter.Flush()
 }
 
+// WriteWithStyle will write a row of cells to the current sheet. Every call to WriteWithStyle on the same sheet must
+// contain the same number of cells as the header provided when the sheet was created or an error will be returned.
+// This function will always trigger a flush on success. WriteWithStyle supports all data types and styles that
+// are supported by StreamCell.
+func (sf *StreamFile) WriteWithStyle(cells []StreamCell) error {
+	if sf.err != nil {
+		return sf.err
+	}
+	err := sf.writeWithStyle(cells)
+	if err != nil {
+		sf.err = err
+		return err
+	}
+	return sf.zipWriter.Flush()
+}
+
 func (sf *StreamFile) WriteAll(records [][]string) error {
 	if sf.err != nil {
 		return sf.err
@@ -65,6 +83,23 @@ func (sf *StreamFile) WriteAll(records [][]string) error {
 	return sf.zipWriter.Flush()
 }
 
+// WriteAllWithStyle will write all the rows provided in records. All rows must have the same number of cells as
+// the headers. This function will always trigger a flush on success. WriteWithStyle supports all data types and
+// styles that are supported by StreamCell.
+func (sf *StreamFile) WriteAllWithStyle(records [][]StreamCell) error{
+	if sf.err != nil {
+		return sf.err
+	}
+	for _, row := range records {
+		err := sf.writeWithStyle(row)
+		if err != nil {
+			sf.err = err
+			return err
+		}
+	}
+	return sf.zipWriter.Flush()
+}
+
 func (sf *StreamFile) write(cells []string) error {
 	if sf.currentSheet == nil {
 		return NoCurrentSheetError
@@ -112,6 +147,128 @@ func (sf *StreamFile) write(cells []string) error {
 	return sf.zipWriter.Flush()
 }
 
+func (sf *StreamFile) writeWithStyle(cells []StreamCell) error {
+	if sf.currentSheet == nil {
+		return NoCurrentSheetError
+	}
+	if len(cells) != sf.currentSheet.columnCount {
+		return WrongNumberOfRowsError
+	}
+	sf.currentSheet.rowCount++
+	// Write the row opening
+	if err := sf.currentSheet.write(`<row r="` + strconv.Itoa(sf.currentSheet.rowCount) + `">`); err != nil {
+		return err
+	}
+
+	// Add cells one by one
+	for colIndex, cell := range cells {
+		// Get the cell reference (location)
+		cellCoordinate := GetCellIDStringFromCoords(colIndex, sf.currentSheet.rowCount-1)
+
+		// Get the cell type string
+		cellType, err := getCellTypeAsString(cell.cellType)
+		if err != nil {
+			return err
+		}
+
+		// Build the XML cell opening
+		cellOpen := `<c r="` + cellCoordinate + `" t="` + cellType + `"`
+		// Add in the style id of the stream cell.
+		if idx, ok := sf.styleIdMap[cell.cellStyle]; ok {
+			cellOpen += ` s="` + strconv.Itoa(idx) + `"`
+		} else {
+			return errors.New("trying to make use of a style that has not been added")
+		}
+		cellOpen += `>`
+
+		// The XML cell contents
+		cellContentsOpen, cellContentsClose, err := getCellContentOpenAncCloseTags(cell.cellType)
+		if err != nil {
+			return err
+		}
+
+		// The XMl cell ending
+		cellClose := `</c>`
+
+		// Write the cell opening
+		if err := sf.currentSheet.write(cellOpen); err != nil {
+			return err
+		}
+
+		// Write the cell contents opening
+		if err := sf.currentSheet.write(cellContentsOpen); err != nil {
+			return err
+		}
+
+		// Write cell contents
+		if err:= xml.EscapeText(sf.currentSheet.writer, []byte(cell.cellData)); err != nil {
+			return err
+		}
+
+		// Write cell contents ending
+		if err := sf.currentSheet.write(cellContentsClose); err != nil {
+			return err
+		}
+
+		// Write the cell ending
+		if err := sf.currentSheet.write(cellClose); err != nil {
+			return err
+		}
+	}
+	// Write the row ending
+	if err := sf.currentSheet.write(`</row>`); err != nil {
+		return err
+	}
+	return sf.zipWriter.Flush()
+}
+
+func getCellTypeAsString(cellType CellType) (string, error) {
+	// documentation for the c.t (cell.Type) attribute:
+	// b (Boolean): Cell containing a boolean.
+	// d (Date): Cell contains a date in the ISO 8601 format.
+	// e (Error): Cell containing an error.
+	// inlineStr (Inline String): Cell containing an (inline) rich string, i.e., one not in the shared string table.
+	// If this cell type is used, then the cell value is in the is element rather than the v element in the cell (c element).
+	// n (Number): Cell containing a number.
+	// s (Shared String): Cell containing a shared string.
+	// str (String): Cell containing a formula string.
+	switch cellType{
+	case CellTypeBool:
+		return "b", nil
+	case CellTypeDate:
+		return "d", nil
+	case CellTypeError:
+		return "e", nil
+	case CellTypeInline:
+		return "inlineStr", nil
+	case CellTypeNumeric:
+		return "n", nil
+	case CellTypeString:
+		// TODO Currently shared strings are types as inline strings
+		return "inlineStr", nil
+		// return "s", nil
+	case CellTypeStringFormula:
+		return "str", nil
+	default:
+		return "", UnsupportedCellTypeError
+	}
+}
+
+func getCellContentOpenAncCloseTags(cellType CellType) (string, string, error) {
+	switch cellType{
+	case CellTypeString:
+		// TODO Currently shared strings are types as inline strings
+		return `<is><t>`, `</t></is>`, nil
+	case CellTypeInline:
+		return `<is><t>`, `</t></is>`, nil
+	case CellTypeStringFormula:
+		// Formulas are currently not supported
+		return ``, ``, UnsupportedCellTypeError
+	default:
+		return `<v>`, `</v>`, nil
+	}
+}
+
 // Error reports any error that has occurred during a previous Write or Flush.
 func (sf *StreamFile) Error() error {
 	return sf.err

+ 125 - 0
stream_file_builder.go

@@ -34,11 +34,15 @@ import (
 
 type StreamFileBuilder struct {
 	built              bool
+	firstSheetAdded    bool
+	customStylesAdded  bool
 	xlsxFile           *File
 	zipWriter          *zip.Writer
 	cellTypeToStyleIds map[CellType]int
 	maxStyleId         int
 	styleIds           [][]int
+	// streamStyles	   map[StreamStyle]struct{}
+	styleIdMap		   map[StreamStyle]int
 }
 
 const (
@@ -61,6 +65,8 @@ func NewStreamFileBuilder(writer io.Writer) *StreamFileBuilder {
 		xlsxFile:           NewFile(),
 		cellTypeToStyleIds: make(map[CellType]int),
 		maxStyleId:         initMaxStyleId,
+		// streamStyles: 		make(map[StreamStyle]struct{}),
+		styleIdMap:			make(map[StreamStyle]int),
 	}
 }
 
@@ -119,6 +125,48 @@ func (sb *StreamFileBuilder) AddSheet(name string, headers []string, cellTypes [
 	return nil
 }
 
+// AddSheetWithStyle will add sheets with the given name with the provided headers. The headers cannot be edited later, and all
+// rows written to the sheet must contain the same number of cells as the header. Sheet names must be unique, or an
+// error will be thrown. Additionally AddSheetWithStyle allows to add Style information to the headers.
+func (sb *StreamFileBuilder) AddSheetWithStyle(name string, cells []StreamCell) error {
+	if sb.built {
+		return BuiltStreamFileBuilderError
+	}
+	sheet, err := sb.xlsxFile.AddSheet(name)
+	if err != nil {
+		// Set built on error so that all subsequent calls to the builder will also fail.
+		sb.built = true
+		return err
+	}
+	// To make sure no new styles can be added after adding a sheet
+	sb.firstSheetAdded = true
+
+	// Check if all styles in the headers have been created
+	for _,cell := range cells{
+		if _, ok := sb.styleIdMap[cell.cellStyle]; !ok {
+			return errors.New("trying to make use of a style that has not been added")
+		}
+	}
+	// TODO Is needed for stream file to work but is not needed for streaming with styles
+	sb.styleIds = append(sb.styleIds, []int{})
+
+	// Set the values of the first row and the the number of columns
+	row := sheet.AddRow()
+	if count := row.WriteCellSlice(cells, -1); count != len(cells) {
+		// Set built on error so that all subsequent calls to the builder will also fail.
+		sb.built = true
+		return errors.New("failed to write headers")
+	}
+
+	// Set default column types based on the cel types in the first row
+	for i, cell := range cells {
+		sheet.Cols[i].SetType(cell.cellType)
+		// TODO Is needed for stream file to work but is not needed for streaming with styles
+		// sb.styleIds[len(sb.styleIds)-1] = append(sb.styleIds[len(sb.styleIds)-1], cellStyleIndex)
+	}
+	return nil
+}
+
 // Build begins streaming the XLSX file to the io, by writing all the XLSX metadata. It creates a StreamFile struct
 // that can be used to write the rows to the sheets.
 func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
@@ -126,16 +174,44 @@ func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
 		return nil, BuiltStreamFileBuilderError
 	}
 	sb.built = true
+
+	// Marshall Parts resets the style sheet, so to keep style information that has been added by the user
+	// we have to marshal it beforehand and add it again after the entire file has been marshaled
+	var xmlStylesSheetString string
+	var err error
+	if sb.customStylesAdded{
+		xmlStylesSheetString, err = sb.marshalStyles()
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	parts, err := sb.xlsxFile.MarshallParts()
 	if err != nil {
 		return nil, err
 	}
+
+	if sb.customStylesAdded{
+		parts["xl/styles.xml"] = xmlStylesSheetString
+	}
+
+	//styleIdMap := make(map[StreamStyle]int)
+	//if len(sb.streamStyles) > 0 {
+	//	// Add the created styles to the XLSX file and marshal the new style sheet
+	//	styleIdMap = sb.addStylesToXlsxFile()
+	//	parts["xl/styles.xml"], err = sb.marshalStyles()
+	//	if err != nil {
+	//		return nil, err
+	//	}
+	//}
+
 	es := &StreamFile{
 		zipWriter:      sb.zipWriter,
 		xlsxFile:       sb.xlsxFile,
 		sheetXmlPrefix: make([]string, len(sb.xlsxFile.Sheets)),
 		sheetXmlSuffix: make([]string, len(sb.xlsxFile.Sheets)),
 		styleIds:       sb.styleIds,
+		styleIdMap:     sb.styleIdMap,
 	}
 	for path, data := range parts {
 		// If the part is a sheet, don't write it yet. We only want to write the XLSX metadata files, since at this
@@ -162,6 +238,55 @@ func (sb *StreamFileBuilder) Build() (*StreamFile, error) {
 	return es, nil
 }
 
+//func (sb *StreamFileBuilder) addStylesToXlsxFile() map[StreamStyle]int {
+//	styleIdMap := make(map[StreamStyle]int)
+//	// TODO test
+//	sb.xlsxFile.styles = newXlsxStyleSheet(sb.xlsxFile.theme)
+//	// TODO test
+//	for streamStyle,_ := range sb.streamStyles{
+//		XfId := handleStyleForXLSX(streamStyle.style, streamStyle.xNumFmtId, sb.xlsxFile.styles)
+//		styleIdMap[streamStyle] = XfId
+//	}
+//	return styleIdMap
+//}
+
+func (sb *StreamFileBuilder) marshalStyles() (string, error) {
+	styleSheetXMLString, err := sb.xlsxFile.styles.Marshal()
+	if err!=nil {
+		return "", err
+	}
+	return styleSheetXMLString, nil
+}
+
+// AddStreamStyle adds a new style to the style sheet.
+// Only Styles that have been added through this function will be usable.
+// This function cannot be used after AddSheetWithStyle has been called, and if it is
+// called after AddSheetWithStyle it will return an error.
+//func (sb *StreamFileBuilder) AddStreamStyle(streamStyle StreamStyle) error {
+//	if sb.firstSheetAdded {
+//		return errors.New("at least one sheet has been added, cannot add new styles anymore")
+//	}
+//	sb.streamStyles[streamStyle] = struct{}{}
+//	return nil
+//}
+
+// AddStreamStyle adds a new style to the style sheet.
+// Only Styles that have been added through this function will be usable.
+// This function cannot be used after AddSheetWithStyle has been called, and if it is
+// called after AddSheetWithStyle it will return an error.
+func (sb *StreamFileBuilder) AddStreamStyle(streamStyle StreamStyle) error {
+	if sb.firstSheetAdded {
+		return errors.New("the style file has been built, cannot add new styles anymore")
+	}
+	if sb.xlsxFile.styles == nil {
+		sb.xlsxFile.styles = newXlsxStyleSheet(sb.xlsxFile.theme)
+	}
+	XfId := handleStyleForXLSX(streamStyle.style, streamStyle.xNumFmtId, sb.xlsxFile.styles)
+	sb.styleIdMap[streamStyle] = XfId
+	sb.customStylesAdded = true
+	return nil
+}
+
 // processEmptySheetXML will take in the path and XML data of an empty sheet, and will save the beginning and end of the
 // XML file so that these can be written at the right time.
 func (sb *StreamFileBuilder) processEmptySheetXML(sf *StreamFile, path, data string) error {

+ 115 - 0
stream_style.go

@@ -0,0 +1,115 @@
+package xlsx
+
+// StreamStyle has style and formatting information.
+// Used to store a style for streaming
+type StreamStyle struct {
+	xNumFmtId	int
+	style 		*Style
+}
+
+const (
+	GeneralFormat				= 0
+	IntegerFormat				= 1
+	DecimalFormat 				= 2
+	DateFormat_dd_mm_yy 		= 14
+	DateTimeFormat_d_m_yy_h_mm 	= 22
+)
+
+var Strings StreamStyle
+var BoldStrings StreamStyle
+var ItalicStrings StreamStyle
+var UnderlinedStrings StreamStyle
+
+var Integers StreamStyle
+var BoldIntegers StreamStyle
+var ItalicIntegers StreamStyle
+var UnderlinedIntegers StreamStyle
+
+// var DefaultStyles []StreamStyle
+
+
+var Bold *Font
+var Italic *Font
+var Underlined *Font
+
+var GreenCell *Fill
+var RedCell *Fill
+var WhiteCel *Fill
+
+func init(){
+	// Init Fonts
+	Bold = DefaultFont()
+	Bold.Bold = true
+
+	Italic = DefaultFont()
+	Italic.Italic = true
+
+	Underlined = DefaultFont()
+	Underlined.Underline = true
+
+	// Init Fills
+	GreenCell = NewFill(Solid_Cell_Fill, RGB_Light_Green, RGB_White)
+	RedCell = NewFill(Solid_Cell_Fill, RGB_Light_Red, RGB_White)
+	WhiteCel = DefaultFill()
+
+	// Init default string styles
+	Strings = MakeStringStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+	BoldStrings = MakeStringStyle(Bold, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	ItalicStrings = MakeStringStyle(Italic, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	UnderlinedStrings = MakeStringStyle(Underlined, DefaultFill(), DefaultAlignment(), DefaultBorder())
+
+	// Init default Integer styles
+	Integers = MakeIntegerStyle(DefaultFont(), DefaultFill(), DefaultAlignment(), DefaultBorder())
+	BoldIntegers = MakeIntegerStyle(Bold, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	ItalicIntegers = MakeIntegerStyle(Italic, DefaultFill(), DefaultAlignment(), DefaultBorder())
+	UnderlinedIntegers = MakeIntegerStyle(Underlined, DefaultFill(), DefaultAlignment(), DefaultBorder())
+}
+
+// MakeStyle creates a new StreamStyle and add it to the styles that will be streamed
+// This function returns a reference to the created StreamStyle
+func MakeStyle(formatStyleId int, font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle {
+	newStyle := NewStyle()
+
+	newStyle.Font = *font
+	newStyle.Fill = *fill
+	newStyle.Alignment = *alignment
+	newStyle.Border = *border
+
+	newStyle.ApplyFont = true
+	newStyle.ApplyFill = true
+	newStyle.ApplyAlignment = true
+	newStyle.ApplyBorder = true
+
+	newStreamStyle := StreamStyle{
+		xNumFmtId: 	formatStyleId,
+		style: 		newStyle,
+	}
+
+	// DefaultStyles = append(DefaultStyles, newStreamStyle)
+	return newStreamStyle
+}
+
+// MakeStringStyle creates a new style that can be used on cells with string data.
+// If used on other data the formatting might be wrong.
+func MakeStringStyle(font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle{
+	return MakeStyle(GeneralFormat, font, fill, alignment, border)
+}
+
+// MakeIntegerStyle creates a new style that can be used on cells with integer data.
+// If used on other data the formatting might be wrong.
+func MakeIntegerStyle(font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle{
+	return MakeStyle(IntegerFormat, font, fill, alignment, border)
+}
+
+// MakeDecimalStyle creates a new style that can be used on cells with decimal numeric data.
+// If used on other data the formatting might be wrong.
+func MakeDecimalStyle(font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle{
+	return MakeStyle(DecimalFormat, font, fill, alignment, border)
+}
+
+// MakeDateStyle creates a new style that can be used on cells with Date data.
+// The formatting used is: dd_mm_yy
+// If used on other data the formatting might be wrong.
+func MakeDateStyle(font *Font, fill *Fill, alignment *Alignment, border *Border) StreamStyle{
+	return MakeStyle(DateFormat_dd_mm_yy, font, fill, alignment, border)
+}

+ 169 - 0
stream_style_test.go

@@ -0,0 +1,169 @@
+package xlsx
+
+import (
+	"bytes"
+	"fmt"
+	. "gopkg.in/check.v1"
+	"io"
+	"reflect"
+)
+
+const (
+	StreamTestsShouldMakeRealFiles = false
+)
+
+type StreamStyleSuite struct{}
+
+var _ = Suite(&StreamStyleSuite{})
+
+func (s *StreamSuite) TestStreamTestsShouldMakeRealFilesShouldBeFalse(t *C) {
+	if StreamTestsShouldMakeRealFiles {
+		t.Fatal("TestsShouldMakeRealFiles should only be true for local debugging. Don't forget to switch back before commiting.")
+	}
+}
+
+func (s *StreamSuite) TestXlsxStreamWriteWithStyle(t *C) {
+	// When shouldMakeRealFiles is set to true this test will make actual XLSX files in the file system.
+	// This is useful to ensure files open in Excel, Numbers, Google Docs, etc.
+	// In case of issues you can use "Open XML SDK 2.5" to diagnose issues in generated XLSX files:
+	// https://www.microsoft.com/en-us/download/details.aspx?id=30425
+	testCases := []struct {
+		testName      string
+		sheetNames    []string
+		workbookData  [][][]StreamCell
+		expectedError error
+	}{
+		{
+			testName: "Number Row",
+			sheetNames: []string{
+				"Sheet1",
+			},
+			workbookData: [][][]StreamCell{
+				{
+					{MakeStringStreamCell("1"), MakeStringStreamCell("25"),
+						MakeStringStreamCell("A"), MakeStringStreamCell("B")},
+					{MakeIntegerStreamCell(1234), MakeStyledIntegerStreamCell(98, BoldIntegers),
+						MakeStyledIntegerStreamCell(34, ItalicIntegers), MakeStyledIntegerStreamCell(26, UnderlinedIntegers)},
+				},
+			},
+		},
+	}
+
+	for i, testCase := range testCases {
+		var filePath string
+		var buffer bytes.Buffer
+		if StreamTestsShouldMakeRealFiles {
+			filePath = fmt.Sprintf("WorkbookWithStyle%d.xlsx", i)
+		}
+
+		err := writeStreamFileWithStyle(filePath, &buffer, testCase.sheetNames, testCase.workbookData, StreamTestsShouldMakeRealFiles)
+		if err != testCase.expectedError && err.Error() != testCase.expectedError.Error() {
+			t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
+		}
+		if testCase.expectedError != nil {
+			return
+		}
+		// read the file back with the xlsx package
+		var bufReader *bytes.Reader
+		var size int64
+		if !StreamTestsShouldMakeRealFiles {
+			bufReader = bytes.NewReader(buffer.Bytes())
+			size = bufReader.Size()
+		}
+		actualSheetNames, actualWorkbookData := readXLSXFile(t, filePath, bufReader, size, StreamTestsShouldMakeRealFiles)
+		// check if data was able to be read correctly
+		if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
+			t.Fatal("Expected sheet names to be equal")
+		}
+
+		expectedWorkbookDataStrings := [][][]string{}
+		for j,_ := range testCase.workbookData {
+			expectedWorkbookDataStrings = append(expectedWorkbookDataStrings, [][]string{})
+			for k,_ := range testCase.workbookData[j]{
+				expectedWorkbookDataStrings[j] = append(expectedWorkbookDataStrings[j], []string{})
+				for _, cell := range testCase.workbookData[j][k] {
+					expectedWorkbookDataStrings[j][k] = append(expectedWorkbookDataStrings[j][k], cell.cellData)
+				}
+			}
+
+		}
+		if !reflect.DeepEqual(actualWorkbookData, expectedWorkbookDataStrings) {
+			t.Fatal("Expected workbook data to be equal")
+		}
+	}
+
+}
+
+// writeStreamFile will write the file using this stream package
+func writeStreamFileWithStyle(filePath string, fileBuffer io.Writer, sheetNames []string, workbookData [][][]StreamCell, shouldMakeRealFiles bool) error {
+	var file *StreamFileBuilder
+	var err error
+	if shouldMakeRealFiles {
+		file, err = NewStreamFileBuilderForPath(filePath)
+		if err != nil {
+			return err
+		}
+	} else {
+		file = NewStreamFileBuilder(fileBuffer)
+	}
+
+	err = file.AddStreamStyle(Strings)
+	err = file.AddStreamStyle(BoldStrings)
+	err = file.AddStreamStyle(ItalicStrings)
+	err = file.AddStreamStyle(UnderlinedStrings)
+	err = file.AddStreamStyle(Integers)
+	err = file.AddStreamStyle(BoldIntegers)
+	err = file.AddStreamStyle(ItalicIntegers)
+	err = file.AddStreamStyle(UnderlinedIntegers)
+	if err != nil { // TODO handle all errors not just one
+		return err
+	}
+
+	for i, sheetName := range sheetNames {
+		header := workbookData[i][0]
+		err := file.AddSheetWithStyle(sheetName, header)
+		if err != nil {
+			return err
+		}
+	}
+	streamFile, err := file.Build()
+	if err != nil {
+		return err
+	}
+	for i, sheetData := range workbookData {
+
+		if i != 0 {
+			err = streamFile.NextSheet()
+			if err != nil {
+				return err
+			}
+		}
+		for i, row := range sheetData {
+			if i == 0 {
+				continue
+			}
+			err = streamFile.WriteWithStyle(row)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	err = streamFile.Close()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 23 - 0
style.go

@@ -2,6 +2,29 @@ package xlsx
 
 import "strconv"
 
+// Several popular font names that can be used to create fonts
+const (
+	Helvetica     = "Helvetica"
+	Baskerville   = "Baskerville Old Face"
+	TimesNewRoman = "Times New Roman"
+	Bodoni        = "Bodoni MT"
+	GillSans      = "Gill Sans MT"
+	Courier       = "Courier"
+)
+
+const (
+	RGB_Light_Green = "FFC6EFCE"
+	RGB_Dard_Green  = "FF006100"
+	RGB_Light_Red   = "FFFFC7CE"
+	RGB_Dark_Red    = "FF9C0006"
+	RGB_White 		= "00000000"
+	RGB_Black 		= "FFFFFFFF"
+)
+
+const (
+	Solid_Cell_Fill = "solid"
+)
+
 // Style is a high level structure intended to provide user access to
 // the contents of Style within an XLSX file.
 type Style struct {

+ 15 - 0
write.go

@@ -63,6 +63,21 @@ func (r *Row) WriteSlice(e interface{}, cols int) int {
 	return i
 }
 
+// WriteCellSlice an array of Cell's to row r. Accepts an array of StreamCells
+// and writes the number of columns to write, 'cols'. If 'cols' is < 0,
+// the entire array will be written if possible. Returns the number of columns written.
+func (r *Row) WriteCellSlice(streamCells []StreamCell, cols int) int {
+	if cols == 0 {
+		return cols
+	}
+	i := 0
+	for _, streamCell := range streamCells {
+		r.AddStreamCell(streamCell)
+		i++
+	}
+	return i
+}
+
 // Writes a struct to row r. Accepts a pointer to struct type 'e',
 // and the number of columns to write, `cols`. If 'cols' is < 0,
 // the entire struct will be written if possible. Returns -1 if the 'e'