picture.go 13.7 KB
Newer Older
1 2 3 4
package excelize

import (
	"bytes"
5
	"encoding/json"
6 7
	"encoding/xml"
	"errors"
8
	"image"
9 10 11 12 13 14 15 16
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
)

17 18
// parseFormatPictureSet provides function to parse the format settings of the
// picture with default value.
19 20
func parseFormatPictureSet(formatSet string) *formatPicture {
	format := formatPicture{
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
		FPrintsWithSheet: true,
		FLocksWithSheet:  false,
		NoChangeAspect:   false,
		OffsetX:          0,
		OffsetY:          0,
		XScale:           1.0,
		YScale:           1.0,
	}
	json.Unmarshal([]byte(formatSet), &format)
	return &format
}

// AddPicture provides the method to add picture in a sheet by given picture
// format set (such as offset, scale, aspect ratio setting and print settings)
// and file path. For example:
36
//
xurime's avatar
xurime 已提交
37
//    package main
38
//
xurime's avatar
xurime 已提交
39 40 41 42 43 44
//    import (
//        "fmt"
//        "os"
//        _ "image/gif"
//        _ "image/jpeg"
//        _ "image/png"
45
//
xurime's avatar
xurime 已提交
46 47
//        "github.com/Luxurioust/excelize"
//    )
48
//
xurime's avatar
xurime 已提交
49
//    func main() {
50
//        xlsx := excelize.NewFile()
51
//        // Insert a picture.
52
//        err := xlsx.AddPicture("Sheet1", "A2", "./image1.jpg", "")
53 54 55
//        if err != nil {
//            fmt.Println(err)
//        }
56
//        // Insert a picture to sheet with scaling.
57
//        err = xlsx.AddPicture("Sheet1", "D2", "./image1.png", `{"x_scale": 0.5, "y_scale": 0.5}`)
58 59 60
//        if err != nil {
//            fmt.Println(err)
//        }
61
//        // Insert a picture offset in the cell with printing support.
62
//        err = xlsx.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`)
63 64 65
//        if err != nil {
//            fmt.Println(err)
//        }
66
//        err = xlsx.SaveAs("./Workbook.xlsx")
67 68 69 70
//        if err != nil {
//            fmt.Println(err)
//            os.Exit(1)
//        }
71 72
//    }
//
73
func (f *File) AddPicture(sheet, cell, picture, format string) error {
74 75 76 77 78
	var err error
	// Check picture exists first.
	if _, err = os.Stat(picture); os.IsNotExist(err) {
		return err
	}
79
	ext, ok := supportImageTypes[path.Ext(picture)]
80 81 82
	if !ok {
		return errors.New("Unsupported image extension")
	}
83 84
	readFile, _ := os.Open(picture)
	image, _, err := image.DecodeConfig(readFile)
85
	_, file := filepath.Split(picture)
86
	formatSet := parseFormatPictureSet(format)
87
	// Read sheet data.
xurime's avatar
xurime 已提交
88
	xlsx := f.workSheetReader(sheet)
89 90 91 92
	// Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder.
	drawingID := f.countDrawings() + 1
	pictureID := f.countMedia() + 1
	drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
xurime's avatar
xurime 已提交
93 94
	drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML)
	drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext)
95
	f.addDrawingPicture(sheet, drawingXML, cell, file, image.Width, image.Height, drawingRID, formatSet)
96
	f.addMedia(picture, ext)
xurime's avatar
xurime 已提交
97
	f.addContentTypePart(drawingID, "drawings")
98 99 100
	return err
}

101 102 103
// addSheetRelationships provides function to add
// xl/worksheets/_rels/sheet%d.xml.rels by given sheet name, relationship type
// and target.
104
func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) int {
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
	var rels = "xl/worksheets/_rels/" + strings.ToLower(sheet) + ".xml.rels"
	var sheetRels xlsxWorkbookRels
	var rID = 1
	var ID bytes.Buffer
	ID.WriteString("rId")
	ID.WriteString(strconv.Itoa(rID))
	_, ok := f.XLSX[rels]
	if ok {
		ID.Reset()
		xml.Unmarshal([]byte(f.readXML(rels)), &sheetRels)
		rID = len(sheetRels.Relationships) + 1
		ID.WriteString("rId")
		ID.WriteString(strconv.Itoa(rID))
	}
	sheetRels.Relationships = append(sheetRels.Relationships, xlsxWorkbookRelation{
120 121 122 123
		ID:         ID.String(),
		Type:       relType,
		Target:     target,
		TargetMode: targetMode,
124
	})
125
	output, _ := xml.Marshal(sheetRels)
126 127 128 129
	f.saveFileList(rels, string(output))
	return rID
}

130 131 132 133 134 135 136 137 138
// addSheetLegacyDrawing provides function to add legacy drawing element to
// xl/worksheets/sheet%d.xml by given sheet name and relationship index.
func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
	xlsx := f.workSheetReader(sheet)
	xlsx.LegacyDrawing = &xlsxLegacyDrawing{
		RID: "rId" + strconv.Itoa(rID),
	}
}

139 140
// addSheetDrawing provides function to add drawing element to
// xl/worksheets/sheet%d.xml by given sheet name and relationship index.
141
func (f *File) addSheetDrawing(sheet string, rID int) {
xurime's avatar
xurime 已提交
142
	xlsx := f.workSheetReader(sheet)
143 144 145
	xlsx.Drawing = &xlsxDrawing{
		RID: "rId" + strconv.Itoa(rID),
	}
146 147 148 149 150
}

// addSheetPicture provides function to add picture element to
// xl/worksheets/sheet%d.xml by given sheet name and relationship index.
func (f *File) addSheetPicture(sheet string, rID int) {
xurime's avatar
xurime 已提交
151
	xlsx := f.workSheetReader(sheet)
152 153 154
	xlsx.Picture = &xlsxPicture{
		RID: "rId" + strconv.Itoa(rID),
	}
155 156
}

157 158
// countDrawings provides function to get drawing files count storage in the
// folder xl/drawings.
159 160 161 162 163 164 165 166 167 168
func (f *File) countDrawings() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/drawings/drawing") {
			count++
		}
	}
	return count
}

169 170
// addDrawingPicture provides function to add picture by given sheet,
// drawingXML, cell, file name, width, height relationship index and format
xurime's avatar
xurime 已提交
171
// sets.
172
func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID int, formatSet *formatPicture) {
173 174 175 176
	cell = strings.ToUpper(cell)
	fromCol := string(strings.Map(letterOnlyMapF, cell))
	fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell))
	row := fromRow - 1
177
	col := TitleToNumber(fromCol)
178 179 180
	width = int(float64(width) * formatSet.XScale)
	height = int(float64(height) * formatSet.YScale)
	colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height)
181 182 183
	content := xlsxWsDr{}
	content.A = NameSpaceDrawingML
	content.Xdr = NameSpaceDrawingMLSpreadSheet
184
	cNvPrID := f.drawingParser(drawingXML, &content)
185
	twoCellAnchor := xdrCellAnchor{}
186 187
	twoCellAnchor.EditAs = "oneCell"
	from := xlsxFrom{}
188
	from.Col = colStart
189
	from.ColOff = formatSet.OffsetX * EMU
190
	from.Row = rowStart
191
	from.RowOff = formatSet.OffsetY * EMU
192
	to := xlsxTo{}
193 194 195 196
	to.Col = colEnd
	to.ColOff = x2 * EMU
	to.Row = rowEnd
	to.RowOff = y2 * EMU
197 198 199
	twoCellAnchor.From = &from
	twoCellAnchor.To = &to
	pic := xlsxPic{}
200
	pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect
201
	pic.NvPicPr.CNvPr.ID = f.countCharts() + f.countMedia() + 1
202 203 204 205 206 207 208
	pic.NvPicPr.CNvPr.Descr = file
	pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID)
	pic.BlipFill.Blip.R = SourceRelationship
	pic.BlipFill.Blip.Embed = "rId" + strconv.Itoa(rID)
	pic.SpPr.PrstGeom.Prst = "rect"

	twoCellAnchor.Pic = &pic
209
	twoCellAnchor.ClientData = &xdrClientData{
210 211
		FLocksWithSheet:  formatSet.FLocksWithSheet,
		FPrintsWithSheet: formatSet.FPrintsWithSheet,
212
	}
213 214 215
	content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
	output, _ := xml.Marshal(content)
	f.saveFileList(drawingXML, string(output))
216 217
}

218 219 220
// addDrawingRelationships provides function to add image part relationships in
// the file xl/drawings/_rels/drawing%d.xml.rels by given drawing index,
// relationship type and target.
221
func (f *File) addDrawingRelationships(index int, relType, target string) int {
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
	var rels = "xl/drawings/_rels/drawing" + strconv.Itoa(index) + ".xml.rels"
	var drawingRels xlsxWorkbookRels
	var rID = 1
	var ID bytes.Buffer
	ID.WriteString("rId")
	ID.WriteString(strconv.Itoa(rID))
	_, ok := f.XLSX[rels]
	if ok {
		ID.Reset()
		xml.Unmarshal([]byte(f.readXML(rels)), &drawingRels)
		rID = len(drawingRels.Relationships) + 1
		ID.WriteString("rId")
		ID.WriteString(strconv.Itoa(rID))
	}
	drawingRels.Relationships = append(drawingRels.Relationships, xlsxWorkbookRelation{
		ID:     ID.String(),
		Type:   relType,
		Target: target,
	})
241
	output, _ := xml.Marshal(drawingRels)
242 243 244 245
	f.saveFileList(rels, string(output))
	return rID
}

246 247
// countMedia provides function to get media files count storage in the folder
// xl/media/image.
248 249 250 251 252 253 254 255 256 257
func (f *File) countMedia() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/media/image") {
			count++
		}
	}
	return count
}

258 259
// addMedia provides function to add picture into folder xl/media/image by given
// file name and extension name.
260
func (f *File) addMedia(file, ext string) {
261 262 263 264 265 266
	count := f.countMedia()
	dat, _ := ioutil.ReadFile(file)
	media := "xl/media/image" + strconv.Itoa(count+1) + ext
	f.XLSX[media] = string(dat)
}

267 268
// setContentTypePartImageExtensions provides function to set the content type
// for relationship parts and the Main Document part.
269
func (f *File) setContentTypePartImageExtensions() {
270
	var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false}
xurime's avatar
xurime 已提交
271
	content := f.contentTypesReader()
272 273 274 275 276 277 278 279 280 281 282 283 284 285
	for _, v := range content.Defaults {
		_, ok := imageTypes[v.Extension]
		if ok {
			imageTypes[v.Extension] = true
		}
	}
	for k, v := range imageTypes {
		if !v {
			content.Defaults = append(content.Defaults, xlsxDefault{
				Extension:   k,
				ContentType: "image/" + k,
			})
		}
	}
286 287
}

288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
// setContentTypePartVMLExtensions provides function to set the content type
// for relationship parts and the Main Document part.
func (f *File) setContentTypePartVMLExtensions() {
	vml := false
	content := f.contentTypesReader()
	for _, v := range content.Defaults {
		if v.Extension == "vml" {
			vml = true
		}
	}
	if !vml {
		content.Defaults = append(content.Defaults, xlsxDefault{
			Extension:   "vml",
			ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing",
		})
	}
}

xurime's avatar
xurime 已提交
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
// addContentTypePart provides function to add content type part relationships
// in the file [Content_Types].xml by given index.
func (f *File) addContentTypePart(index int, contentType string) {
	setContentType := map[string]func(){
		"comments": f.setContentTypePartVMLExtensions,
		"drawings": f.setContentTypePartImageExtensions,
	}
	partNames := map[string]string{
		"chart":    "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
		"comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
		"drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
		"table":    "/xl/tables/table" + strconv.Itoa(index) + ".xml",
	}
	contentTypes := map[string]string{
		"chart":    "application/vnd.openxmlformats-officedocument.drawingml.chart+xml",
		"comments": "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml",
		"drawings": "application/vnd.openxmlformats-officedocument.drawing+xml",
		"table":    "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml",
	}
	s, ok := setContentType[contentType]
	if ok {
		s()
328
	}
329 330
	content := f.contentTypesReader()
	for _, v := range content.Overrides {
xurime's avatar
xurime 已提交
331
		if v.PartName == partNames[contentType] {
332 333 334 335
			return
		}
	}
	content.Overrides = append(content.Overrides, xlsxOverride{
xurime's avatar
xurime 已提交
336 337
		PartName:    partNames[contentType],
		ContentType: contentTypes[contentType],
338 339 340
	})
}

341 342 343
// getSheetRelationshipsTargetByID provides function to get Target attribute
// value in xl/worksheets/_rels/sheet%d.xml.rels by given sheet name and
// relationship index.
344
func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
345 346 347 348 349 350 351 352 353 354
	var rels = "xl/worksheets/_rels/" + strings.ToLower(sheet) + ".xml.rels"
	var sheetRels xlsxWorkbookRels
	xml.Unmarshal([]byte(f.readXML(rels)), &sheetRels)
	for _, v := range sheetRels.Relationships {
		if v.ID == rID {
			return v.Target
		}
	}
	return ""
}
355 356 357 358 359

// GetPicture provides function to get picture base name and raw content embed
// in XLSX by given worksheet and cell name. This function returns the file name
// in XLSX and file contents as []byte data types. For example:
//
360
//    xlsx, err := excelize.OpenFile("./Workbook.xlsx")
361 362 363 364 365 366 367 368
//    if err != nil {
//        fmt.Println(err)
//        os.Exit(1)
//    }
//    file, raw := xlsx.GetPicture("Sheet1", "A2")
//    if file == "" {
//        os.Exit(1)
//    }
xurime's avatar
xurime 已提交
369 370
//    err := ioutil.WriteFile(file, raw, 0644)
//    if err != nil {
371 372
//        fmt.Println(err)
//        os.Exit(1)
xurime's avatar
xurime 已提交
373
//    }
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
//
func (f *File) GetPicture(sheet, cell string) (string, []byte) {
	xlsx := f.workSheetReader(sheet)
	if xlsx.Drawing == nil {
		return "", []byte{}
	}
	target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID)
	drawingXML := strings.Replace(target, "..", "xl", -1)

	_, ok := f.XLSX[drawingXML]
	if !ok {
		return "", []byte{}
	}
	decodeWsDr := decodeWsDr{}
	xml.Unmarshal([]byte(f.readXML(drawingXML)), &decodeWsDr)

	cell = strings.ToUpper(cell)
	fromCol := string(strings.Map(letterOnlyMapF, cell))
	fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell))
	row := fromRow - 1
394
	col := TitleToNumber(fromCol)
395 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 421 422 423 424 425 426 427 428 429 430 431 432 433

	drawingRelationships := strings.Replace(strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1)

	for _, anchor := range decodeWsDr.TwoCellAnchor {
		decodeTwoCellAnchor := decodeTwoCellAnchor{}
		xml.Unmarshal([]byte("<decodeTwoCellAnchor>"+anchor.Content+"</decodeTwoCellAnchor>"), &decodeTwoCellAnchor)
		if decodeTwoCellAnchor.From == nil || decodeTwoCellAnchor.Pic == nil {
			continue
		}
		if decodeTwoCellAnchor.From.Col == col && decodeTwoCellAnchor.From.Row == row {
			xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed)
			_, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)]
			if !ok {
				continue
			}

			return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)])
		}
	}
	return "", []byte{}
}

// getDrawingRelationships provides function to get drawing relationships from
// xl/drawings/_rels/drawing%s.xml.rels by given file name and relationship ID.
func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation {
	_, ok := f.XLSX[rels]
	if !ok {
		return nil
	}
	var drawingRels xlsxWorkbookRels
	xml.Unmarshal([]byte(f.readXML(rels)), &drawingRels)
	for _, v := range drawingRels.Relationships {
		if v.ID != rID {
			continue
		}
		return &v
	}
	return nil
}