excelize.go 11.2 KB
Newer Older
xurime's avatar
xurime 已提交
1
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
xurime's avatar
xurime 已提交
2 3
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
xurime's avatar
xurime 已提交
4

xurime's avatar
xurime 已提交
5 6 7
// Package excelize providing a set of functions that allow you to write to
// and read from XLSX files. Support reads and writes XLSX file generated by
// Microsoft Excel™ 2007 and later. Support save file without losing original
8
// charts of XLSX. This library needs Go version 1.10 or later.
xurime's avatar
xurime 已提交
9 10
//
// See https://xuri.me/excelize for more information about this package.
xurime's avatar
xurime 已提交
11 12 13 14
package excelize

import (
	"archive/zip"
15
	"bytes"
xurime's avatar
xurime 已提交
16
	"encoding/xml"
17
	"errors"
xurime's avatar
xurime 已提交
18
	"fmt"
J
Josh Fyne 已提交
19 20 21
	"io"
	"io/ioutil"
	"os"
22
	"path"
xurime's avatar
xurime 已提交
23
	"strconv"
24
	"strings"
xurime's avatar
xurime 已提交
25 26
)

27
// File define a populated XLSX file struct.
28
type File struct {
29 30 31 32 33
	checked          map[string]bool
	sheetMap         map[string]string
	CalcChain        *xlsxCalcChain
	Comments         map[string]*xlsxComments
	ContentTypes     *xlsxTypes
34
	Drawings         map[string]*xlsxWsDr
35 36 37 38 39 40 41 42 43
	Path             string
	SharedStrings    *xlsxSST
	Sheet            map[string]*xlsxWorksheet
	SheetCount       int
	Styles           *xlsxStyleSheet
	Theme            *xlsxTheme
	DecodeVMLDrawing map[string]*decodeVmlDrawing
	VMLDrawing       map[string]*vmlDrawing
	WorkBook         *xlsxWorkbook
xurime's avatar
xurime 已提交
44
	Relationships    map[string]*xlsxRelationships
45
	XLSX             map[string][]byte
xurime's avatar
xurime 已提交
46 47
}

48 49
// OpenFile take the name of an XLSX file and returns a populated XLSX file
// struct for it.
50
func OpenFile(filename string) (*File, error) {
J
Josh Fyne 已提交
51
	file, err := os.Open(filename)
52
	if err != nil {
J
Josh Fyne 已提交
53 54
		return nil, err
	}
J
Josh Fyne 已提交
55
	defer file.Close()
J
Josh Fyne 已提交
56 57 58 59 60 61
	f, err := OpenReader(file)
	if err != nil {
		return nil, err
	}
	f.Path = filename
	return f, nil
J
Josh Fyne 已提交
62 63 64 65 66
}

// OpenReader take an io.Reader and return a populated XLSX file.
func OpenReader(r io.Reader) (*File, error) {
	b, err := ioutil.ReadAll(r)
J
Josh Fyne 已提交
67 68 69 70
	if err != nil {
		return nil, err
	}

J
Josh Fyne 已提交
71
	zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
J
Josh Fyne 已提交
72
	if err != nil {
73 74 75 76 77 78 79 80 81 82 83
		identifier := []byte{
			// checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier
			0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00,
			0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00,
			0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00,
			0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00,
			0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
		}
		if bytes.Contains(b, identifier) {
			return nil, errors.New("not support encrypted file currently")
		}
J
Josh Fyne 已提交
84 85
		return nil, err
	}
J
Josh Fyne 已提交
86

J
Josh Fyne 已提交
87 88 89
	file, sheetCount, err := ReadZipReader(zr)
	if err != nil {
		return nil, err
xurime's avatar
xurime 已提交
90
	}
wing2life's avatar
wing2life 已提交
91
	f := &File{
92 93
		checked:          make(map[string]bool),
		Comments:         make(map[string]*xlsxComments),
94
		Drawings:         make(map[string]*xlsxWsDr),
95 96 97 98
		Sheet:            make(map[string]*xlsxWorksheet),
		SheetCount:       sheetCount,
		DecodeVMLDrawing: make(map[string]*decodeVmlDrawing),
		VMLDrawing:       make(map[string]*vmlDrawing),
xurime's avatar
xurime 已提交
99
		Relationships:    make(map[string]*xlsxRelationships),
100
		XLSX:             file,
wing2life's avatar
wing2life 已提交
101
	}
102
	f.CalcChain = f.calcChainReader()
wing2life's avatar
wing2life 已提交
103 104
	f.sheetMap = f.getSheetMap()
	f.Styles = f.stylesReader()
xurime's avatar
xurime 已提交
105
	f.Theme = f.themeReader()
wing2life's avatar
wing2life 已提交
106
	return f, nil
xurime's avatar
xurime 已提交
107 108
}

xurime's avatar
xurime 已提交
109
// setDefaultTimeStyle provides a function to set default numbers format for
110 111
// time.Time type cell value by given worksheet name, cell coordinates and
// number format code.
112 113 114 115 116 117
func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error {
	s, err := f.GetCellStyle(sheet, axis)
	if err != nil {
		return err
	}
	if s == 0 {
118
		style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`)
119
		f.SetCellStyle(sheet, axis, axis, style)
120
	}
121
	return err
122 123
}

xurime's avatar
xurime 已提交
124 125
// workSheetReader provides a function to get the pointer to the structure
// after deserialization by given worksheet name.
xurime's avatar
xurime 已提交
126
func (f *File) workSheetReader(sheet string) (*xlsxWorksheet, error) {
127
	name, ok := f.sheetMap[trimSheetName(sheet)]
128
	if !ok {
xurime's avatar
xurime 已提交
129
		return nil, fmt.Errorf("sheet %s is not exist", sheet)
130
	}
131
	if f.Sheet[name] == nil {
xurime's avatar
xurime 已提交
132
		var xlsx xlsxWorksheet
133
		_ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(name)), &xlsx)
xurime's avatar
xurime 已提交
134 135 136 137 138
		if f.checked == nil {
			f.checked = make(map[string]bool)
		}
		ok := f.checked[name]
		if !ok {
139
			checkSheet(&xlsx)
xurime's avatar
xurime 已提交
140 141 142 143
			checkRow(&xlsx)
			f.checked[name] = true
		}
		f.Sheet[name] = &xlsx
144
	}
xurime's avatar
xurime 已提交
145
	return f.Sheet[name], nil
xurime's avatar
xurime 已提交
146
}
147

xurime's avatar
xurime 已提交
148
// checkSheet provides a function to fill each row element and make that is
149 150 151
// continuous in a worksheet of XML.
func checkSheet(xlsx *xlsxWorksheet) {
	row := len(xlsx.SheetData.Row)
xurime's avatar
xurime 已提交
152
	if row >= 1 {
153 154 155 156 157
		lastRow := xlsx.SheetData.Row[row-1].R
		if lastRow >= row {
			row = lastRow
		}
	}
158 159
	sheetData := xlsxSheetData{}
	existsRows := map[int]int{}
160 161
	for k := range xlsx.SheetData.Row {
		existsRows[xlsx.SheetData.Row[k].R] = k
162 163 164 165 166
	}
	for i := 0; i < row; i++ {
		_, ok := existsRows[i+1]
		if ok {
			sheetData.Row = append(sheetData.Row, xlsx.SheetData.Row[existsRows[i+1]])
xurime's avatar
xurime 已提交
167 168 169 170
		} else {
			sheetData.Row = append(sheetData.Row, xlsxRow{
				R: i + 1,
			})
xurime's avatar
xurime 已提交
171
		}
172 173
	}
	xlsx.SheetData = sheetData
xurime's avatar
xurime 已提交
174 175
}

xurime's avatar
xurime 已提交
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
// addRels provides a function to add relationships by given XML path,
// relationship type, target and target mode.
func (f *File) addRels(relPath, relType, target, targetMode string) int {
	rels := f.relsReader(relPath)
	rID := 0
	if rels == nil {
		rels = &xlsxRelationships{}
	}
	rID = len(rels.Relationships) + 1
	var ID bytes.Buffer
	ID.WriteString("rId")
	ID.WriteString(strconv.Itoa(rID))
	rels.Relationships = append(rels.Relationships, xlsxRelationship{
		ID:         ID.String(),
		Type:       relType,
		Target:     target,
		TargetMode: targetMode,
	})
	f.Relationships[relPath] = rels
	return rID
}

xurime's avatar
xurime 已提交
198
// replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace
199
// xl/worksheets/sheet%d.xml XML tags to self-closing for compatible Microsoft
200
// Office Excel 2007.
201 202
func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte {
	var oldXmlns = []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
xurime's avatar
xurime 已提交
203
	var newXmlns = []byte(`<worksheet xr:uid="{00000000-0001-0000-0000-000000000000}" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr6="http://schemas.microsoft.com/office/spreadsheetml/2016/revision6" xmlns:xr10="http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" mc:Ignorable="x14ac xr xr2 xr3 xr6 xr10 x15" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
204 205 206 207
	workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1)
	return workbookMarshal
}

xurime's avatar
xurime 已提交
208 209 210 211 212 213 214 215 216 217
// replaceStyleRelationshipsNameSpaceBytes provides a function to replace
// xl/styles.xml XML tags to self-closing for compatible Microsoft Office
// Excel 2007.
func replaceStyleRelationshipsNameSpaceBytes(contentMarshal []byte) []byte {
	var oldXmlns = []byte(`<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
	var newXmlns = []byte(`<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac x16r2 xr xr9" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr9="http://schemas.microsoft.com/office/spreadsheetml/2016/revision9">`)
	contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1)
	return contentMarshal
}

218 219
// UpdateLinkedValue fix linked values within a spreadsheet are not updating in
// Office Excel 2007 and 2010. This function will be remove value tag when met a
220
// cell have a linked value. Reference
221
// https://social.technet.microsoft.com/Forums/office/en-US/e16bae1f-6a2c-4325-8013-e989a3479066/excel-2010-linked-cells-not-updating
222 223 224 225 226 227
//
// Notice: after open XLSX file Excel will be update linked value and generate
// new value and will prompt save file or not.
//
// For example:
//
228 229 230 231 232 233
//    <row r="19" spans="2:2">
//        <c r="B19">
//            <f>SUM(Sheet2!D2,Sheet2!D11)</f>
//            <v>100</v>
//         </c>
//    </row>
234 235 236
//
// to
//
237 238 239 240 241
//    <row r="19" spans="2:2">
//        <c r="B19">
//            <f>SUM(Sheet2!D2,Sheet2!D11)</f>
//        </c>
//    </row>
242
//
xurime's avatar
xurime 已提交
243
func (f *File) UpdateLinkedValue() error {
244
	for _, name := range f.GetSheetMap() {
xurime's avatar
xurime 已提交
245 246 247 248
		xlsx, err := f.workSheetReader(name)
		if err != nil {
			return err
		}
249 250
		for indexR := range xlsx.SheetData.Row {
			for indexC, col := range xlsx.SheetData.Row[indexR].C {
251
				if col.F != nil && col.V != "" {
252 253
					xlsx.SheetData.Row[indexR].C[indexC].V = ""
					xlsx.SheetData.Row[indexR].C[indexC].T = ""
254 255 256 257
				}
			}
		}
	}
xurime's avatar
xurime 已提交
258
	return nil
259
}
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286

// AddVBAProject provides the method to add vbaProject.bin file which contains
// functions and/or macros. The file extension should be .xlsm. For example:
//
//    err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1"))
//    if err != nil {
//        fmt.Println(err)
//    }
//    err = f.AddVBAProject("vbaProject.bin")
//    if err != nil {
//        fmt.Println(err)
//    }
//    err = f.SaveAs("macros.xlsm")
//    if err != nil {
//        fmt.Println(err)
//    }
//
func (f *File) AddVBAProject(bin string) error {
	var err error
	// Check vbaProject.bin exists first.
	if _, err = os.Stat(bin); os.IsNotExist(err) {
		return err
	}
	if path.Ext(bin) != ".bin" {
		return errors.New("unsupported VBA project extension")
	}
	f.setContentTypePartVBAProjectExtensions()
xurime's avatar
xurime 已提交
287
	wb := f.relsReader("xl/_rels/workbook.xml.rels")
288 289 290 291 292 293 294 295 296 297 298 299 300 301
	var rID int
	var ok bool
	for _, rel := range wb.Relationships {
		if rel.Target == "vbaProject.bin" && rel.Type == SourceRelationshipVBAProject {
			ok = true
			continue
		}
		t, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
		if t > rID {
			rID = t
		}
	}
	rID++
	if !ok {
xurime's avatar
xurime 已提交
302
		wb.Relationships = append(wb.Relationships, xlsxRelationship{
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
			ID:     "rId" + strconv.Itoa(rID),
			Target: "vbaProject.bin",
			Type:   SourceRelationshipVBAProject,
		})
	}
	file, _ := ioutil.ReadFile(bin)
	f.XLSX["xl/vbaProject.bin"] = file
	return err
}

// setContentTypePartVBAProjectExtensions provides a function to set the
// content type for relationship parts and the main document part.
func (f *File) setContentTypePartVBAProjectExtensions() {
	var ok bool
	content := f.contentTypesReader()
	for _, v := range content.Defaults {
		if v.Extension == "bin" {
			ok = true
		}
	}
	for idx, o := range content.Overrides {
		if o.PartName == "/xl/workbook.xml" {
			content.Overrides[idx].ContentType = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
		}
	}
	if !ok {
		content.Defaults = append(content.Defaults, xlsxDefault{
			Extension:   "bin",
			ContentType: "application/vnd.ms-office.vbaProject",
		})
	}
}