picture.go 12.2 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 215 216 217 218 219 220 221 222 223 224
	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
		for _, v := range decodeWsDr.TwoCellAnchor {
			content.WsDr.TwoCellAnchor = append(content.WsDr.TwoCellAnchor, &xlsxTwoCellAnchor{
				EditAs:       v.EditAs,
				GraphicFrame: v.Content,
			})
		}
	}
	twoCellAnchor := xlsxTwoCellAnchor{}
	twoCellAnchor.EditAs = "oneCell"
	from := xlsxFrom{}
225
	from.Col = colStart
226
	from.ColOff = formatSet.OffsetX * EMU
227
	from.Row = rowStart
228
	from.RowOff = formatSet.OffsetY * EMU
229
	to := xlsxTo{}
230 231 232 233
	to.Col = colEnd
	to.ColOff = x2 * EMU
	to.Row = rowEnd
	to.RowOff = y2 * EMU
234 235 236
	twoCellAnchor.From = &from
	twoCellAnchor.To = &to
	pic := xlsxPic{}
237
	pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect
238 239 240 241 242 243 244 245 246
	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{
247 248
		FLocksWithSheet:  formatSet.FLocksWithSheet,
		FPrintsWithSheet: formatSet.FPrintsWithSheet,
249 250 251 252 253 254 255 256 257 258 259 260
	}
	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)
}

261 262 263
// 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.
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
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
}

292 293
// countMedia provides function to get media files count storage in the folder
// xl/media/image.
294 295 296 297 298 299 300 301 302 303
func (f *File) countMedia() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/media/image") {
			count++
		}
	}
	return count
}

304 305
// addMedia provides function to add picture into folder xl/media/image by given
// file name and extension name.
306 307 308 309 310 311 312
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)
}

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

359 360 361
// getSheetRelationshipsTargetByID provides function to get Target attribute
// value in xl/worksheets/_rels/sheet%d.xml.rels by given sheet name and
// relationship index.
362 363 364 365 366 367 368 369 370 371 372
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 ""
}