2017-08-28 20:56:18 -05:00
|
|
|
// Copyright 2017 Baliance. All rights reserved.
|
|
|
|
//
|
|
|
|
// Use of this source code is governed by the terms of the Affero GNU General
|
|
|
|
// Public License version 3.0 as published by the Free Software Foundation and
|
|
|
|
// appearing in the file LICENSE included in the packaging of this file. A
|
|
|
|
// commercial license can be purchased by contacting sales@baliance.com.
|
|
|
|
|
|
|
|
package spreadsheet
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2017-09-02 16:17:41 -05:00
|
|
|
"image"
|
|
|
|
"image/jpeg"
|
2017-08-28 20:56:18 -05:00
|
|
|
"io"
|
|
|
|
"os"
|
2017-09-05 16:37:53 -04:00
|
|
|
"time"
|
2017-08-28 20:56:18 -05:00
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
"baliance.com/gooxml"
|
2017-08-28 20:56:18 -05:00
|
|
|
"baliance.com/gooxml/common"
|
|
|
|
"baliance.com/gooxml/zippkg"
|
2017-09-02 16:33:40 -05:00
|
|
|
|
|
|
|
dml "baliance.com/gooxml/schema/schemas.openxmlformats.org/drawingml"
|
2017-09-03 08:17:57 -05:00
|
|
|
crt "baliance.com/gooxml/schema/schemas.openxmlformats.org/drawingml/2006/chart"
|
|
|
|
sd "baliance.com/gooxml/schema/schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
|
|
|
|
"baliance.com/gooxml/schema/schemas.openxmlformats.org/package/2006/relationships"
|
2017-09-02 16:33:40 -05:00
|
|
|
sml "baliance.com/gooxml/schema/schemas.openxmlformats.org/spreadsheetml"
|
2017-08-28 20:56:18 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// Workbook is the top level container item for a set of spreadsheets.
|
|
|
|
type Workbook struct {
|
|
|
|
common.DocBase
|
2017-09-02 16:33:40 -05:00
|
|
|
x *sml.Workbook
|
2017-08-28 20:56:18 -05:00
|
|
|
|
2017-09-02 15:27:53 -05:00
|
|
|
StyleSheet StyleSheet
|
2017-08-28 20:56:18 -05:00
|
|
|
SharedStrings SharedStrings
|
2017-09-02 16:33:40 -05:00
|
|
|
|
2017-09-03 08:17:57 -05:00
|
|
|
xws []*sml.Worksheet
|
|
|
|
xwsRels []common.Relationships
|
|
|
|
wbRels common.Relationships
|
|
|
|
themes []*dml.Theme
|
|
|
|
drawings []*sd.WsDr
|
|
|
|
drawingRels []common.Relationships
|
|
|
|
charts []*crt.ChartSpace
|
2017-08-28 20:56:18 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// X returns the inner wrapped XML type.
|
2017-09-02 16:33:40 -05:00
|
|
|
func (wb *Workbook) X() *sml.Workbook {
|
2017-08-28 20:56:18 -05:00
|
|
|
return wb.x
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddSheet adds a new sheet with a given name to a workbook.
|
2017-09-02 14:54:38 -05:00
|
|
|
func (wb *Workbook) AddSheet() Sheet {
|
2017-09-02 16:33:40 -05:00
|
|
|
rs := sml.NewCT_Sheet()
|
2017-09-02 14:54:38 -05:00
|
|
|
|
2017-08-28 20:56:18 -05:00
|
|
|
// Assign a unique sheet ID
|
|
|
|
rs.SheetIdAttr = 1
|
|
|
|
for _, s := range wb.x.Sheets.Sheet {
|
|
|
|
if rs.SheetIdAttr <= s.SheetIdAttr {
|
|
|
|
rs.SheetIdAttr = s.SheetIdAttr + 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
wb.x.Sheets.Sheet = append(wb.x.Sheets.Sheet, rs)
|
|
|
|
|
|
|
|
rs.NameAttr = fmt.Sprintf("Sheet %d", rs.SheetIdAttr)
|
|
|
|
|
|
|
|
// create the actual worksheet
|
2017-09-02 16:33:40 -05:00
|
|
|
ws := sml.NewWorksheet()
|
|
|
|
ws.Dimension = sml.NewCT_SheetDimension()
|
2017-08-28 20:56:18 -05:00
|
|
|
ws.Dimension.RefAttr = "A1"
|
|
|
|
wb.xws = append(wb.xws, ws)
|
2017-09-03 18:39:35 -05:00
|
|
|
wsRel := common.NewRelationships()
|
|
|
|
wb.xwsRels = append(wb.xwsRels, wsRel)
|
2017-09-02 16:33:40 -05:00
|
|
|
ws.SheetData = sml.NewCT_SheetData()
|
2017-08-28 20:56:18 -05:00
|
|
|
|
2017-09-03 12:01:55 -05:00
|
|
|
dt := gooxml.DocTypeSpreadsheet
|
2017-08-28 20:56:18 -05:00
|
|
|
// update the references
|
2017-09-03 12:01:55 -05:00
|
|
|
rid := wb.wbRels.AddAutoRelationship(dt, len(wb.x.Sheets.Sheet), gooxml.WorksheetType)
|
2017-08-28 20:56:18 -05:00
|
|
|
rs.IdAttr = rid.ID()
|
|
|
|
|
|
|
|
// add the content type
|
2017-09-03 12:01:55 -05:00
|
|
|
wb.ContentTypes.AddOverride(gooxml.AbsoluteFilename(dt, gooxml.WorksheetContentType, len(wb.x.Sheets.Sheet)),
|
|
|
|
gooxml.WorksheetContentType)
|
2017-08-28 20:56:18 -05:00
|
|
|
|
|
|
|
return Sheet{wb, rs, ws}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SaveToFile writes the workbook out to a file.
|
|
|
|
func (wb *Workbook) SaveToFile(path string) error {
|
|
|
|
f, err := os.Create(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
return wb.Save(f)
|
|
|
|
}
|
|
|
|
|
2017-09-05 16:37:53 -04:00
|
|
|
// Uses1904Dates returns true if the the workbook uses dates relative to
|
|
|
|
// 1 Jan 1904. This is uncommon.
|
|
|
|
func (wb *Workbook) Uses1904Dates() bool {
|
|
|
|
if wb.x.WorkbookPr == nil || wb.x.WorkbookPr.Date1904Attr == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return *wb.x.WorkbookPr.Date1904Attr
|
|
|
|
}
|
|
|
|
|
|
|
|
// Epoch returns the point at which the dates/times in the workbook are relative to.
|
|
|
|
func (wb *Workbook) Epoch() time.Time {
|
|
|
|
if wb.Uses1904Dates() {
|
|
|
|
time.Date(1904, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
}
|
|
|
|
return time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
|
|
|
|
}
|
|
|
|
|
2017-08-28 20:56:18 -05:00
|
|
|
// Save writes the workbook out to a writer in the zipped xlsx format.
|
|
|
|
func (wb *Workbook) Save(w io.Writer) error {
|
|
|
|
z := zip.NewWriter(w)
|
|
|
|
defer z.Close()
|
2017-09-03 12:01:55 -05:00
|
|
|
dt := gooxml.DocTypeSpreadsheet
|
|
|
|
|
2017-08-28 20:56:18 -05:00
|
|
|
if err := zippkg.MarshalXML(z, zippkg.BaseRelsFilename, wb.Rels.X()); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-09-03 12:01:55 -05:00
|
|
|
if err := zippkg.MarshalXMLByType(z, dt, gooxml.ExtendedPropertiesType, wb.AppProperties.X()); err != nil {
|
2017-08-28 20:56:18 -05:00
|
|
|
return err
|
|
|
|
}
|
2017-09-03 12:01:55 -05:00
|
|
|
if err := zippkg.MarshalXMLByType(z, dt, gooxml.CorePropertiesType, wb.CoreProperties.X()); err != nil {
|
2017-08-28 20:56:18 -05:00
|
|
|
return err
|
|
|
|
}
|
2017-09-03 12:01:55 -05:00
|
|
|
|
|
|
|
workbookFn := gooxml.AbsoluteFilename(dt, gooxml.OfficeDocumentType, 0)
|
|
|
|
if err := zippkg.MarshalXML(z, workbookFn, wb.x); err != nil {
|
2017-08-28 20:56:18 -05:00
|
|
|
return err
|
|
|
|
}
|
2017-09-03 12:01:55 -05:00
|
|
|
if err := zippkg.MarshalXML(z, zippkg.RelationsPathFor(workbookFn), wb.wbRels.X()); err != nil {
|
2017-08-28 20:56:18 -05:00
|
|
|
return err
|
|
|
|
}
|
2017-09-03 12:01:55 -05:00
|
|
|
|
|
|
|
if err := zippkg.MarshalXMLByType(z, dt, gooxml.StylesType, wb.StyleSheet.X()); err != nil {
|
2017-08-28 20:56:18 -05:00
|
|
|
return err
|
|
|
|
}
|
2017-09-03 12:01:55 -05:00
|
|
|
|
2017-09-02 16:33:40 -05:00
|
|
|
for i, thm := range wb.themes {
|
2017-09-03 12:01:55 -05:00
|
|
|
if err := zippkg.MarshalXMLByTypeIndex(z, dt, gooxml.ThemeType, i+1, thm); err != nil {
|
2017-09-02 16:33:40 -05:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2017-08-28 20:56:18 -05:00
|
|
|
for i, sheet := range wb.xws {
|
2017-09-03 12:01:55 -05:00
|
|
|
fn := gooxml.AbsoluteFilename(dt, gooxml.WorksheetType, i+1)
|
2017-09-02 16:55:21 -05:00
|
|
|
zippkg.MarshalXML(z, fn, sheet)
|
|
|
|
zippkg.MarshalXML(z, zippkg.RelationsPathFor(fn), wb.xwsRels[i].X())
|
2017-08-28 20:56:18 -05:00
|
|
|
}
|
2017-09-03 18:39:35 -05:00
|
|
|
if err := zippkg.MarshalXMLByType(z, dt, gooxml.SharedStingsType, wb.SharedStrings.X()); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-09-02 16:17:41 -05:00
|
|
|
if wb.Thumbnail != nil {
|
2017-09-03 12:01:55 -05:00
|
|
|
fn := gooxml.AbsoluteFilename(dt, gooxml.ThumbnailType, 0)
|
|
|
|
tn, err := z.Create(fn)
|
2017-09-02 16:17:41 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := jpeg.Encode(tn, wb.Thumbnail, nil); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2017-09-03 08:26:22 -05:00
|
|
|
for i, chart := range wb.charts {
|
2017-09-03 12:01:55 -05:00
|
|
|
fn := gooxml.AbsoluteFilename(dt, gooxml.ChartType, i+1)
|
2017-09-03 08:26:22 -05:00
|
|
|
zippkg.MarshalXML(z, fn, chart)
|
|
|
|
}
|
|
|
|
for i, drawing := range wb.drawings {
|
2017-09-03 12:01:55 -05:00
|
|
|
fn := gooxml.AbsoluteFilename(dt, gooxml.DrawingType, i+1)
|
2017-09-03 08:26:22 -05:00
|
|
|
zippkg.MarshalXML(z, fn, drawing)
|
2017-09-05 09:28:40 -05:00
|
|
|
if !wb.drawingRels[i].IsEmpty() {
|
|
|
|
zippkg.MarshalXML(z, zippkg.RelationsPathFor(fn), wb.drawingRels[i].X())
|
|
|
|
}
|
2017-09-03 08:26:22 -05:00
|
|
|
}
|
2017-09-05 09:28:40 -05:00
|
|
|
if err := zippkg.MarshalXML(z, zippkg.ContentTypesFilename, wb.ContentTypes.X()); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-09-02 16:55:21 -05:00
|
|
|
if err := wb.WriteExtraFiles(z); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-08-28 20:56:18 -05:00
|
|
|
return z.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate attempts to validate the structure of a workbook.
|
|
|
|
func (wb *Workbook) Validate() error {
|
|
|
|
if wb == nil || wb.x == nil {
|
|
|
|
return errors.New("workbook not initialized correctly, nil base")
|
|
|
|
}
|
|
|
|
|
|
|
|
maxID := uint32(0)
|
|
|
|
for _, s := range wb.x.Sheets.Sheet {
|
|
|
|
if s.SheetIdAttr > maxID {
|
|
|
|
maxID = s.SheetIdAttr
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if maxID != uint32(len(wb.xws)) {
|
|
|
|
return fmt.Errorf("found %d worksheet descriptions and %d worksheets", maxID, len(wb.xws))
|
|
|
|
}
|
|
|
|
|
2017-09-02 14:54:38 -05:00
|
|
|
// Excel doesn't like reused sheet names
|
|
|
|
usedNames := map[string]struct{}{}
|
2017-08-28 20:56:18 -05:00
|
|
|
for i, s := range wb.x.Sheets.Sheet {
|
|
|
|
sw := Sheet{wb, s, wb.xws[i]}
|
2017-09-02 14:54:38 -05:00
|
|
|
if _, ok := usedNames[sw.Name()]; ok {
|
2017-09-03 08:17:57 -05:00
|
|
|
return fmt.Errorf("workbook/Sheet[%d] has duplicate name '%s'", i, sw.Name())
|
2017-09-02 14:54:38 -05:00
|
|
|
}
|
|
|
|
usedNames[sw.Name()] = struct{}{}
|
2017-08-28 20:56:18 -05:00
|
|
|
if err := sw.ValidateWithPath(fmt.Sprintf("workbook/Sheet[%d]", i)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-09-06 14:53:48 -04:00
|
|
|
|
|
|
|
if err := sw.Validate(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-08-28 20:56:18 -05:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sheets returns the sheets from the workbook.
|
|
|
|
func (wb *Workbook) Sheets() []Sheet {
|
|
|
|
ret := []Sheet{}
|
|
|
|
for i, wks := range wb.xws {
|
|
|
|
r := wb.x.Sheets.Sheet[i]
|
|
|
|
ret = append(ret, Sheet{wb, r, wks})
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
// SheetCount returns the number of sheets in the workbook.
|
|
|
|
func (wb Workbook) SheetCount() int {
|
|
|
|
return len(wb.xws)
|
|
|
|
}
|
2017-09-03 08:17:57 -05:00
|
|
|
|
|
|
|
func (wb *Workbook) onNewRelationship(decMap *zippkg.DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship) error {
|
2017-09-03 12:01:55 -05:00
|
|
|
dt := gooxml.DocTypeSpreadsheet
|
2017-09-05 09:28:40 -05:00
|
|
|
// if we know of a better filename
|
|
|
|
if fn := gooxml.RelativeFilename(dt, typ, 0); fn != "" {
|
|
|
|
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
|
|
|
}
|
2017-09-03 12:01:55 -05:00
|
|
|
|
2017-09-03 08:17:57 -05:00
|
|
|
switch typ {
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.OfficeDocumentType:
|
2017-09-03 08:17:57 -05:00
|
|
|
wb.x = sml.NewWorkbook()
|
|
|
|
decMap.AddTarget(target, wb.x)
|
|
|
|
// look for the workbook relationships file as well
|
|
|
|
wb.wbRels = common.NewRelationships()
|
|
|
|
decMap.AddTarget(zippkg.RelationsPathFor(target), wb.wbRels.X())
|
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.CorePropertiesType:
|
2017-09-03 08:17:57 -05:00
|
|
|
decMap.AddTarget(target, wb.CoreProperties.X())
|
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.ExtendedPropertiesType:
|
2017-09-03 08:17:57 -05:00
|
|
|
decMap.AddTarget(target, wb.AppProperties.X())
|
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.WorksheetType:
|
2017-09-03 08:17:57 -05:00
|
|
|
ws := sml.NewWorksheet()
|
|
|
|
wb.xws = append(wb.xws, ws)
|
|
|
|
decMap.AddTarget(target, ws)
|
|
|
|
// look for worksheet rels
|
|
|
|
wksRel := common.NewRelationships()
|
|
|
|
decMap.AddTarget(zippkg.RelationsPathFor(target), wksRel.X())
|
|
|
|
wb.xwsRels = append(wb.xwsRels, wksRel)
|
|
|
|
// fix the relationship target so it points to where we'll save
|
|
|
|
// the worksheet
|
2017-09-03 12:01:55 -05:00
|
|
|
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(wb.xws))
|
2017-09-03 08:17:57 -05:00
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.StylesType:
|
2017-09-05 18:42:39 -04:00
|
|
|
wb.StyleSheet = NewStyleSheet(wb)
|
2017-09-03 08:17:57 -05:00
|
|
|
decMap.AddTarget(target, wb.StyleSheet.X())
|
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.ThemeType:
|
2017-09-03 08:17:57 -05:00
|
|
|
thm := dml.NewTheme()
|
|
|
|
wb.themes = append(wb.themes, thm)
|
|
|
|
decMap.AddTarget(target, thm)
|
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.SharedStingsType:
|
2017-09-03 08:17:57 -05:00
|
|
|
wb.SharedStrings = NewSharedStrings()
|
|
|
|
decMap.AddTarget(target, wb.SharedStrings.X())
|
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.ThumbnailType:
|
2017-09-03 08:17:57 -05:00
|
|
|
// read our thumbnail
|
|
|
|
for i, f := range files {
|
|
|
|
if f == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if f.Name == target {
|
|
|
|
rc, err := f.Open()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error reading thumbnail: %s", err)
|
|
|
|
}
|
|
|
|
wb.Thumbnail, _, err = image.Decode(rc)
|
|
|
|
rc.Close()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error decoding thumbnail: %s", err)
|
|
|
|
}
|
|
|
|
files[i] = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.DrawingType:
|
2017-09-03 08:17:57 -05:00
|
|
|
drawing := sd.NewWsDr()
|
|
|
|
decMap.AddTarget(target, drawing)
|
|
|
|
wb.drawings = append(wb.drawings, drawing)
|
|
|
|
|
|
|
|
drel := common.NewRelationships()
|
|
|
|
decMap.AddTarget(zippkg.RelationsPathFor(target), drel.X())
|
|
|
|
wb.drawingRels = append(wb.drawingRels, drel)
|
2017-09-03 12:01:55 -05:00
|
|
|
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(wb.drawings))
|
2017-09-03 08:17:57 -05:00
|
|
|
|
2017-09-03 11:05:27 -05:00
|
|
|
case gooxml.ChartType:
|
2017-09-03 08:17:57 -05:00
|
|
|
chart := crt.NewChartSpace()
|
|
|
|
decMap.AddTarget(target, chart)
|
|
|
|
wb.charts = append(wb.charts, chart)
|
2017-09-03 12:01:55 -05:00
|
|
|
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(wb.charts))
|
2017-09-03 08:17:57 -05:00
|
|
|
|
|
|
|
default:
|
|
|
|
fmt.Println("unsupported relationship", target, typ)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2017-09-03 16:47:46 -05:00
|
|
|
|
|
|
|
func (wb *Workbook) AddDrawing() Drawing {
|
|
|
|
drawing := sd.NewWsDr()
|
|
|
|
wb.drawings = append(wb.drawings, drawing)
|
|
|
|
fn := gooxml.AbsoluteFilename(gooxml.DocTypeSpreadsheet, gooxml.DrawingType, len(wb.drawings))
|
2017-09-03 18:39:35 -05:00
|
|
|
wb.ContentTypes.AddOverride(fn, gooxml.DrawingContentType)
|
2017-09-03 16:47:46 -05:00
|
|
|
wb.drawingRels = append(wb.drawingRels, common.NewRelationships())
|
2017-09-04 12:18:15 -05:00
|
|
|
d := Drawing{wb, drawing}
|
2017-09-04 12:56:57 -05:00
|
|
|
d.InitializeDefaults()
|
2017-09-04 12:18:15 -05:00
|
|
|
return d
|
2017-09-03 16:47:46 -05:00
|
|
|
}
|
2017-09-07 07:23:30 -04:00
|
|
|
|
|
|
|
// AddDefinedName adds a name for a cell or range reference that can be used in
|
|
|
|
// formulas and charts.
|
|
|
|
func (wb *Workbook) AddDefinedName(name, ref string) DefinedName {
|
|
|
|
if wb.x.DefinedNames == nil {
|
|
|
|
wb.x.DefinedNames = sml.NewCT_DefinedNames()
|
|
|
|
}
|
|
|
|
dn := sml.NewCT_DefinedName()
|
|
|
|
dn.Content = ref
|
|
|
|
dn.NameAttr = name
|
|
|
|
wb.x.DefinedNames.DefinedName = append(wb.x.DefinedNames.DefinedName, dn)
|
|
|
|
return DefinedName{dn}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DefinedNames returns a slice of all defined names in the workbook.
|
|
|
|
func (wb *Workbook) DefinedNames() []DefinedName {
|
|
|
|
if wb.x.DefinedNames == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
ret := []DefinedName{}
|
|
|
|
for _, dn := range wb.x.DefinedNames.DefinedName {
|
|
|
|
ret = append(ret, DefinedName{dn})
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|