2019-07-25 19:43:46 +03:00
// Copyright 2017 FoxyUtils ehf. All rights reserved.
2017-08-28 20:56:18 -05:00
//
// 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
Functions2 (#348)
* MATCH, IFS, MAXA, MINA
* OFFSET fixed
* ISBLANK, ISERR, ISERROR, ISEVEN ,ISFORMULA, ISNONTEXT, ISNUMBER, ISODD, ISTEXT
* ISLEAPYEAR, ISLOGICAL, ISNA, ISREF
* FIND, FINDB
* SEARCH, SEARCHB
* CONCAT, CONCATENATE
* YEAR, YEARFRAC
* CONCAT is fixed, now TRUE and FALSE are concatenated instead of 1 and 0 in case of boolean results
* NOW, TODAY, TIME, TIMEVALUE
* DATE
* DATEDIF
2019-11-21 02:21:00 +03:00
// commercial license can be purchased on https://unidoc.io.
2017-08-28 20:56:18 -05:00
package spreadsheet
import (
"archive/zip"
"errors"
2019-10-10 14:26:10 +03:00
"flag"
2017-08-28 20:56:18 -05:00
"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
2019-05-04 11:18:06 +03:00
"github.com/unidoc/unioffice"
2019-10-10 14:26:10 +03:00
"github.com/unidoc/unioffice/color"
2019-05-04 11:18:06 +03:00
"github.com/unidoc/unioffice/common"
2019-10-10 14:26:10 +03:00
"github.com/unidoc/unioffice/common/license"
2020-04-24 15:01:19 +03:00
"github.com/unidoc/unioffice/common/tempstorage"
2019-05-04 11:18:06 +03:00
"github.com/unidoc/unioffice/vmldrawing"
"github.com/unidoc/unioffice/zippkg"
2017-09-02 16:33:40 -05:00
2019-05-04 11:18:06 +03:00
"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"
2017-08-28 20:56:18 -05:00
)
2017-10-02 18:15:59 -05:00
// ErrorNotFound is returned when something is not found
var ErrorNotFound = errors . New ( "not found" )
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-08 21:25:01 -05:00
comments [ ] * sml . Comments
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
2017-09-08 21:25:01 -05:00
vmlDrawings [ ] * vmldrawing . Container
2017-09-03 08:17:57 -05:00
charts [ ] * crt . ChartSpace
2017-09-17 12:58:51 -05:00
tables [ ] * sml . Table
2019-11-05 19:36:09 +03:00
filename string
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
}
2019-06-17 21:27:10 +03:00
// AddSheet adds a new sheet 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 ( )
2017-09-08 21:25:01 -05:00
2017-09-03 18:39:35 -05:00
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-08 21:25:01 -05:00
wb . comments = append ( wb . comments , nil )
2019-05-04 13:54:29 +00:00
dt := unioffice . DocTypeSpreadsheet
2017-09-28 17:05:38 -05:00
2017-08-28 20:56:18 -05:00
// update the references
2019-05-04 13:54:29 +00:00
rid := wb . wbRels . AddAutoRelationship ( dt , unioffice . OfficeDocumentType , len ( wb . x . Sheets . Sheet ) , unioffice . WorksheetType )
2017-08-28 20:56:18 -05:00
rs . IdAttr = rid . ID ( )
// add the content type
2019-05-04 13:54:29 +00:00
wb . ContentTypes . AddOverride ( unioffice . AbsoluteFilename ( dt , unioffice . WorksheetContentType , len ( wb . x . Sheets . Sheet ) ) ,
unioffice . WorksheetContentType )
2017-08-28 20:56:18 -05:00
return Sheet { wb , rs , ws }
}
2020-01-29 13:43:43 +03:00
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 )
}
}
2019-05-12 15:12:11 +03:00
// 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 )
}
2020-03-25 16:40:25 +03:00
sheet := Sheet { wb , & copiedSheet , & copiedWs }
return sheet , nil
2019-05-12 15:12:11 +03:00
}
// 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 )
}
2017-08-28 20:56:18 -05:00
// 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 {
2019-10-10 14:26:10 +03:00
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 ( ) {
2019-12-02 19:25:18 +03:00
row1 := sheet . Row ( 1 )
row1 . SetHeight ( 50 )
a1 := row1 . Cell ( "A" )
2019-10-10 14:26:10 +03:00
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 )
}
}
2017-08-28 20:56:18 -05:00
z := zip . NewWriter ( w )
defer z . Close ( )
2019-05-04 13:54:29 +00:00
dt := unioffice . DocTypeSpreadsheet
2017-09-03 12:01:55 -05:00
2019-05-04 13:54:29 +00:00
if err := zippkg . MarshalXML ( z , unioffice . BaseRelsFilename , wb . Rels . X ( ) ) ; err != nil {
2017-08-28 20:56:18 -05:00
return err
}
2019-05-04 13:54:29 +00:00
if err := zippkg . MarshalXMLByType ( z , dt , unioffice . ExtendedPropertiesType , wb . AppProperties . X ( ) ) ; err != nil {
2017-08-28 20:56:18 -05:00
return err
}
2019-05-04 13:54:29 +00:00
if err := zippkg . MarshalXMLByType ( z , dt , unioffice . CorePropertiesType , wb . CoreProperties . X ( ) ) ; err != nil {
2017-08-28 20:56:18 -05:00
return err
}
2017-09-03 12:01:55 -05:00
2019-05-04 13:54:29 +00:00
workbookFn := unioffice . AbsoluteFilename ( dt , unioffice . OfficeDocumentType , 0 )
2017-09-03 12:01:55 -05:00
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
2019-05-04 13:54:29 +00:00
if err := zippkg . MarshalXMLByType ( z , dt , unioffice . 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 {
2019-05-04 13:54:29 +00:00
if err := zippkg . MarshalXMLByTypeIndex ( z , dt , unioffice . 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-27 18:13:32 -05:00
// recalculate sheet dimensions
sheet . Dimension . RefAttr = Sheet { wb , nil , sheet } . Extents ( )
2019-05-04 13:54:29 +00:00
fn := unioffice . AbsoluteFilename ( dt , unioffice . 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
}
2020-01-29 13:43:43 +03:00
if err := zippkg . MarshalXMLByType ( z , dt , unioffice . SharedStringsType , wb . SharedStrings . X ( ) ) ; err != nil {
2017-09-03 18:39:35 -05:00
return err
}
2020-04-24 15:01:19 +03:00
if wb . CustomProperties . X ( ) != nil {
if err := zippkg . MarshalXMLByType ( z , dt , unioffice . CustomPropertiesType , wb . CustomProperties . X ( ) ) ; err != nil {
return err
}
}
2017-09-03 18:39:35 -05:00
2017-09-02 16:17:41 -05:00
if wb . Thumbnail != nil {
2019-05-04 13:54:29 +00:00
fn := unioffice . AbsoluteFilename ( dt , unioffice . ThumbnailType , 0 )
2017-09-03 12:01:55 -05:00
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 {
2019-05-04 13:54:29 +00:00
fn := unioffice . AbsoluteFilename ( dt , unioffice . ChartType , i + 1 )
2017-09-03 08:26:22 -05:00
zippkg . MarshalXML ( z , fn , chart )
}
2017-09-17 12:58:51 -05:00
for i , tbl := range wb . tables {
2019-05-04 13:54:29 +00:00
fn := unioffice . AbsoluteFilename ( dt , unioffice . TableType , i + 1 )
2017-09-17 12:58:51 -05:00
zippkg . MarshalXML ( z , fn , tbl )
}
2017-09-03 08:26:22 -05:00
for i , drawing := range wb . drawings {
2019-05-04 13:54:29 +00:00
fn := unioffice . AbsoluteFilename ( dt , unioffice . 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-08 21:25:01 -05:00
for i , drawing := range wb . vmlDrawings {
2019-05-04 13:54:29 +00:00
zippkg . MarshalXML ( z , unioffice . AbsoluteFilename ( dt , unioffice . VMLDrawingType , i + 1 ) , drawing )
2017-09-08 21:25:01 -05:00
// never seen relationships for a VML drawing yet
}
2017-09-11 18:31:47 -05:00
for i , img := range wb . Images {
2019-07-02 16:54:55 +02:00
if err := common . AddImageToZip ( z , img , i + 1 , unioffice . DocTypeSpreadsheet ) ; err != nil {
return err
2017-09-11 18:31:47 -05:00
}
}
2019-05-04 13:54:29 +00:00
if err := zippkg . MarshalXML ( z , unioffice . ContentTypesFilename , wb . ContentTypes . X ( ) ) ; err != nil {
2017-09-05 09:28:40 -05:00
return err
}
2017-09-08 21:25:01 -05:00
for i , cmt := range wb . comments {
if cmt == nil {
continue
}
2019-05-04 13:54:29 +00:00
zippkg . MarshalXML ( z , unioffice . AbsoluteFilename ( dt , unioffice . CommentsType , i + 1 ) , cmt )
2017-09-08 21:25:01 -05:00
}
2017-09-05 09:28:40 -05:00
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 ]
2020-03-25 16:40:25 +03:00
sheet := Sheet { wb , r , wks }
ret = append ( ret , sheet )
2017-08-28 20:56:18 -05:00
}
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
2017-09-08 21:25:01 -05:00
func ( wb * Workbook ) onNewRelationship ( decMap * zippkg . DecodeMap , target , typ string , files [ ] * zip . File , rel * relationships . Relationship , src zippkg . Target ) error {
2019-05-04 13:54:29 +00:00
dt := unioffice . DocTypeSpreadsheet
2017-09-03 12:01:55 -05:00
2017-09-03 08:17:57 -05:00
switch typ {
2019-05-04 13:54:29 +00:00
case unioffice . OfficeDocumentType :
2017-09-03 08:17:57 -05:00
wb . x = sml . NewWorkbook ( )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , wb . x , typ , 0 )
2017-09-03 08:17:57 -05:00
// look for the workbook relationships file as well
wb . wbRels = common . NewRelationships ( )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( zippkg . RelationsPathFor ( target ) , wb . wbRels . X ( ) , typ , 0 )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , 0 )
2017-09-03 08:17:57 -05:00
2019-05-04 13:54:29 +00:00
case unioffice . CorePropertiesType :
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , wb . CoreProperties . X ( ) , typ , 0 )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , 0 )
2017-09-03 08:17:57 -05:00
2020-04-24 15:01:19 +03:00
case unioffice . CustomPropertiesType :
decMap . AddTarget ( target , wb . CustomProperties . X ( ) , typ , 0 )
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , 0 )
2019-05-04 13:54:29 +00:00
case unioffice . ExtendedPropertiesType :
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , wb . AppProperties . X ( ) , typ , 0 )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , 0 )
2017-09-03 08:17:57 -05:00
2019-05-04 13:54:29 +00:00
case unioffice . WorksheetType :
2017-09-03 08:17:57 -05:00
ws := sml . NewWorksheet ( )
2017-09-08 21:25:01 -05:00
idx := uint32 ( len ( wb . xws ) )
2017-09-03 08:17:57 -05:00
wb . xws = append ( wb . xws , ws )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , ws , typ , idx )
2017-09-03 08:17:57 -05:00
// look for worksheet rels
wksRel := common . NewRelationships ( )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( zippkg . RelationsPathFor ( target ) , wksRel . X ( ) , typ , 0 )
2017-09-03 08:17:57 -05:00
wb . xwsRels = append ( wb . xwsRels , wksRel )
2017-09-08 21:25:01 -05:00
// add a comments placeholder that will be replaced if we see a comments
// relationship for the current sheet
wb . comments = append ( wb . comments , nil )
2017-09-03 08:17:57 -05:00
// fix the relationship target so it points to where we'll save
// the worksheet
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , len ( wb . xws ) )
2017-09-03 08:17:57 -05:00
2019-05-04 13:54:29 +00:00
case unioffice . StylesType :
2017-09-05 18:42:39 -04:00
wb . StyleSheet = NewStyleSheet ( wb )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , wb . StyleSheet . X ( ) , typ , 0 )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , 0 )
2017-09-03 08:17:57 -05:00
2019-05-04 13:54:29 +00:00
case unioffice . ThemeType :
2017-09-03 08:17:57 -05:00
thm := dml . NewTheme ( )
wb . themes = append ( wb . themes , thm )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , thm , typ , 0 )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , len ( wb . themes ) )
2017-09-03 08:17:57 -05:00
2020-01-29 13:43:43 +03:00
case unioffice . SharedStringsType :
2017-09-03 08:17:57 -05:00
wb . SharedStrings = NewSharedStrings ( )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , wb . SharedStrings . X ( ) , typ , 0 )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , 0 )
2017-09-03 08:17:57 -05:00
2019-05-04 13:54:29 +00:00
case unioffice . 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
}
}
2019-05-04 13:54:29 +00:00
case unioffice . ImageType :
2017-09-11 18:31:47 -05:00
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
}
2020-04-24 15:01:19 +03:00
img , err := common . ImageFromStorage ( path )
2017-09-11 18:31:47 -05:00
if err != nil {
return err
}
iref := common . MakeImageRef ( img , & wb . DocBase , wb . wbRels )
wb . Images = append ( wb . Images , iref )
files [ i ] = nil
}
}
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , len ( wb . Images ) )
2017-09-11 18:31:47 -05:00
2019-05-04 13:54:29 +00:00
case unioffice . DrawingType :
2017-09-03 08:17:57 -05:00
drawing := sd . NewWsDr ( )
2017-09-08 21:25:01 -05:00
idx := uint32 ( len ( wb . drawings ) )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , drawing , typ , idx )
2017-09-03 08:17:57 -05:00
wb . drawings = append ( wb . drawings , drawing )
drel := common . NewRelationships ( )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( zippkg . RelationsPathFor ( target ) , drel . X ( ) , typ , idx )
2017-09-03 08:17:57 -05:00
wb . drawingRels = append ( wb . drawingRels , drel )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , len ( wb . drawings ) )
2017-09-03 08:17:57 -05:00
2019-05-04 13:54:29 +00:00
case unioffice . VMLDrawingType :
2017-09-08 21:25:01 -05:00
vd := vmldrawing . NewContainer ( )
idx := uint32 ( len ( wb . vmlDrawings ) )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , vd , typ , idx )
2017-09-08 21:25:01 -05:00
wb . vmlDrawings = append ( wb . vmlDrawings , vd )
2019-05-04 13:54:29 +00:00
case unioffice . CommentsType :
2017-09-08 21:25:01 -05:00
wb . comments [ src . Index ] = sml . NewComments ( )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , wb . comments [ src . Index ] , typ , src . Index )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , len ( wb . comments ) )
2017-09-08 21:25:01 -05:00
2019-05-04 13:54:29 +00:00
case unioffice . ChartType :
2017-09-03 08:17:57 -05:00
chart := crt . NewChartSpace ( )
2017-09-08 21:25:01 -05:00
idx := uint32 ( len ( wb . charts ) )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , chart , typ , idx )
2017-09-03 08:17:57 -05:00
wb . charts = append ( wb . charts , chart )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , len ( wb . charts ) )
2017-09-03 08:17:57 -05:00
2019-05-04 13:54:29 +00:00
case unioffice . TableType :
2017-09-17 12:58:51 -05:00
tbl := sml . NewTable ( )
idx := uint32 ( len ( wb . tables ) )
2017-09-28 17:05:38 -05:00
decMap . AddTarget ( target , tbl , typ , idx )
2017-09-17 12:58:51 -05:00
wb . tables = append ( wb . tables , tbl )
2019-05-04 13:54:29 +00:00
rel . TargetAttr = unioffice . RelativeFilename ( dt , src . Typ , typ , len ( wb . tables ) )
2017-09-03 08:17:57 -05:00
default :
2019-05-04 13:54:29 +00:00
unioffice . Log ( "unsupported relationship %s %s" , target , typ )
2017-09-03 08:17:57 -05:00
}
return nil
}
2017-09-03 16:47:46 -05:00
2017-09-11 18:31:47 -05:00
// AddDrawing adds a drawing to a workbook. However the drawing is not actually
// displayed or used until it's set on a sheet.
2017-09-03 16:47:46 -05:00
func ( wb * Workbook ) AddDrawing ( ) Drawing {
drawing := sd . NewWsDr ( )
wb . drawings = append ( wb . drawings , drawing )
2019-05-04 13:54:29 +00:00
fn := unioffice . AbsoluteFilename ( unioffice . DocTypeSpreadsheet , unioffice . DrawingType , len ( wb . drawings ) )
wb . ContentTypes . AddOverride ( fn , unioffice . DrawingContentType )
2017-09-03 16:47:46 -05:00
wb . drawingRels = append ( wb . drawingRels , common . NewRelationships ( ) )
2017-09-11 18:31:47 -05:00
return Drawing { wb , drawing }
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 }
}
2017-09-07 14:57:04 -05:00
// 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" )
}
2017-09-07 07:23:30 -04:00
// 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
}
2017-09-10 17:15:44 -05:00
// 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 ( )
}
}
2017-09-11 18:31:47 -05:00
2017-09-17 14:49:25 -05:00
// RecalculateFormulas re-computes any computed formula values that are stored
2020-07-08 15:52:48 +03:00
// 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
2017-09-17 14:49:25 -05:00
// 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 ( )
}
}
2017-09-11 18:31:47 -05:00
// 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 )
2019-03-29 17:26:52 +01:00
if i . Data == nil && i . Path == "" {
return r , errors . New ( "image must have data or a path" )
2017-09-11 18:31:47 -05:00
}
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" )
}
2020-04-24 15:01:19 +03:00
if i . Path != "" {
err := tempstorage . Add ( i . Path )
if err != nil {
return r , err
}
}
2017-09-11 18:31:47 -05:00
wb . Images = append ( wb . Images , r )
return r , nil
}
2017-09-12 16:16:41 -05:00
// 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 ( ) )
}
2019-05-04 13:54:29 +00:00
wb . x . BookViews . WorkbookView [ 0 ] . ActiveTabAttr = unioffice . Uint32 ( idx )
2017-09-12 16:16:41 -05:00
}
2017-09-17 12:58:51 -05:00
// Tables returns a slice of all defined tables in the workbook.
2017-09-20 11:27:19 -04:00
func ( wb * Workbook ) Tables ( ) [ ] Table {
if wb . tables == nil {
2017-09-17 12:58:51 -05:00
return nil
}
ret := [ ] Table { }
2017-09-20 11:27:19 -04:00
for _ , t := range wb . tables {
2017-09-17 12:58:51 -05:00
ret = append ( ret , Table { t } )
}
return ret
}
2017-09-20 11:27:19 -04:00
// 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 }
}
2017-09-28 17:05:38 -05:00
2017-10-02 18:15:59 -05:00
// 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 ) {
2017-09-28 17:05:38 -05:00
for _ , s := range wb . Sheets ( ) {
if s . Name ( ) == name {
2017-10-02 18:15:59 -05:00
return s , nil
2017-09-28 17:05:38 -05:00
}
}
2017-10-02 18:15:59 -05:00
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 {
2020-04-24 15:01:19 +03:00
if wb . TmpPath != "" {
return tempstorage . RemoveAll ( wb . TmpPath )
2017-10-02 18:15:59 -05:00
}
return nil
2017-09-28 17:05:38 -05:00
}
2018-10-16 21:50:48 -05:00
// 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
}
}
}
2019-11-05 19:36:09 +03:00
// GetFilename returns the name of file from which workbook was opened with full path to it
func ( wb * Workbook ) GetFilename ( ) string {
return wb . filename
}
2020-04-24 15:01:19 +03:00
// 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 {
2020-07-14 18:03:09 +03:00
wb . createCustomProperties ( )
2020-04-24 15:01:19 +03:00
}
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 )
}