excelize.go 9.6 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 8
// 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
// charts of XLSX. This library needs Go version 1.8 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 35
	DrawingRels      map[string]*xlsxWorkbookRels
	Drawings         map[string]*xlsxWsDr
36 37 38 39 40 41 42 43 44 45
	Path             string
	SharedStrings    *xlsxSST
	Sheet            map[string]*xlsxWorksheet
	SheetCount       int
	Styles           *xlsxStyleSheet
	Theme            *xlsxTheme
	DecodeVMLDrawing map[string]*decodeVmlDrawing
	VMLDrawing       map[string]*vmlDrawing
	WorkBook         *xlsxWorkbook
	WorkBookRels     *xlsxWorkbookRels
46
	WorkSheetRels    map[string]*xlsxWorkbookRels
47
	XLSX             map[string][]byte
xurime's avatar
xurime 已提交
48 49
}

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

// 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 已提交
69 70 71 72
	if err != nil {
		return nil, err
	}

J
Josh Fyne 已提交
73
	zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b)))
J
Josh Fyne 已提交
74
	if err != nil {
75 76 77 78 79 80 81 82 83 84 85
		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 已提交
86 87
		return nil, err
	}
J
Josh Fyne 已提交
88

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

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

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

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

xurime's avatar
xurime 已提交
179
// replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace
180
// xl/worksheets/sheet%d.xml XML tags to self-closing for compatible Microsoft
181
// Office Excel 2007.
182 183 184 185 186 187 188
func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte {
	var oldXmlns = []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
	var newXmlns = []byte(`<worksheet xr:uid="{00000000-0001-0000-0000-000000000000}" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" mc:Ignorable="x14ac xr xr2 xr3" 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">`)
	workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1)
	return workbookMarshal
}

189 190
// 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
191
// cell have a linked value. Reference
192
// https://social.technet.microsoft.com/Forums/office/en-US/e16bae1f-6a2c-4325-8013-e989a3479066/excel-2010-linked-cells-not-updating
193 194 195 196 197 198
//
// Notice: after open XLSX file Excel will be update linked value and generate
// new value and will prompt save file or not.
//
// For example:
//
199 200 201 202 203 204
//    <row r="19" spans="2:2">
//        <c r="B19">
//            <f>SUM(Sheet2!D2,Sheet2!D11)</f>
//            <v>100</v>
//         </c>
//    </row>
205 206 207
//
// to
//
208 209 210 211 212
//    <row r="19" spans="2:2">
//        <c r="B19">
//            <f>SUM(Sheet2!D2,Sheet2!D11)</f>
//        </c>
//    </row>
213
//
xurime's avatar
xurime 已提交
214
func (f *File) UpdateLinkedValue() error {
215
	for _, name := range f.GetSheetMap() {
xurime's avatar
xurime 已提交
216 217 218 219
		xlsx, err := f.workSheetReader(name)
		if err != nil {
			return err
		}
220 221
		for indexR := range xlsx.SheetData.Row {
			for indexC, col := range xlsx.SheetData.Row[indexR].C {
222
				if col.F != nil && col.V != "" {
223 224
					xlsx.SheetData.Row[indexR].C[indexC].V = ""
					xlsx.SheetData.Row[indexR].C[indexC].T = ""
225 226 227 228
				}
			}
		}
	}
xurime's avatar
xurime 已提交
229
	return nil
230
}
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 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 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305

// 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()
	wb := f.workbookRelsReader()
	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 {
		wb.Relationships = append(wb.Relationships, xlsxWorkbookRelation{
			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",
		})
	}
}