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

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

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
// parseFormatPictureSet provides function to parse the format settings of the
// picture with default value.
func parseFormatPictureSet(formatSet string) *xlsxFormatPicture {
	format := xlsxFormatPicture{
		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:
37
//
xurime's avatar
xurime 已提交
38
//    package main
39
//
xurime's avatar
xurime 已提交
40 41 42 43 44 45
//    import (
//        "fmt"
//        "os"
//        _ "image/gif"
//        _ "image/jpeg"
//        _ "image/png"
46
//
xurime's avatar
xurime 已提交
47 48
//        "github.com/Luxurioust/excelize"
//    )
49
//
xurime's avatar
xurime 已提交
50
//    func main() {
51 52
//        xlsx := excelize.CreateFile()
//        // Insert a picture.
53
//        err := xlsx.AddPicture("Sheet1", "A2", "/tmp/image1.jpg", "")
54 55 56
//        if err != nil {
//            fmt.Println(err)
//        }
57
//        // Insert a picture to sheet with scaling.
58
//        err = xlsx.AddPicture("Sheet1", "D2", "/tmp/image1.png", `{"x_scale": 0.5, "y_scale": 0.5}`)
59 60 61
//        if err != nil {
//            fmt.Println(err)
//        }
62 63
//        // Insert a picture offset in the cell with printing support.
//        err = xlsx.AddPicture("Sheet1", "H2", "/tmp/image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`)
64 65 66 67 68 69 70 71
//        if err != nil {
//            fmt.Println(err)
//        }
//        err = xlsx.WriteTo("/tmp/Workbook.xlsx")
//        if err != nil {
//            fmt.Println(err)
//            os.Exit(1)
//        }
72 73
//    }
//
74
func (f *File) AddPicture(sheet, cell, picture, format string) error {
75
	var supportTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png"}
76 77 78 79 80 81 82 83 84
	var err error
	// Check picture exists first.
	if _, err = os.Stat(picture); os.IsNotExist(err) {
		return err
	}
	ext, ok := supportTypes[path.Ext(picture)]
	if !ok {
		return errors.New("Unsupported image extension")
	}
85 86
	readFile, _ := os.Open(picture)
	image, _, err := image.DecodeConfig(readFile)
87
	_, file := filepath.Split(picture)
88
	formatSet := parseFormatPictureSet(format)
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
	// Read sheet data.
	var xlsx xlsxWorksheet
	name := "xl/worksheets/" + strings.ToLower(sheet) + ".xml"
	xml.Unmarshal([]byte(f.readXML(name)), &xlsx)
	// 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"
	sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"

	var drawingRID int
	if xlsx.Drawing != nil {
		// The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml.
		sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID)
		drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml"))
		drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1)
	} else {
		// Add first picture for given sheet.
107
		rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "")
108 109 110
		f.addSheetDrawing(sheet, rID)
	}
	drawingRID = f.addDrawingRelationships(drawingID, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext)
111
	f.addDrawing(sheet, drawingXML, cell, file, image.Width, image.Height, drawingRID, formatSet)
112 113 114 115 116
	f.addMedia(picture, ext)
	f.addDrawingContentTypePart(drawingID)
	return err
}

117 118 119
// addSheetRelationships provides function to add
// xl/worksheets/_rels/sheet%d.xml.rels by given sheet name, relationship type
// and target.
120
func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) int {
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
	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{
136 137 138 139
		ID:         ID.String(),
		Type:       relType,
		Target:     target,
		TargetMode: targetMode,
140 141 142 143 144 145 146 147 148
	})
	output, err := xml.Marshal(sheetRels)
	if err != nil {
		fmt.Println(err)
	}
	f.saveFileList(rels, string(output))
	return rID
}

149 150
// addSheetDrawing provides function to add drawing element to
// xl/worksheets/sheet%d.xml by given sheet name and relationship index.
151 152 153 154 155 156 157 158 159 160 161
func (f *File) addSheetDrawing(sheet string, rID int) {
	var xlsx xlsxWorksheet
	name := "xl/worksheets/" + strings.ToLower(sheet) + ".xml"
	xml.Unmarshal([]byte(f.readXML(name)), &xlsx)
	xlsx.Drawing = &xlsxDrawing{
		RID: "rId" + strconv.Itoa(rID),
	}
	output, err := xml.Marshal(xlsx)
	if err != nil {
		fmt.Println(err)
	}
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
	f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpace(string(output)))
}

// 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) {
	var xlsx xlsxWorksheet
	name := "xl/worksheets/" + strings.ToLower(sheet) + ".xml"
	xml.Unmarshal([]byte(f.readXML(name)), &xlsx)
	xlsx.Picture = &xlsxPicture{
		RID: "rId" + strconv.Itoa(rID),
	}
	output, err := xml.Marshal(xlsx)
	if err != nil {
		fmt.Println(err)
	}
	f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpace(string(output)))
179 180
}

181 182
// countDrawings provides function to get drawing files count storage in the
// folder xl/drawings.
183 184 185 186 187 188 189 190 191 192
func (f *File) countDrawings() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/drawings/drawing") {
			count++
		}
	}
	return count
}

193 194 195 196
// addDrawing provides function to add picture by given drawingXML, xAxis,
// yAxis, file name and relationship index. In order to solve the problem that
// the label structure is changed after serialization and deserialization, two
// different structures: decodeWsDr and encodeWsDr are defined.
197
func (f *File) addDrawing(sheet, drawingXML, cell, file string, width, height, rID int, formatSet *xlsxFormatPicture) {
198 199 200 201 202
	cell = strings.ToUpper(cell)
	fromCol := string(strings.Map(letterOnlyMapF, cell))
	fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell))
	row := fromRow - 1
	col := titleToNumber(fromCol)
203 204 205
	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)
206 207 208 209 210 211 212 213 214
	content := encodeWsDr{}
	content.WsDr.A = NameSpaceDrawingML
	content.WsDr.Xdr = NameSpaceSpreadSheetDrawing
	cNvPrID := 1
	_, ok := f.XLSX[drawingXML]
	if ok { // Append Model
		decodeWsDr := decodeWsDr{}
		xml.Unmarshal([]byte(f.readXML(drawingXML)), &decodeWsDr)
		cNvPrID = len(decodeWsDr.TwoCellAnchor) + 1
215 216 217 218 219 220
		for _, v := range decodeWsDr.OneCellAnchor {
			content.WsDr.OneCellAnchor = append(content.WsDr.OneCellAnchor, &xlsxCellAnchor{
				EditAs:       v.EditAs,
				GraphicFrame: v.Content,
			})
		}
221
		for _, v := range decodeWsDr.TwoCellAnchor {
222
			content.WsDr.TwoCellAnchor = append(content.WsDr.TwoCellAnchor, &xlsxCellAnchor{
223 224 225 226 227
				EditAs:       v.EditAs,
				GraphicFrame: v.Content,
			})
		}
	}
228
	twoCellAnchor := xlsxCellAnchor{}
229 230
	twoCellAnchor.EditAs = "oneCell"
	from := xlsxFrom{}
231
	from.Col = colStart
232
	from.ColOff = formatSet.OffsetX * EMU
233
	from.Row = rowStart
234
	from.RowOff = formatSet.OffsetY * EMU
235
	to := xlsxTo{}
236 237 238 239
	to.Col = colEnd
	to.ColOff = x2 * EMU
	to.Row = rowEnd
	to.RowOff = y2 * EMU
240 241 242
	twoCellAnchor.From = &from
	twoCellAnchor.To = &to
	pic := xlsxPic{}
243
	pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect
244 245 246 247 248 249 250 251 252
	pic.NvPicPr.CNvPr.ID = cNvPrID
	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
	twoCellAnchor.ClientData = &xlsxClientData{
253 254
		FLocksWithSheet:  formatSet.FLocksWithSheet,
		FPrintsWithSheet: formatSet.FPrintsWithSheet,
255 256 257 258 259 260 261 262 263 264 265 266
	}
	content.WsDr.TwoCellAnchor = append(content.WsDr.TwoCellAnchor, &twoCellAnchor)
	output, err := xml.Marshal(content)
	if err != nil {
		fmt.Println(err)
	}
	// Create replacer with pairs as arguments and replace all pairs.
	r := strings.NewReplacer("<encodeWsDr>", "", "</encodeWsDr>", "")
	result := r.Replace(string(output))
	f.saveFileList(drawingXML, result)
}

267 268 269
// 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.
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
func (f *File) addDrawingRelationships(index int, relType string, target string) int {
	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,
	})
	output, err := xml.Marshal(drawingRels)
	if err != nil {
		fmt.Println(err)
	}
	f.saveFileList(rels, string(output))
	return rID
}

298 299
// countMedia provides function to get media files count storage in the folder
// xl/media/image.
300 301 302 303 304 305 306 307 308 309
func (f *File) countMedia() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/media/image") {
			count++
		}
	}
	return count
}

310 311
// addMedia provides function to add picture into folder xl/media/image by given
// file name and extension name.
312 313 314 315 316 317 318
func (f *File) addMedia(file string, ext string) {
	count := f.countMedia()
	dat, _ := ioutil.ReadFile(file)
	media := "xl/media/image" + strconv.Itoa(count+1) + ext
	f.XLSX[media] = string(dat)
}

319 320
// setContentTypePartImageExtensions provides function to set the content type
// for relationship parts and the Main Document part.
321
func (f *File) setContentTypePartImageExtensions() {
322
	var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false}
323
	var content xlsxTypes
324
	xml.Unmarshal([]byte(f.readXML("[Content_Types].xml")), &content)
325 326 327 328 329 330 331 332 333 334 335 336 337 338
	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,
			})
		}
	}
339 340 341 342 343 344 345 346 347 348 349
	output, _ := xml.Marshal(content)
	f.saveFileList("[Content_Types].xml", string(output))
}

// addDrawingContentTypePart provides function to add image part relationships
// in http://purl.oclc.org/ooxml/officeDocument/relationships/image and
// appropriate content type.
func (f *File) addDrawingContentTypePart(index int) {
	f.setContentTypePartImageExtensions()
	var content xlsxTypes
	xml.Unmarshal([]byte(f.readXML("[Content_Types].xml")), &content)
350 351 352 353 354 355 356 357 358 359 360 361
	for _, v := range content.Overrides {
		if v.PartName == "/xl/drawings/drawing"+strconv.Itoa(index)+".xml" {
			output, _ := xml.Marshal(content)
			f.saveFileList(`[Content_Types].xml`, string(output))
			return
		}
	}
	content.Overrides = append(content.Overrides, xlsxOverride{
		PartName:    "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
		ContentType: "application/vnd.openxmlformats-officedocument.drawing+xml",
	})
	output, _ := xml.Marshal(content)
362
	f.saveFileList("[Content_Types].xml", string(output))
363 364
}

365 366 367
// getSheetRelationshipsTargetByID provides function to get Target attribute
// value in xl/worksheets/_rels/sheet%d.xml.rels by given sheet name and
// relationship index.
368 369 370 371 372 373 374 375 376 377 378
func (f *File) getSheetRelationshipsTargetByID(sheet string, rID string) string {
	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 ""
}