sheet.go 15.2 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 75 76 77 78
	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 {
			output, _ := xml.Marshal(sheet)
			f.saveFileList(path, replaceWorkSheetsRelationshipsNameSpace(string(output)))
		}
	}
xurime's avatar
xurime 已提交
79 80
}

xurime's avatar
xurime 已提交
81
// Read and update property of contents type of XLSX.
82
func (f *File) setContentTypes(index int) {
xurime's avatar
xurime 已提交
83
	content := f.contentTypesReader()
xurime's avatar
xurime 已提交
84
	content.Overrides = append(content.Overrides, xlsxOverride{
85 86
		PartName:    "/xl/worksheets/sheet" + strconv.Itoa(index) + ".xml",
		ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
xurime's avatar
xurime 已提交
87 88 89
	})
}

xurime's avatar
xurime 已提交
90
// Update sheet property by given index.
91
func (f *File) setSheet(index int) {
xurime's avatar
xurime 已提交
92
	var xlsx xlsxWorksheet
93
	xlsx.Dimension.Ref = "A1"
xurime's avatar
xurime 已提交
94
	xlsx.SheetViews.SheetView = append(xlsx.SheetViews.SheetView, xlsxSheetView{
xurime's avatar
xurime 已提交
95
		WorkbookViewID: 0,
xurime's avatar
xurime 已提交
96
	})
97
	path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml"
xurime's avatar
xurime 已提交
98
	f.Sheet[path] = &xlsx
xurime's avatar
xurime 已提交
99 100
}

101 102
// setWorkbook update workbook property of XLSX. Maximum 31 characters are
// allowed in sheet title.
103
func (f *File) setWorkbook(name string, rid int) {
104 105
	r := strings.NewReplacer(":", "", "\\", "", "/", "", "?", "", "*", "", "[", "", "]", "")
	name = r.Replace(name)
106 107 108
	if len(name) > 31 {
		name = name[0:31]
	}
xurime's avatar
xurime 已提交
109
	content := f.workbookReader()
xurime's avatar
xurime 已提交
110 111
	content.Sheets.Sheet = append(content.Sheets.Sheet, xlsxSheet{
		Name:    name,
112
		SheetID: strconv.Itoa(rid),
113
		ID:      "rId" + strconv.Itoa(rid),
xurime's avatar
xurime 已提交
114
	})
xurime's avatar
xurime 已提交
115 116 117 118 119 120 121 122 123
}

// 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 已提交
124
	}
xurime's avatar
xurime 已提交
125
	return f.WorkBookRels
xurime's avatar
xurime 已提交
126 127
}

xurime's avatar
xurime 已提交
128 129 130 131 132 133 134
// 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 已提交
135 136
}

137
// addXlsxWorkbookRels update workbook relationships property of XLSX.
138
func (f *File) addXlsxWorkbookRels(sheet int) int {
xurime's avatar
xurime 已提交
139
	content := f.workbookRelsReader()
140 141 142 143 144 145 146 147
	rID := 0
	for _, v := range content.Relationships {
		t, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId"))
		if t > rID {
			rID = t
		}
	}
	rID++
148
	ID := bytes.Buffer{}
149
	ID.WriteString("rId")
150 151
	ID.WriteString(strconv.Itoa(rID))
	target := bytes.Buffer{}
152
	target.WriteString("worksheets/sheet")
153
	target.WriteString(strconv.Itoa(sheet))
154
	target.WriteString(".xml")
xurime's avatar
xurime 已提交
155
	content.Relationships = append(content.Relationships, xlsxWorkbookRelation{
156 157
		ID:     ID.String(),
		Target: target.String(),
158
		Type:   SourceRelationshipWorkSheet,
xurime's avatar
xurime 已提交
159
	})
160
	return rID
xurime's avatar
xurime 已提交
161 162
}

163
// setAppXML update docProps/app.xml file of XML.
164
func (f *File) setAppXML() {
165
	f.saveFileList("docProps/app.xml", templateDocpropsApp)
xurime's avatar
xurime 已提交
166 167
}

168 169 170 171 172 173 174
// 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 已提交
175 176
func replaceRelationshipsNameSpace(workbookMarshal string) string {
	oldXmlns := `<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`
177
	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 已提交
178 179 180
	return strings.Replace(workbookMarshal, oldXmlns, newXmlns, -1)
}

181
// SetActiveSheet provides function to set default active sheet of XLSX by given
182
// index.
183
func (f *File) SetActiveSheet(index int) {
xurime's avatar
xurime 已提交
184 185 186
	if index < 1 {
		index = 1
	}
xurime's avatar
xurime 已提交
187
	index--
xurime's avatar
xurime 已提交
188
	content := f.workbookReader()
xurime's avatar
xurime 已提交
189 190 191 192 193 194 195 196
	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 已提交
197
	index++
xurime's avatar
xurime 已提交
198 199
	for i := 0; i < sheets; i++ {
		sheetIndex := i + 1
xurime's avatar
xurime 已提交
200
		xlsx := f.workSheetReader("sheet" + strconv.Itoa(sheetIndex))
xurime's avatar
xurime 已提交
201 202 203 204 205 206 207 208 209 210 211 212 213 214
		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
			}
		}
	}
215
	return
xurime's avatar
xurime 已提交
216 217
}

218 219
// GetActiveSheetIndex provides function to get active sheet of XLSX. If not
// found the active sheet will be return integer 0.
220 221
func (f *File) GetActiveSheetIndex() int {
	buffer := bytes.Buffer{}
xurime's avatar
xurime 已提交
222
	content := f.workbookReader()
223 224
	for _, v := range content.Sheets.Sheet {
		xlsx := xlsxWorksheet{}
225 226 227
		buffer.WriteString("xl/worksheets/sheet")
		buffer.WriteString(strings.TrimPrefix(v.ID, "rId"))
		buffer.WriteString(".xml")
228 229 230
		xml.Unmarshal([]byte(f.readXML(buffer.String())), &xlsx)
		for _, sheetView := range xlsx.SheetViews.SheetView {
			if sheetView.TabSelected {
231 232
				ID, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId"))
				return ID
233
			}
234
		}
235
		buffer.Reset()
236
	}
237
	return 0
238 239
}

240 241 242 243 244
// 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.
245
func (f *File) SetSheetName(oldName, newName string) {
246 247
	oldName = trimSheetName(oldName)
	newName = trimSheetName(newName)
xurime's avatar
xurime 已提交
248
	content := f.workbookReader()
249 250 251 252 253 254 255
	for k, v := range content.Sheets.Sheet {
		if v.Name == oldName {
			content.Sheets.Sheet[k].Name = newName
		}
	}
}

256
// GetSheetName provides function to get sheet name of XLSX by given worksheet
257
// index. If given sheet index is invalid, will return an empty string.
258
func (f *File) GetSheetName(index int) string {
xurime's avatar
xurime 已提交
259
	content := f.workbookReader()
260 261 262 263 264 265 266 267 268
	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
				}
			}
269 270
		}
	}
271
	return ""
272 273
}

274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
// 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
}

292
// GetSheetMap provides function to get sheet map of XLSX. For example:
293
//
294
//    xlsx, err := excelize.OpenFile("./Workbook.xlsx")
295 296 297 298 299 300 301 302 303
//    if err != nil {
//        fmt.Println(err)
//        os.Exit(1)
//    }
//    for k, v := range xlsx.GetSheetMap()
//        fmt.Println(k, v)
//    }
//
func (f *File) GetSheetMap() map[int]string {
xurime's avatar
xurime 已提交
304
	content := f.workbookReader()
305
	rels := f.workbookRelsReader()
306 307
	sheetMap := map[int]string{}
	for _, v := range content.Sheets.Sheet {
308 309 310 311 312 313
		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
			}
		}
314 315
	}
	return sheetMap
xurime's avatar
xurime 已提交
316
}
317 318 319 320 321 322 323 324 325

// 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
	}
326
	ext, ok := supportImageTypes[path.Ext(picture)]
327 328 329 330 331 332 333 334 335 336
	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
}
337

338
// DeleteSheet provides function to delete worksheet in a workbook by given
339 340 341 342 343
// 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 已提交
344
	content := f.workbookReader()
345 346 347 348 349 350 351
	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"
352 353
		target := f.deleteSheetFromWorkbookRels(v.ID)
		f.deleteSheetFromContentTypes(target)
354 355 356 357 358 359 360 361 362 363 364 365
		_, 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 已提交
366
		f.SheetCount--
367 368 369
	}
}

370
// deleteSheetFromWorkbookRels provides function to remove worksheet
371 372
// relationships by given relationships ID in the file
// xl/_rels/workbook.xml.rels.
373
func (f *File) deleteSheetFromWorkbookRels(rID string) string {
xurime's avatar
xurime 已提交
374
	content := f.workbookRelsReader()
375 376 377 378 379 380 381 382 383 384
	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 ""
}

385
// deleteSheetFromContentTypes provides function to remove worksheet
386
// relationships by given target name in the file [Content_Types].xml.
387
func (f *File) deleteSheetFromContentTypes(target string) {
xurime's avatar
xurime 已提交
388
	content := f.contentTypesReader()
389 390 391 392 393 394 395
	for k, v := range content.Overrides {
		if v.PartName != "/xl/"+target {
			continue
		}
		content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
	}
}
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420

// 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))
421
	worksheet := *sheet
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
	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]
	}
}
437 438 439

// HideSheet provides function to hide worksheet by given name. A workbook must
// contain at least one visible worksheet. If the given worksheet has been
xurime's avatar
xurime 已提交
440 441 442 443 444 445 446 447
// activated, this setting will be invalidated. Sheet state values as defined by
// http://msdn.microsoft.com/en-
// us/library/office/documentformat.openxml.spreadsheet.sheetstatevalues.aspx
//
//    visible
//    hidden
//    veryHidden
//
448 449 450 451 452
func (f *File) HideSheet(name string) {
	name = trimSheetName(name)
	content := f.workbookReader()
	count := 0
	for _, v := range content.Sheets.Sheet {
453
		if v.State != "hidden" {
454 455 456 457 458 459 460 461 462 463 464
			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 {
465
			content.Sheets.Sheet[k].State = "hidden"
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
		}
	}
}

// UnhideSheet provides function to unhide worksheet by given name.
func (f *File) UnhideSheet(name string) {
	name = trimSheetName(name)
	content := f.workbookReader()
	for k, v := range content.Sheets.Sheet {
		if v.Name == name {
			content.Sheets.Sheet[k].State = ""
		}
	}
}

// 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
}