picture.go 9.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
package excelize

import (
	"bytes"
	"encoding/xml"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
)

16 17
// AddPicture provide the method to add picture in a sheet by given xAxis, yAxis
// and file path. For example:
18 19 20 21 22 23 24 25 26 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
//
//    xlsx := excelize.CreateFile()
//    err := xlsx.AddPicture("Sheet1", "A2", "H9", "./image.jpg")
//    if err != nil {
//        fmt.Println(err)
//        os.Exit(1)
//    }
//
func (f *File) AddPicture(sheet string, xAxis string, yAxis string, picture string) error {
	var supportTypes = map[string]string{".bmp": ".jpeg", ".gif": ".gif", ".ico": ".png", ".tif": ".tiff", ".tiff": ".tiff", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png"}
	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")
	}
	_, file := filepath.Split(picture)
	// 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.
		rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML)
		f.addSheetDrawing(sheet, rID)
	}
	drawingRID = f.addDrawingRelationships(drawingID, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext)
	f.addDrawing(drawingXML, xAxis, yAxis, file, drawingRID)
	f.addMedia(picture, ext)
	f.addDrawingContentTypePart(drawingID)
	return err
}

66 67 68
// addSheetRelationships provides function to add
// xl/worksheets/_rels/sheet%d.xml.rels by given sheet name, relationship type
// and target.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
func (f *File) addSheetRelationships(sheet string, relType string, target string) int {
	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{
		ID:     ID.String(),
		Type:   relType,
		Target: target,
	})
	output, err := xml.Marshal(sheetRels)
	if err != nil {
		fmt.Println(err)
	}
	f.saveFileList(rels, string(output))
	return rID
}

97 98
// addSheetDrawing provides function to add drawing element to
// xl/worksheets/sheet%d.xml by given sheet name and relationship index.
99 100 101 102 103 104 105 106 107 108 109 110 111 112
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)
	}
	f.saveFileList(name, string(output))
}

113 114
// countDrawings provides function to get drawing files count storage in the
// folder xl/drawings.
115 116 117 118 119 120 121 122 123 124
func (f *File) countDrawings() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/drawings/drawing") {
			count++
		}
	}
	return count
}

125 126 127 128
// 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.
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
func (f *File) addDrawing(drawingXML string, xAxis string, yAxis string, file string, rID int) {
	xAxis = strings.ToUpper(xAxis)
	fromCol := string(strings.Map(letterOnlyMapF, xAxis))
	fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, xAxis))
	fromXAxis := fromRow - 1
	fromYAxis := titleToNumber(fromCol)
	yAxis = strings.ToUpper(yAxis)
	ToCol := string(strings.Map(letterOnlyMapF, yAxis))
	ToRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, yAxis))
	ToXAxis := ToRow - 1
	ToYAxis := titleToNumber(ToCol)
	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{}
	from.Col = fromYAxis
	from.Row = fromXAxis
	to := xlsxTo{}
	to.Col = ToYAxis
	to.Row = ToXAxis
	twoCellAnchor.From = &from
	twoCellAnchor.To = &to
	pic := xlsxPic{}
	pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = false
	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{
		FLocksWithSheet:  false,
		FPrintsWithSheet: false,
	}
	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)
}

191 192 193
// 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.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
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
}

222 223
// countMedia provides function to get media files count storage in the folder
// xl/media/image.
224 225 226 227 228 229 230 231 232 233
func (f *File) countMedia() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/media/image") {
			count++
		}
	}
	return count
}

234 235
// addMedia provides function to add picture into folder xl/media/image by given
// file name and extension name.
236 237 238 239 240 241 242
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)
}

243 244 245
// addDrawingContentTypePart provides function to add image part relationships
// in http://purl.oclc.org/ooxml/officeDocument/relationships/image and
// appropriate content type.
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
func (f *File) addDrawingContentTypePart(index int) {
	var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false}
	var content xlsxTypes
	xml.Unmarshal([]byte(f.readXML(`[Content_Types].xml`)), &content)
	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,
			})
		}
	}
	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)
	f.saveFileList(`[Content_Types].xml`, string(output))
}

279 280 281
// getSheetRelationshipsTargetByID provides function to get Target attribute
// value in xl/worksheets/_rels/sheet%d.xml.rels by given sheet name and
// relationship index.
282 283 284 285 286 287 288 289 290 291 292
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 ""
}