sheet.go 16.1 KB
Newer Older
xurime's avatar
xurime 已提交
1 2 3
package excelize

import (
4
	"bytes"
xurime's avatar
xurime 已提交
5
	"encoding/xml"
6 7 8
	"errors"
	"os"
	"path"
xurime's avatar
xurime 已提交
9 10 11 12
	"strconv"
	"strings"
)

xurime's avatar
xurime 已提交
13 14 15
// NewSheet provides function to create a new sheet by given index, when
// creating a new XLSX file, the default sheet will be create, when you create a
// new file, you need to ensure that the index is continuous.
16
func (f *File) NewSheet(index int, name string) {
xurime's avatar
xurime 已提交
17
	// Update docProps/app.xml
18
	f.setAppXML()
xurime's avatar
xurime 已提交
19
	// Update [Content_Types].xml
20
	f.setContentTypes(index)
xurime's avatar
xurime 已提交
21
	// Create new sheet /xl/worksheets/sheet%d.xml
22
	f.setSheet(index)
xurime's avatar
xurime 已提交
23
	// Update xl/_rels/workbook.xml.rels
xurime's avatar
xurime 已提交
24
	rID := f.addXlsxWorkbookRels(index)
xurime's avatar
xurime 已提交
25
	// Update xl/workbook.xml
xurime's avatar
xurime 已提交
26
	f.setWorkbook(name, rID)
xurime's avatar
xurime 已提交
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
	f.SheetCount++
}

// contentTypesReader provides function to get the pointer to the
// [Content_Types].xml structure after deserialization.
func (f *File) contentTypesReader() *xlsxTypes {
	if f.ContentTypes == nil {
		var content xlsxTypes
		xml.Unmarshal([]byte(f.readXML("[Content_Types].xml")), &content)
		f.ContentTypes = &content
	}
	return f.ContentTypes
}

// contentTypesWriter provides function to save [Content_Types].xml after
// serialize structure.
func (f *File) contentTypesWriter() {
	if f.ContentTypes != nil {
		output, _ := xml.Marshal(f.ContentTypes)
		f.saveFileList("[Content_Types].xml", string(output))
	}
}

// workbookReader provides function to get the pointer to the xl/workbook.xml
// structure after deserialization.
func (f *File) workbookReader() *xlsxWorkbook {
	if f.WorkBook == nil {
		var content xlsxWorkbook
		xml.Unmarshal([]byte(f.readXML("xl/workbook.xml")), &content)
		f.WorkBook = &content
	}
	return f.WorkBook
}

// workbookWriter provides function to save xl/workbook.xml after serialize
// structure.
func (f *File) workbookWriter() {
	if f.WorkBook != nil {
		output, _ := xml.Marshal(f.WorkBook)
		f.saveFileList("xl/workbook.xml", replaceRelationshipsNameSpace(string(output)))
	}
}

// worksheetWriter provides function to save xl/worksheets/sheet%d.xml after
// serialize structure.
func (f *File) worksheetWriter() {
	for path, sheet := range f.Sheet {
		if sheet != nil {
75 76 77
			for k, v := range sheet.SheetData.Row {
				f.Sheet[path].SheetData.Row[k].C = trimCell(v.C)
			}
xurime's avatar
xurime 已提交
78 79
			output, _ := xml.Marshal(sheet)
			f.saveFileList(path, replaceWorkSheetsRelationshipsNameSpace(string(output)))
80 81 82 83
			ok := f.checked[path]
			if ok {
				f.checked[path] = false
			}
xurime's avatar
xurime 已提交
84 85
		}
	}
xurime's avatar
xurime 已提交
86 87
}

88 89 90 91 92 93 94 95 96 97 98 99
// trimCell provides function to trim blank cells which created by completeCol.
func trimCell(column []xlsxC) []xlsxC {
	col := []xlsxC{}
	for _, c := range column {
		if c.S == 0 && c.V == "" && c.F == nil && c.T == "" {
			continue
		}
		col = append(col, c)
	}
	return col
}

xurime's avatar
xurime 已提交
100
// Read and update property of contents type of XLSX.
101
func (f *File) setContentTypes(index int) {
xurime's avatar
xurime 已提交
102
	content := f.contentTypesReader()
xurime's avatar
xurime 已提交
103
	content.Overrides = append(content.Overrides, xlsxOverride{
104 105
		PartName:    "/xl/worksheets/sheet" + strconv.Itoa(index) + ".xml",
		ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
xurime's avatar
xurime 已提交
106 107 108
	})
}

xurime's avatar
xurime 已提交
109
// Update sheet property by given index.
110
func (f *File) setSheet(index int) {
xurime's avatar
xurime 已提交
111
	var xlsx xlsxWorksheet
112
	xlsx.Dimension.Ref = "A1"
xurime's avatar
xurime 已提交
113
	xlsx.SheetViews.SheetView = append(xlsx.SheetViews.SheetView, xlsxSheetView{
xurime's avatar
xurime 已提交
114
		WorkbookViewID: 0,
xurime's avatar
xurime 已提交
115
	})
116
	path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml"
xurime's avatar
xurime 已提交
117
	f.Sheet[path] = &xlsx
xurime's avatar
xurime 已提交
118 119
}

120 121
// setWorkbook update workbook property of XLSX. Maximum 31 characters are
// allowed in sheet title.
122
func (f *File) setWorkbook(name string, rid int) {
123 124
	r := strings.NewReplacer(":", "", "\\", "", "/", "", "?", "", "*", "", "[", "", "]", "")
	name = r.Replace(name)
125 126 127
	if len(name) > 31 {
		name = name[0:31]
	}
xurime's avatar
xurime 已提交
128
	content := f.workbookReader()
xurime's avatar
xurime 已提交
129 130
	content.Sheets.Sheet = append(content.Sheets.Sheet, xlsxSheet{
		Name:    name,
131
		SheetID: strconv.Itoa(rid),
132
		ID:      "rId" + strconv.Itoa(rid),
xurime's avatar
xurime 已提交
133
	})
xurime's avatar
xurime 已提交
134 135 136 137 138 139 140 141 142
}

// workbookRelsReader provides function to read and unmarshal workbook
// relationships of XLSX file.
func (f *File) workbookRelsReader() *xlsxWorkbookRels {
	if f.WorkBookRels == nil {
		var content xlsxWorkbookRels
		xml.Unmarshal([]byte(f.readXML("xl/_rels/workbook.xml.rels")), &content)
		f.WorkBookRels = &content
xurime's avatar
xurime 已提交
143
	}
xurime's avatar
xurime 已提交
144
	return f.WorkBookRels
xurime's avatar
xurime 已提交
145 146
}

xurime's avatar
xurime 已提交
147 148 149 150 151 152 153
// workbookRelsWriter provides function to save xl/_rels/workbook.xml.rels after
// serialize structure.
func (f *File) workbookRelsWriter() {
	if f.WorkBookRels != nil {
		output, _ := xml.Marshal(f.WorkBookRels)
		f.saveFileList("xl/_rels/workbook.xml.rels", string(output))
	}
xurime's avatar
xurime 已提交
154 155
}

156
// addXlsxWorkbookRels update workbook relationships property of XLSX.
157
func (f *File) addXlsxWorkbookRels(sheet int) int {
xurime's avatar
xurime 已提交
158
	content := f.workbookRelsReader()
159 160 161 162 163 164 165 166
	rID := 0
	for _, v := range content.Relationships {
		t, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId"))
		if t > rID {
			rID = t
		}
	}
	rID++
167
	ID := bytes.Buffer{}
168
	ID.WriteString("rId")
169 170
	ID.WriteString(strconv.Itoa(rID))
	target := bytes.Buffer{}
171
	target.WriteString("worksheets/sheet")
172
	target.WriteString(strconv.Itoa(sheet))
173
	target.WriteString(".xml")
xurime's avatar
xurime 已提交
174
	content.Relationships = append(content.Relationships, xlsxWorkbookRelation{
175 176
		ID:     ID.String(),
		Target: target.String(),
177
		Type:   SourceRelationshipWorkSheet,
xurime's avatar
xurime 已提交
178
	})
179
	return rID
xurime's avatar
xurime 已提交
180 181
}

182
// setAppXML update docProps/app.xml file of XML.
183
func (f *File) setAppXML() {
184
	f.saveFileList("docProps/app.xml", templateDocpropsApp)
xurime's avatar
xurime 已提交
185 186
}

187 188 189 190 191 192 193
// Some tools that read XLSX files have very strict requirements about the
// structure of the input XML. In particular both Numbers on the Mac and SAS
// dislike inline XML namespace declarations, or namespace prefixes that don't
// match the ones that Excel itself uses. This is a problem because the Go XML
// library doesn't multiple namespace declarations in a single element of a
// document. This function is a horrible hack to fix that after the XML
// marshalling is completed.
xurime's avatar
xurime 已提交
194 195
func replaceRelationshipsNameSpace(workbookMarshal string) string {
	oldXmlns := `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`
196
	newXmlns := `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x15" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main">`
xurime's avatar
xurime 已提交
197 198 199
	return strings.Replace(workbookMarshal, oldXmlns, newXmlns, -1)
}

200
// SetActiveSheet provides function to set default active sheet of XLSX by given
201
// index.
202
func (f *File) SetActiveSheet(index int) {
xurime's avatar
xurime 已提交
203 204 205
	if index < 1 {
		index = 1
	}
xurime's avatar
xurime 已提交
206
	index--
xurime's avatar
xurime 已提交
207
	content := f.workbookReader()
xurime's avatar
xurime 已提交
208 209 210 211 212 213 214 215
	if len(content.BookViews.WorkBookView) > 0 {
		content.BookViews.WorkBookView[0].ActiveTab = index
	} else {
		content.BookViews.WorkBookView = append(content.BookViews.WorkBookView, xlsxWorkBookView{
			ActiveTab: index,
		})
	}
	sheets := len(content.Sheets.Sheet)
xurime's avatar
xurime 已提交
216
	index++
xurime's avatar
xurime 已提交
217 218
	for i := 0; i < sheets; i++ {
		sheetIndex := i + 1
xurime's avatar
xurime 已提交
219
		xlsx := f.workSheetReader("sheet" + strconv.Itoa(sheetIndex))
xurime's avatar
xurime 已提交
220 221 222 223 224 225 226 227 228 229 230 231 232 233
		if index == sheetIndex {
			if len(xlsx.SheetViews.SheetView) > 0 {
				xlsx.SheetViews.SheetView[0].TabSelected = true
			} else {
				xlsx.SheetViews.SheetView = append(xlsx.SheetViews.SheetView, xlsxSheetView{
					TabSelected: true,
				})
			}
		} else {
			if len(xlsx.SheetViews.SheetView) > 0 {
				xlsx.SheetViews.SheetView[0].TabSelected = false
			}
		}
	}
234
	return
xurime's avatar
xurime 已提交
235 236
}

237 238
// GetActiveSheetIndex provides function to get active sheet of XLSX. If not
// found the active sheet will be return integer 0.
239 240
func (f *File) GetActiveSheetIndex() int {
	buffer := bytes.Buffer{}
xurime's avatar
xurime 已提交
241
	content := f.workbookReader()
242 243
	for _, v := range content.Sheets.Sheet {
		xlsx := xlsxWorksheet{}
244 245 246
		buffer.WriteString("xl/worksheets/sheet")
		buffer.WriteString(strings.TrimPrefix(v.ID, "rId"))
		buffer.WriteString(".xml")
247 248 249
		xml.Unmarshal([]byte(f.readXML(buffer.String())), &xlsx)
		for _, sheetView := range xlsx.SheetViews.SheetView {
			if sheetView.TabSelected {
250 251
				ID, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId"))
				return ID
252
			}
253
		}
254
		buffer.Reset()
255
	}
256
	return 0
257 258
}

259 260 261 262 263
// SetSheetName provides function to set the sheet name be given old and new
// sheet name. Maximum 31 characters are allowed in sheet title and this
// function only changes the name of the sheet and will not update the sheet
// name in the formula or reference associated with the cell. So there may be
// problem formula error or reference missing.
264
func (f *File) SetSheetName(oldName, newName string) {
265 266
	oldName = trimSheetName(oldName)
	newName = trimSheetName(newName)
xurime's avatar
xurime 已提交
267
	content := f.workbookReader()
268 269 270 271 272 273 274
	for k, v := range content.Sheets.Sheet {
		if v.Name == oldName {
			content.Sheets.Sheet[k].Name = newName
		}
	}
}

275
// GetSheetName provides function to get sheet name of XLSX by given worksheet
276
// index. If given sheet index is invalid, will return an empty string.
277
func (f *File) GetSheetName(index int) string {
xurime's avatar
xurime 已提交
278
	content := f.workbookReader()
279 280 281 282 283 284 285 286 287
	rels := f.workbookRelsReader()
	for _, rel := range rels.Relationships {
		rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml"))
		if rID == index {
			for _, v := range content.Sheets.Sheet {
				if v.ID == rel.ID {
					return v.Name
				}
			}
288 289
		}
	}
290
	return ""
291 292
}

293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
// GetSheetIndex provides function to get worksheet index of XLSX by given sheet
// name. If given sheet name is invalid, will return an integer type value 0.
func (f *File) GetSheetIndex(name string) int {
	content := f.workbookReader()
	rels := f.workbookRelsReader()
	for _, v := range content.Sheets.Sheet {
		if v.Name == name {
			for _, rel := range rels.Relationships {
				if v.ID == rel.ID {
					rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml"))
					return rID
				}
			}
		}
	}
	return 0
}

311
// GetSheetMap provides function to get sheet map of XLSX. For example:
312
//
313
//    xlsx, err := excelize.OpenFile("./Workbook.xlsx")
314 315 316 317
//    if err != nil {
//        fmt.Println(err)
//        os.Exit(1)
//    }
M
Mark Stenglein 已提交
318
//    for k, v := range xlsx.GetSheetMap() {
319 320 321 322
//        fmt.Println(k, v)
//    }
//
func (f *File) GetSheetMap() map[int]string {
xurime's avatar
xurime 已提交
323
	content := f.workbookReader()
324
	rels := f.workbookRelsReader()
325 326
	sheetMap := map[int]string{}
	for _, v := range content.Sheets.Sheet {
327 328 329 330 331 332
		for _, rel := range rels.Relationships {
			if rel.ID == v.ID {
				rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml"))
				sheetMap[rID] = v.Name
			}
		}
333 334
	}
	return sheetMap
xurime's avatar
xurime 已提交
335
}
336 337 338 339 340 341 342 343 344

// SetSheetBackground provides function to set background picture by given sheet
// index.
func (f *File) SetSheetBackground(sheet, picture string) error {
	var err error
	// Check picture exists first.
	if _, err = os.Stat(picture); os.IsNotExist(err) {
		return err
	}
345
	ext, ok := supportImageTypes[path.Ext(picture)]
346 347 348 349 350 351 352 353 354 355
	if !ok {
		return errors.New("Unsupported image extension")
	}
	pictureID := f.countMedia() + 1
	rID := f.addSheetRelationships(sheet, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext, "")
	f.addSheetPicture(sheet, rID)
	f.addMedia(picture, ext)
	f.setContentTypePartImageExtensions()
	return err
}
356

357
// DeleteSheet provides function to delete worksheet in a workbook by given
358 359 360 361 362
// sheet name. Use this method with caution, which will affect changes in
// references such as formulas, charts, and so on. If there is any referenced
// value of the deleted worksheet, it will cause a file error when you open it.
// This function will be invalid when only the one worksheet is left.
func (f *File) DeleteSheet(name string) {
xurime's avatar
xurime 已提交
363
	content := f.workbookReader()
364 365 366 367 368 369 370
	for k, v := range content.Sheets.Sheet {
		if v.Name != name || len(content.Sheets.Sheet) < 2 {
			continue
		}
		content.Sheets.Sheet = append(content.Sheets.Sheet[:k], content.Sheets.Sheet[k+1:]...)
		sheet := "xl/worksheets/sheet" + strings.TrimPrefix(v.ID, "rId") + ".xml"
		rels := "xl/worksheets/_rels/sheet" + strings.TrimPrefix(v.ID, "rId") + ".xml.rels"
371 372
		target := f.deleteSheetFromWorkbookRels(v.ID)
		f.deleteSheetFromContentTypes(target)
373 374 375 376 377 378 379 380 381 382 383 384
		_, ok := f.XLSX[sheet]
		if ok {
			delete(f.XLSX, sheet)
		}
		_, ok = f.XLSX[rels]
		if ok {
			delete(f.XLSX, rels)
		}
		_, ok = f.Sheet[sheet]
		if ok {
			delete(f.Sheet, sheet)
		}
xurime's avatar
xurime 已提交
385
		f.SheetCount--
386 387 388
	}
}

389
// deleteSheetFromWorkbookRels provides function to remove worksheet
390 391
// relationships by given relationships ID in the file
// xl/_rels/workbook.xml.rels.
392
func (f *File) deleteSheetFromWorkbookRels(rID string) string {
xurime's avatar
xurime 已提交
393
	content := f.workbookRelsReader()
394 395 396 397 398 399 400 401 402 403
	for k, v := range content.Relationships {
		if v.ID != rID {
			continue
		}
		content.Relationships = append(content.Relationships[:k], content.Relationships[k+1:]...)
		return v.Target
	}
	return ""
}

404
// deleteSheetFromContentTypes provides function to remove worksheet
405
// relationships by given target name in the file [Content_Types].xml.
406
func (f *File) deleteSheetFromContentTypes(target string) {
xurime's avatar
xurime 已提交
407
	content := f.contentTypesReader()
408 409 410 411 412 413 414
	for k, v := range content.Overrides {
		if v.PartName != "/xl/"+target {
			continue
		}
		content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
	}
}
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439

// CopySheet provides function to duplicate a worksheet by gave source and
// target worksheet index. Note that currently doesn't support duplicate
// workbooks that contain tables, charts or pictures. For Example:
//
//    // Sheet1 already exists...
//    xlsx.NewSheet(2, "sheet2")
//    err := xlsx.CopySheet(1, 2)
//    if err != nil {
//        fmt.Println(err)
//        os.Exit(1)
//    }
//
func (f *File) CopySheet(from, to int) error {
	if from < 1 || to < 1 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" {
		return errors.New("Invalid worksheet index")
	}
	f.copySheet(from, to)
	return nil
}

// copySheet provides function to duplicate a worksheet by gave source and
// target worksheet index.
func (f *File) copySheet(from, to int) {
	sheet := f.workSheetReader("sheet" + strconv.Itoa(from))
440
	worksheet := *sheet
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
	path := "xl/worksheets/sheet" + strconv.Itoa(to) + ".xml"
	if len(worksheet.SheetViews.SheetView) > 0 {
		worksheet.SheetViews.SheetView[0].TabSelected = false
	}
	worksheet.Drawing = nil
	worksheet.TableParts = nil
	worksheet.PageSetUp = nil
	f.Sheet[path] = &worksheet
	toRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(to) + ".xml.rels"
	fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(from) + ".xml.rels"
	_, ok := f.XLSX[fromRels]
	if ok {
		f.XLSX[toRels] = f.XLSX[fromRels]
	}
}
456

457 458 459
// SetSheetVisible provides function to set worksheet visible by given worksheet
// name. A workbook must contain at least one visible worksheet. If the given
// worksheet has been activated, this setting will be invalidated. Sheet state
460
// values as defined by http://msdn.microsoft.com/en-us/library/office/documentformat.openxml.spreadsheet.sheetstatevalues.aspx
xurime's avatar
xurime 已提交
461 462 463 464 465
//
//    visible
//    hidden
//    veryHidden
//
466 467 468 469 470
// For example, hide Sheet1:
//
//    xlsx.SetSheetVisible("Sheet1", false)
//
func (f *File) SetSheetVisible(name string, visible bool) {
471 472
	name = trimSheetName(name)
	content := f.workbookReader()
473 474 475 476 477 478 479 480
	if visible {
		for k, v := range content.Sheets.Sheet {
			if v.Name == name {
				content.Sheets.Sheet[k].State = ""
			}
		}
		return
	}
481 482
	count := 0
	for _, v := range content.Sheets.Sheet {
483
		if v.State != "hidden" {
484 485 486 487 488 489 490 491 492 493 494
			count++
		}
	}
	for k, v := range content.Sheets.Sheet {
		sheetIndex := k + 1
		xlsx := f.workSheetReader("sheet" + strconv.Itoa(sheetIndex))
		tabSelected := false
		if len(xlsx.SheetViews.SheetView) > 0 {
			tabSelected = xlsx.SheetViews.SheetView[0].TabSelected
		}
		if v.Name == name && count > 1 && !tabSelected {
495
			content.Sheets.Sheet[k].State = "hidden"
496 497 498 499
		}
	}
}

500 501 502 503 504 505
// GetSheetVisible provides function to get worksheet visible by given worksheet
// name. For example, get visible state of Sheet1:
//
//    xlsx.GetSheetVisible("Sheet1")
//
func (f *File) GetSheetVisible(name string) bool {
506 507
	name = trimSheetName(name)
	content := f.workbookReader()
508
	visible := false
509 510
	for k, v := range content.Sheets.Sheet {
		if v.Name == name {
511 512 513
			if content.Sheets.Sheet[k].State == "" || content.Sheets.Sheet[k].State == "visible" {
				visible = true
			}
514 515
		}
	}
516
	return visible
517 518 519 520 521 522 523 524 525 526 527 528
}

// trimSheetName provides function to trim invaild characters by given worksheet
// name.
func trimSheetName(name string) string {
	r := strings.NewReplacer(":", "", "\\", "", "/", "", "?", "", "*", "", "[", "", "]", "")
	name = r.Replace(name)
	if len(name) > 31 {
		name = name[0:31]
	}
	return name
}