mirror of
https://github.com/unidoc/unioffice.git
synced 2025-04-27 13:48:54 +08:00
811 lines
24 KiB
Go
811 lines
24 KiB
Go
// Copyright 2017 FoxyUtils ehf. 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 on https://unidoc.io.
|
|
|
|
package spreadsheet
|
|
|
|
import (
|
|
"archive/zip"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/unidoc/unioffice"
|
|
"github.com/unidoc/unioffice/color"
|
|
"github.com/unidoc/unioffice/common"
|
|
"github.com/unidoc/unioffice/common/license"
|
|
"github.com/unidoc/unioffice/common/tempstorage"
|
|
"github.com/unidoc/unioffice/vmldrawing"
|
|
"github.com/unidoc/unioffice/zippkg"
|
|
|
|
"github.com/unidoc/unioffice/schema/soo/dml"
|
|
crt "github.com/unidoc/unioffice/schema/soo/dml/chart"
|
|
sd "github.com/unidoc/unioffice/schema/soo/dml/spreadsheetDrawing"
|
|
"github.com/unidoc/unioffice/schema/soo/pkg/relationships"
|
|
"github.com/unidoc/unioffice/schema/soo/sml"
|
|
)
|
|
|
|
// ErrorNotFound is returned when something is not found
|
|
var ErrorNotFound = errors.New("not found")
|
|
|
|
// Workbook is the top level container item for a set of spreadsheets.
|
|
type Workbook struct {
|
|
common.DocBase
|
|
x *sml.Workbook
|
|
|
|
StyleSheet StyleSheet
|
|
SharedStrings SharedStrings
|
|
|
|
comments []*sml.Comments
|
|
xws []*sml.Worksheet
|
|
xwsRels []common.Relationships
|
|
wbRels common.Relationships
|
|
themes []*dml.Theme
|
|
drawings []*sd.WsDr
|
|
drawingRels []common.Relationships
|
|
vmlDrawings []*vmldrawing.Container
|
|
charts []*crt.ChartSpace
|
|
tables []*sml.Table
|
|
filename string
|
|
}
|
|
|
|
// X returns the inner wrapped XML type.
|
|
func (wb *Workbook) X() *sml.Workbook {
|
|
return wb.x
|
|
}
|
|
|
|
// AddSheet adds a new sheet to a workbook.
|
|
func (wb *Workbook) AddSheet() Sheet {
|
|
rs := sml.NewCT_Sheet()
|
|
|
|
// 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
|
|
ws := sml.NewWorksheet()
|
|
ws.Dimension = sml.NewCT_SheetDimension()
|
|
ws.Dimension.RefAttr = "A1"
|
|
wb.xws = append(wb.xws, ws)
|
|
wsRel := common.NewRelationships()
|
|
|
|
wb.xwsRels = append(wb.xwsRels, wsRel)
|
|
ws.SheetData = sml.NewCT_SheetData()
|
|
|
|
wb.comments = append(wb.comments, nil)
|
|
|
|
dt := unioffice.DocTypeSpreadsheet
|
|
|
|
// update the references
|
|
rid := wb.wbRels.AddAutoRelationship(dt, unioffice.OfficeDocumentType, len(wb.x.Sheets.Sheet), unioffice.WorksheetType)
|
|
rs.IdAttr = rid.ID()
|
|
|
|
// add the content type
|
|
wb.ContentTypes.AddOverride(unioffice.AbsoluteFilename(dt, unioffice.WorksheetContentType, len(wb.x.Sheets.Sheet)),
|
|
unioffice.WorksheetContentType)
|
|
|
|
return Sheet{wb, rs, ws}
|
|
}
|
|
|
|
var sstAbsTargetAttr = unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet, unioffice.SharedStringsType, 0)
|
|
var sstRelTargetAttr = unioffice.RelativeFilename(unioffice.DocTypeSpreadsheet, unioffice.OfficeDocumentType, unioffice.SharedStringsType, 0)
|
|
|
|
// ensureSharedStringsRelationships checks if relationships and content types related to shared strings are already exist and add them in the case of absence.
|
|
func (wb *Workbook) ensureSharedStringsRelationships() {
|
|
foundSharedStringContentType := false
|
|
for _, o := range wb.ContentTypes.X().Override {
|
|
if o.ContentTypeAttr == unioffice.SharedStringsContentType {
|
|
foundSharedStringContentType = true
|
|
break
|
|
}
|
|
}
|
|
if !foundSharedStringContentType {
|
|
wb.ContentTypes.AddOverride(sstAbsTargetAttr, unioffice.SharedStringsContentType)
|
|
}
|
|
foundSharedStringRel := false
|
|
for _, rel := range wb.wbRels.Relationships() {
|
|
if rel.X().TargetAttr == sstRelTargetAttr {
|
|
foundSharedStringRel = true
|
|
break
|
|
}
|
|
}
|
|
if !foundSharedStringRel {
|
|
wb.wbRels.AddRelationship(sstRelTargetAttr, unioffice.SharedStringsType)
|
|
}
|
|
}
|
|
|
|
// RemoveSheet removes the sheet with the given index from the workbook.
|
|
func (wb *Workbook) RemoveSheet(ind int) error {
|
|
if wb.SheetCount() <= ind {
|
|
return ErrorNotFound
|
|
}
|
|
|
|
for _, r := range wb.wbRels.Relationships() {
|
|
if r.ID() == wb.x.Sheets.Sheet[ind].IdAttr {
|
|
wb.wbRels.Remove(r)
|
|
break
|
|
}
|
|
}
|
|
|
|
wb.ContentTypes.RemoveOverride(unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet,
|
|
unioffice.WorksheetContentType, ind+1))
|
|
|
|
copy(wb.xws[ind:], wb.xws[ind+1:])
|
|
wb.xws = wb.xws[:len(wb.xws)-1]
|
|
|
|
removed := wb.x.Sheets.Sheet[ind]
|
|
|
|
copy(wb.x.Sheets.Sheet[ind:], wb.x.Sheets.Sheet[ind+1:])
|
|
wb.x.Sheets.Sheet = wb.x.Sheets.Sheet[:len(wb.x.Sheets.Sheet)-1]
|
|
|
|
// fix sheet IDs by decrementing each one after the removed sheet
|
|
for i := range wb.x.Sheets.Sheet {
|
|
if wb.x.Sheets.Sheet[i].SheetIdAttr > removed.SheetIdAttr {
|
|
wb.x.Sheets.Sheet[i].SheetIdAttr--
|
|
}
|
|
}
|
|
|
|
copy(wb.xwsRels[ind:], wb.xwsRels[ind+1:])
|
|
wb.xwsRels = wb.xwsRels[:len(wb.xwsRels)-1]
|
|
|
|
copy(wb.comments[ind:], wb.comments[ind+1:])
|
|
wb.comments = wb.comments[:len(wb.comments)-1]
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveSheetByName removes the sheet with the given name from the workbook.
|
|
func (wb *Workbook) RemoveSheetByName(name string) error {
|
|
sheetInd := -1
|
|
for i, s := range wb.Sheets() {
|
|
if name == s.Name() {
|
|
sheetInd = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if sheetInd == -1 {
|
|
return ErrorNotFound
|
|
}
|
|
|
|
return wb.RemoveSheet(sheetInd)
|
|
}
|
|
|
|
// CopySheet copies the existing sheet at index `ind` and puts its copy with the name `copiedSheetName`.
|
|
func (wb *Workbook) CopySheet(ind int, copiedSheetName string) (Sheet, error) {
|
|
if wb.SheetCount() <= ind {
|
|
return Sheet{}, ErrorNotFound
|
|
}
|
|
|
|
var copiedRel common.Relationship
|
|
for _, r := range wb.wbRels.Relationships() {
|
|
if r.ID() == wb.x.Sheets.Sheet[ind].IdAttr {
|
|
var ok bool
|
|
if copiedRel, ok = wb.wbRels.CopyRelationship(r.ID()); !ok {
|
|
return Sheet{}, ErrorNotFound
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
wb.ContentTypes.CopyOverride(unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet,
|
|
unioffice.WorksheetContentType, ind+1), unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet,
|
|
unioffice.WorksheetContentType, len(wb.ContentTypes.X().Override)))
|
|
|
|
copiedWs := *wb.xws[ind]
|
|
wb.xws = append(wb.xws, &copiedWs)
|
|
|
|
var nextSheetID uint32 = 0
|
|
for _, s := range wb.x.Sheets.Sheet {
|
|
if s.SheetIdAttr > nextSheetID {
|
|
nextSheetID = s.SheetIdAttr
|
|
}
|
|
}
|
|
nextSheetID++
|
|
|
|
copiedSheet := *wb.x.Sheets.Sheet[ind]
|
|
copiedSheet.IdAttr = copiedRel.ID()
|
|
copiedSheet.NameAttr = copiedSheetName
|
|
copiedSheet.SheetIdAttr = nextSheetID
|
|
|
|
wb.x.Sheets.Sheet = append(wb.x.Sheets.Sheet, &copiedSheet)
|
|
|
|
copiedXwsRel := common.NewRelationshipsCopy(wb.xwsRels[ind])
|
|
wb.xwsRels = append(wb.xwsRels, copiedXwsRel)
|
|
|
|
copiedCommentsPtr := wb.comments[ind]
|
|
if copiedCommentsPtr == nil {
|
|
wb.comments = append(wb.comments, nil)
|
|
} else {
|
|
copiedComments := *copiedCommentsPtr
|
|
wb.comments = append(wb.comments, &copiedComments)
|
|
}
|
|
|
|
sheet := Sheet{wb, &copiedSheet, &copiedWs}
|
|
return sheet, nil
|
|
}
|
|
|
|
// CopySheetByName copies the existing sheet with the name `name` and puts its copy with the name `copiedSheetName`.
|
|
func (wb *Workbook) CopySheetByName(name, copiedSheetName string) (Sheet, error) {
|
|
sheetInd := -1
|
|
for i, s := range wb.Sheets() {
|
|
if name == s.Name() {
|
|
sheetInd = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if sheetInd == -1 {
|
|
return Sheet{}, ErrorNotFound
|
|
}
|
|
|
|
return wb.CopySheet(sheetInd, copiedSheetName)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Save writes the workbook out to a writer in the zipped xlsx format.
|
|
func (wb *Workbook) Save(w io.Writer) error {
|
|
if !license.GetLicenseKey().IsLicensed() && flag.Lookup("test.v") == nil {
|
|
fmt.Println("Unlicensed version of UniOffice")
|
|
fmt.Println("- Get a license on https://unidoc.io")
|
|
for _, sheet := range wb.Sheets() {
|
|
row1 := sheet.Row(1)
|
|
row1.SetHeight(50)
|
|
a1 := row1.Cell("A")
|
|
|
|
rt := a1.SetRichTextString()
|
|
run := rt.AddRun()
|
|
run.SetText("Unlicensed version of UniOffice - Get a license on https://unidoc.io")
|
|
run.SetBold(true)
|
|
run.SetColor(color.Red)
|
|
}
|
|
}
|
|
|
|
z := zip.NewWriter(w)
|
|
defer z.Close()
|
|
dt := unioffice.DocTypeSpreadsheet
|
|
|
|
if err := zippkg.MarshalXML(z, unioffice.BaseRelsFilename, wb.Rels.X()); err != nil {
|
|
return err
|
|
}
|
|
if err := zippkg.MarshalXMLByType(z, dt, unioffice.ExtendedPropertiesType, wb.AppProperties.X()); err != nil {
|
|
return err
|
|
}
|
|
if err := zippkg.MarshalXMLByType(z, dt, unioffice.CorePropertiesType, wb.CoreProperties.X()); err != nil {
|
|
return err
|
|
}
|
|
|
|
workbookFn := unioffice.AbsoluteFilename(dt, unioffice.OfficeDocumentType, 0)
|
|
if err := zippkg.MarshalXML(z, workbookFn, wb.x); err != nil {
|
|
return err
|
|
}
|
|
if err := zippkg.MarshalXML(z, zippkg.RelationsPathFor(workbookFn), wb.wbRels.X()); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := zippkg.MarshalXMLByType(z, dt, unioffice.StylesType, wb.StyleSheet.X()); err != nil {
|
|
return err
|
|
}
|
|
|
|
for i, thm := range wb.themes {
|
|
if err := zippkg.MarshalXMLByTypeIndex(z, dt, unioffice.ThemeType, i+1, thm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i, sheet := range wb.xws {
|
|
// recalculate sheet dimensions
|
|
sheet.Dimension.RefAttr = Sheet{wb, nil, sheet}.Extents()
|
|
|
|
fn := unioffice.AbsoluteFilename(dt, unioffice.WorksheetType, i+1)
|
|
zippkg.MarshalXML(z, fn, sheet)
|
|
zippkg.MarshalXML(z, zippkg.RelationsPathFor(fn), wb.xwsRels[i].X())
|
|
}
|
|
if err := zippkg.MarshalXMLByType(z, dt, unioffice.SharedStringsType, wb.SharedStrings.X()); err != nil {
|
|
return err
|
|
}
|
|
if wb.CustomProperties.X() != nil {
|
|
if err := zippkg.MarshalXMLByType(z, dt, unioffice.CustomPropertiesType, wb.CustomProperties.X()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if wb.Thumbnail != nil {
|
|
fn := unioffice.AbsoluteFilename(dt, unioffice.ThumbnailType, 0)
|
|
tn, err := z.Create(fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := jpeg.Encode(tn, wb.Thumbnail, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i, chart := range wb.charts {
|
|
fn := unioffice.AbsoluteFilename(dt, unioffice.ChartType, i+1)
|
|
zippkg.MarshalXML(z, fn, chart)
|
|
}
|
|
for i, tbl := range wb.tables {
|
|
fn := unioffice.AbsoluteFilename(dt, unioffice.TableType, i+1)
|
|
zippkg.MarshalXML(z, fn, tbl)
|
|
}
|
|
for i, drawing := range wb.drawings {
|
|
fn := unioffice.AbsoluteFilename(dt, unioffice.DrawingType, i+1)
|
|
zippkg.MarshalXML(z, fn, drawing)
|
|
if !wb.drawingRels[i].IsEmpty() {
|
|
zippkg.MarshalXML(z, zippkg.RelationsPathFor(fn), wb.drawingRels[i].X())
|
|
}
|
|
}
|
|
for i, drawing := range wb.vmlDrawings {
|
|
zippkg.MarshalXML(z, unioffice.AbsoluteFilename(dt, unioffice.VMLDrawingType, i+1), drawing)
|
|
// never seen relationships for a VML drawing yet
|
|
}
|
|
|
|
for i, img := range wb.Images {
|
|
if err := common.AddImageToZip(z, img, i+1, unioffice.DocTypeSpreadsheet); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := zippkg.MarshalXML(z, unioffice.ContentTypesFilename, wb.ContentTypes.X()); err != nil {
|
|
return err
|
|
}
|
|
for i, cmt := range wb.comments {
|
|
if cmt == nil {
|
|
continue
|
|
}
|
|
zippkg.MarshalXML(z, unioffice.AbsoluteFilename(dt, unioffice.CommentsType, i+1), cmt)
|
|
}
|
|
|
|
if err := wb.WriteExtraFiles(z); err != nil {
|
|
return err
|
|
}
|
|
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))
|
|
}
|
|
|
|
// Excel doesn't like reused sheet names
|
|
usedNames := map[string]struct{}{}
|
|
for i, s := range wb.x.Sheets.Sheet {
|
|
sw := Sheet{wb, s, wb.xws[i]}
|
|
if _, ok := usedNames[sw.Name()]; ok {
|
|
return fmt.Errorf("workbook/Sheet[%d] has duplicate name '%s'", i, sw.Name())
|
|
}
|
|
usedNames[sw.Name()] = struct{}{}
|
|
if err := sw.ValidateWithPath(fmt.Sprintf("workbook/Sheet[%d]", i)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := sw.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
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]
|
|
sheet := Sheet{wb, r, wks}
|
|
ret = append(ret, sheet)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// SheetCount returns the number of sheets in the workbook.
|
|
func (wb Workbook) SheetCount() int {
|
|
return len(wb.xws)
|
|
}
|
|
|
|
func (wb *Workbook) onNewRelationship(decMap *zippkg.DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship, src zippkg.Target) error {
|
|
dt := unioffice.DocTypeSpreadsheet
|
|
|
|
switch typ {
|
|
case unioffice.OfficeDocumentType:
|
|
wb.x = sml.NewWorkbook()
|
|
decMap.AddTarget(target, wb.x, typ, 0)
|
|
// look for the workbook relationships file as well
|
|
wb.wbRels = common.NewRelationships()
|
|
decMap.AddTarget(zippkg.RelationsPathFor(target), wb.wbRels.X(), typ, 0)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, 0)
|
|
|
|
case unioffice.CorePropertiesType:
|
|
decMap.AddTarget(target, wb.CoreProperties.X(), typ, 0)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, 0)
|
|
|
|
case unioffice.CustomPropertiesType:
|
|
decMap.AddTarget(target, wb.CustomProperties.X(), typ, 0)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, 0)
|
|
|
|
case unioffice.ExtendedPropertiesType:
|
|
decMap.AddTarget(target, wb.AppProperties.X(), typ, 0)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, 0)
|
|
|
|
case unioffice.WorksheetType:
|
|
ws := sml.NewWorksheet()
|
|
idx := uint32(len(wb.xws))
|
|
wb.xws = append(wb.xws, ws)
|
|
decMap.AddTarget(target, ws, typ, idx)
|
|
// look for worksheet rels
|
|
wksRel := common.NewRelationships()
|
|
decMap.AddTarget(zippkg.RelationsPathFor(target), wksRel.X(), typ, 0)
|
|
wb.xwsRels = append(wb.xwsRels, wksRel)
|
|
|
|
// add a comments placeholder that will be replaced if we see a comments
|
|
// relationship for the current sheet
|
|
wb.comments = append(wb.comments, nil)
|
|
|
|
// fix the relationship target so it points to where we'll save
|
|
// the worksheet
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, len(wb.xws))
|
|
|
|
case unioffice.StylesType:
|
|
wb.StyleSheet = NewStyleSheet(wb)
|
|
decMap.AddTarget(target, wb.StyleSheet.X(), typ, 0)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, 0)
|
|
|
|
case unioffice.ThemeType:
|
|
thm := dml.NewTheme()
|
|
wb.themes = append(wb.themes, thm)
|
|
decMap.AddTarget(target, thm, typ, 0)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, len(wb.themes))
|
|
|
|
case unioffice.SharedStringsType:
|
|
wb.SharedStrings = NewSharedStrings()
|
|
decMap.AddTarget(target, wb.SharedStrings.X(), typ, 0)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, 0)
|
|
|
|
case unioffice.ThumbnailType:
|
|
// 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
|
|
}
|
|
}
|
|
|
|
case unioffice.ImageType:
|
|
for i, f := range files {
|
|
if f == nil {
|
|
continue
|
|
}
|
|
if f.Name == target {
|
|
path, err := zippkg.ExtractToDiskTmp(f, wb.TmpPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
img, err := common.ImageFromStorage(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
iref := common.MakeImageRef(img, &wb.DocBase, wb.wbRels)
|
|
wb.Images = append(wb.Images, iref)
|
|
files[i] = nil
|
|
}
|
|
}
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, len(wb.Images))
|
|
|
|
case unioffice.DrawingType:
|
|
drawing := sd.NewWsDr()
|
|
idx := uint32(len(wb.drawings))
|
|
decMap.AddTarget(target, drawing, typ, idx)
|
|
wb.drawings = append(wb.drawings, drawing)
|
|
|
|
drel := common.NewRelationships()
|
|
decMap.AddTarget(zippkg.RelationsPathFor(target), drel.X(), typ, idx)
|
|
wb.drawingRels = append(wb.drawingRels, drel)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, len(wb.drawings))
|
|
|
|
case unioffice.VMLDrawingType:
|
|
vd := vmldrawing.NewContainer()
|
|
idx := uint32(len(wb.vmlDrawings))
|
|
decMap.AddTarget(target, vd, typ, idx)
|
|
wb.vmlDrawings = append(wb.vmlDrawings, vd)
|
|
|
|
case unioffice.CommentsType:
|
|
wb.comments[src.Index] = sml.NewComments()
|
|
decMap.AddTarget(target, wb.comments[src.Index], typ, src.Index)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, len(wb.comments))
|
|
|
|
case unioffice.ChartType:
|
|
chart := crt.NewChartSpace()
|
|
idx := uint32(len(wb.charts))
|
|
decMap.AddTarget(target, chart, typ, idx)
|
|
wb.charts = append(wb.charts, chart)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, len(wb.charts))
|
|
|
|
case unioffice.TableType:
|
|
tbl := sml.NewTable()
|
|
idx := uint32(len(wb.tables))
|
|
decMap.AddTarget(target, tbl, typ, idx)
|
|
wb.tables = append(wb.tables, tbl)
|
|
rel.TargetAttr = unioffice.RelativeFilename(dt, src.Typ, typ, len(wb.tables))
|
|
default:
|
|
unioffice.Log("unsupported relationship %s %s", target, typ)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddDrawing adds a drawing to a workbook. However the drawing is not actually
|
|
// displayed or used until it's set on a sheet.
|
|
func (wb *Workbook) AddDrawing() Drawing {
|
|
drawing := sd.NewWsDr()
|
|
wb.drawings = append(wb.drawings, drawing)
|
|
fn := unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet, unioffice.DrawingType, len(wb.drawings))
|
|
wb.ContentTypes.AddOverride(fn, unioffice.DrawingContentType)
|
|
wb.drawingRels = append(wb.drawingRels, common.NewRelationships())
|
|
return Drawing{wb, drawing}
|
|
}
|
|
|
|
// 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}
|
|
}
|
|
|
|
// RemoveDefinedName removes an existing defined name.
|
|
func (wb *Workbook) RemoveDefinedName(dn DefinedName) error {
|
|
if dn.X() == nil {
|
|
return errors.New("attempt to remove nil DefinedName")
|
|
}
|
|
for i, sdn := range wb.x.DefinedNames.DefinedName {
|
|
if sdn == dn.X() {
|
|
copy(wb.x.DefinedNames.DefinedName[i:], wb.x.DefinedNames.DefinedName[i+1:])
|
|
wb.x.DefinedNames.DefinedName[len(wb.x.DefinedNames.DefinedName)-1] = nil
|
|
wb.x.DefinedNames.DefinedName = wb.x.DefinedNames.DefinedName[:len(wb.x.DefinedNames.DefinedName)-1]
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("defined name not found")
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// ClearCachedFormulaResults clears any computed formula values that are stored
|
|
// in the sheet. This may be required if you modify cells that are used as a
|
|
// formula input to force the formulas to be recomputed the next time the sheet
|
|
// is opened in Excel.
|
|
func (wb *Workbook) ClearCachedFormulaResults() {
|
|
for _, s := range wb.Sheets() {
|
|
s.ClearCachedFormulaResults()
|
|
}
|
|
}
|
|
|
|
// RecalculateFormulas re-computes any computed formula values that are stored
|
|
// in the sheet. As unioffice formula support is still new and not all functins are
|
|
// supported, if formula execution fails either due to a parse error or missing
|
|
// function, or erorr in the result (even if expected) the cached value will be
|
|
// left empty allowing Excel to recompute it on load.
|
|
func (wb *Workbook) RecalculateFormulas() {
|
|
for _, s := range wb.Sheets() {
|
|
s.RecalculateFormulas()
|
|
}
|
|
}
|
|
|
|
// AddImage adds an image to the workbook package, returning a reference that
|
|
// can be used to add the image to a drawing.
|
|
func (wb *Workbook) AddImage(i common.Image) (common.ImageRef, error) {
|
|
r := common.MakeImageRef(i, &wb.DocBase, wb.wbRels)
|
|
if i.Data == nil && i.Path == "" {
|
|
return r, errors.New("image must have data or a path")
|
|
}
|
|
|
|
if i.Format == "" {
|
|
return r, errors.New("image must have a valid format")
|
|
}
|
|
if i.Size.X == 0 || i.Size.Y == 0 {
|
|
return r, errors.New("image must have a valid size")
|
|
}
|
|
if i.Path != "" {
|
|
err := tempstorage.Add(i.Path)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
}
|
|
|
|
wb.Images = append(wb.Images, r)
|
|
return r, nil
|
|
}
|
|
|
|
// SetActiveSheet sets the active sheet which will be the tab displayed when the
|
|
// spreadsheet is initially opened.
|
|
func (wb *Workbook) SetActiveSheet(s Sheet) {
|
|
for i, st := range wb.xws {
|
|
if s.x == st {
|
|
wb.SetActiveSheetIndex(uint32(i))
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetActiveSheetIndex sets the index of the active sheet (0-n) which will be
|
|
// the tab displayed when the spreadsheet is initially opened.
|
|
func (wb *Workbook) SetActiveSheetIndex(idx uint32) {
|
|
if wb.x.BookViews == nil {
|
|
wb.x.BookViews = sml.NewCT_BookViews()
|
|
}
|
|
if len(wb.x.BookViews.WorkbookView) == 0 {
|
|
wb.x.BookViews.WorkbookView = append(wb.x.BookViews.WorkbookView, sml.NewCT_BookView())
|
|
}
|
|
|
|
wb.x.BookViews.WorkbookView[0].ActiveTabAttr = unioffice.Uint32(idx)
|
|
}
|
|
|
|
// Tables returns a slice of all defined tables in the workbook.
|
|
func (wb *Workbook) Tables() []Table {
|
|
if wb.tables == nil {
|
|
return nil
|
|
}
|
|
ret := []Table{}
|
|
for _, t := range wb.tables {
|
|
ret = append(ret, Table{t})
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// ClearProtection clears all workbook protections.
|
|
func (wb *Workbook) ClearProtection() {
|
|
wb.x.WorkbookProtection = nil
|
|
}
|
|
|
|
// Protection allows control over the workbook protections.
|
|
func (wb *Workbook) Protection() WorkbookProtection {
|
|
if wb.x.WorkbookProtection == nil {
|
|
wb.x.WorkbookProtection = sml.NewCT_WorkbookProtection()
|
|
}
|
|
return WorkbookProtection{wb.x.WorkbookProtection}
|
|
}
|
|
|
|
// GetSheet returns a sheet by name, or an error if a sheet by the given name
|
|
// was not found.
|
|
func (wb *Workbook) GetSheet(name string) (Sheet, error) {
|
|
for _, s := range wb.Sheets() {
|
|
if s.Name() == name {
|
|
return s, nil
|
|
}
|
|
}
|
|
return Sheet{}, ErrorNotFound
|
|
}
|
|
|
|
// Close closes the workbook, removing any temporary files that might have been
|
|
// created when opening a document.
|
|
func (wb *Workbook) Close() error {
|
|
if wb.TmpPath != "" {
|
|
return tempstorage.RemoveAll(wb.TmpPath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveCalcChain removes the cached caculation chain. This is sometimes needed
|
|
// as we don't update it when rows are added/removed.
|
|
func (wb *Workbook) RemoveCalcChain() {
|
|
var calcFile string
|
|
for _, r := range wb.wbRels.Relationships() {
|
|
if r.Type() == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" {
|
|
calcFile = "xl/" + r.Target()
|
|
wb.wbRels.Remove(r)
|
|
break
|
|
}
|
|
}
|
|
|
|
if calcFile == "" {
|
|
return
|
|
}
|
|
wb.ContentTypes.RemoveOverride(calcFile)
|
|
for i, x := range wb.ExtraFiles {
|
|
if x.ZipPath == calcFile {
|
|
wb.ExtraFiles[i] = wb.ExtraFiles[len(wb.ExtraFiles)-1]
|
|
wb.ExtraFiles = wb.ExtraFiles[:len(wb.ExtraFiles)-1]
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetFilename returns the name of file from which workbook was opened with full path to it
|
|
func (wb *Workbook) GetFilename() string {
|
|
return wb.filename
|
|
}
|
|
|
|
// GetOrCreateCustomProperties returns the custom properties of the document (and if they not exist yet, creating them first)
|
|
func (wb *Workbook) GetOrCreateCustomProperties() common.CustomProperties {
|
|
if wb.CustomProperties.X() == nil {
|
|
wb.createCustomProperties()
|
|
}
|
|
return wb.CustomProperties
|
|
}
|
|
|
|
func (wb *Workbook) createCustomProperties() {
|
|
wb.CustomProperties = common.NewCustomProperties()
|
|
wb.addCustomRelationships()
|
|
}
|
|
|
|
func (wb *Workbook) addCustomRelationships() {
|
|
wb.ContentTypes.AddOverride("/docProps/custom.xml", "application/vnd.openxmlformats-officedocument.custom-properties+xml")
|
|
wb.Rels.AddRelationship("docProps/custom.xml", unioffice.CustomPropertiesType)
|
|
}
|