diff --git a/README.md b/README.md index f9ead958..0346fe80 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,21 @@ package main import ( "fmt" "os" + _ "image/gif" + _ "image/jpeg" + _ "image/png" "github.com/Luxurioust/excelize" ) func main() { xlsx := excelize.CreateFile() - err := xlsx.AddPicture("Sheet1", "A2", "H9", "/tmp/image.jpg") + // Insert a picture. + err := xlsx.AddPicture("Sheet1", "A2", "/tmp/image1.jpg", 0, 0, 1, 1) + // Insert a picture to sheet with scaling. + err = xlsx.AddPicture("Sheet1", "D2", "/tmp/image1.png", 0, 0, 0.5, 0.5) + // Insert a picture offset in the cell. + err = xlsx.AddPicture("Sheet1", "H2", "/tmp/image3.gif", 15, 10, 1, 1) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/col.go b/col.go index 7b583c52..60adab44 100644 --- a/col.go +++ b/col.go @@ -2,9 +2,18 @@ package excelize import ( "encoding/xml" + "math" + "strconv" "strings" ) +// Define the default cell size and EMU unit of measurement. +const ( + defaultColWidthPixels int = 64 + defaultRowHeightPixels int = 20 + EMU int = 9525 +) + // SetColWidth provides function to set the width of a single column or multiple // columns. For example: // @@ -41,3 +50,177 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) { output, _ := xml.Marshal(xlsx) f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpace(string(output))) } + +// positionObjectPixels calculate the vertices that define the position of a +// graphical object within the worksheet in pixels. +// +// +------------+------------+ +// | A | B | +// +-----+------------+------------+ +// | |(x1,y1) | | +// | 1 |(A1)._______|______ | +// | | | | | +// | | | | | +// +-----+----| OBJECT |-----+ +// | | | | | +// | 2 | |______________. | +// | | | (B2)| +// | | | (x2,y2)| +// +-----+------------+------------+ +// +// Example of an object that covers some of the area from cell A1 to B2. +// +// Based on the width and height of the object we need to calculate 8 vars: +// +// colStart, rowStart, colEnd, rowEnd, x1, y1, x2, y2. +// +// We also calculate the absolute x and y position of the top left vertex of +// the object. This is required for images. +// +// The width and height of the cells that the object occupies can be +// variable and have to be taken into account. +// +// The values of col_start and row_start are passed in from the calling +// function. The values of col_end and row_end are calculated by +// subtracting the width and height of the object from the width and +// height of the underlying cells. +// +// colStart # Col containing upper left corner of object. +// x1 # Distance to left side of object. +// +// rowStart # Row containing top left corner of object. +// y1 # Distance to top of object. +// +// colEnd # Col containing lower right corner of object. +// x2 # Distance to right side of object. +// +// rowEnd # Row containing bottom right corner of object. +// y2 # Distance to bottom of object. +// +// width # Width of object frame. +// height # Height of object frame. +// +// xAbs # Absolute distance to left side of object. +// yAbs # Absolute distance to top side of object. +// +func (f *File) positionObjectPixels(sheet string, colStart, rowStart, x1, y1, width, height int) (int, int, int, int, int, int, int, int) { + xAbs := 0 + yAbs := 0 + + // Calculate the absolute x offset of the top-left vertex. + for colID := 1; colID <= colStart; colID++ { + xAbs += f.getColWidth(sheet, colID) + } + xAbs += x1 + + // Calculate the absolute y offset of the top-left vertex. + // Store the column change to allow optimisations. + for rowID := 1; rowID <= rowStart; rowID++ { + yAbs += f.getRowHeight(sheet, rowID) + } + yAbs += y1 + + // Adjust start column for offsets that are greater than the col width. + for x1 >= f.getColWidth(sheet, colStart) { + x1 -= f.getColWidth(sheet, colStart) + colStart++ + } + + // Adjust start row for offsets that are greater than the row height. + for y1 >= f.getRowHeight(sheet, rowStart) { + y1 -= f.getRowHeight(sheet, rowStart) + rowStart++ + } + + // Initialise end cell to the same as the start cell. + colEnd := colStart + rowEnd := rowStart + + width += x1 + height += y1 + + // Subtract the underlying cell widths to find end cell of the object. + for width >= f.getColWidth(sheet, colEnd) { + colEnd++ + width -= f.getColWidth(sheet, colEnd) + } + + // Subtract the underlying cell heights to find end cell of the object. + for height >= f.getRowHeight(sheet, rowEnd) { + rowEnd++ + height -= f.getRowHeight(sheet, rowEnd) + } + + // The end vertices are whatever is left from the width and height. + x2 := width + y2 := height + return colStart, rowStart, xAbs, yAbs, colEnd, rowEnd, x2, y2 +} + +// getColWidth provides function to get column width in pixels by given sheet +// name and column index. +func (f *File) getColWidth(sheet string, col int) int { + var xlsx xlsxWorksheet + name := "xl/worksheets/" + strings.ToLower(sheet) + ".xml" + xml.Unmarshal([]byte(f.readXML(name)), &xlsx) + if xlsx.Cols != nil { + var width float64 + for _, v := range xlsx.Cols.Col { + if v.Min <= col && col <= v.Max { + width = v.Width + } + } + if width != 0 { + return int(convertColWidthToPixels(width)) + } + } + // Optimisation for when the column widths haven't changed. + return defaultColWidthPixels +} + +// getRowHeight provides function to get row height in pixels by given sheet +// name and row index. +func (f *File) getRowHeight(sheet string, row int) int { + var xlsx xlsxWorksheet + name := "xl/worksheets/" + strings.ToLower(sheet) + ".xml" + xml.Unmarshal([]byte(f.readXML(name)), &xlsx) + for _, v := range xlsx.SheetData.Row { + if v.R == row && v.Ht != "" { + ht, _ := strconv.ParseFloat(v.Ht, 64) + return int(convertRowHeightToPixels(ht)) + } + } + // Optimisation for when the row heights haven't changed. + return defaultRowHeightPixels +} + +// convertColWidthToPixels provieds function to convert the width of a cell from +// user's units to pixels. Excel rounds the column width to the nearest pixel. +// If the width hasn't been set by the user we use the default value. If the +// column is hidden it has a value of zero. +func convertColWidthToPixels(width float64) float64 { + var padding float64 = 5 + var pixels float64 + var maxDigitWidth float64 = 7 + if width == 0 { + return pixels + } + if width < 1 { + pixels = (width * 12) + 0.5 + return math.Ceil(pixels) + } + pixels = (width*maxDigitWidth + 0.5) + padding + return math.Ceil(pixels) +} + +// convertRowHeightToPixels provides function to convert the height of a cell +// from user's units to pixels. If the height hasn't been set by the user we use +// the default value. If the row is hidden it has a value of zero. +func convertRowHeightToPixels(height float64) float64 { + var pixels float64 + if height == 0 { + return pixels + } + pixels = math.Ceil(4.0 / 3.0 * height) + return pixels +} diff --git a/excelize_test.go b/excelize_test.go index 278912b3..8da4a999 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1,6 +1,9 @@ package excelize import ( + _ "image/gif" + _ "image/jpeg" + _ "image/png" "strconv" "testing" ) @@ -92,29 +95,22 @@ func TestAddPicture(t *testing.T) { t.Log(err) } // Test add picture to sheet. - err = xlsx.AddPicture("Sheet2", "I1", "L10", "./test/images/excel.jpg") + err = xlsx.AddPicture("Sheet2", "I9", "./test/images/excel.jpg", 140, 120, 1, 1) if err != nil { t.Log(err) } - err = xlsx.AddPicture("Sheet1", "F21", "G25", "./test/images/excel.png") - if err != nil { - t.Log(err) - } - err = xlsx.AddPicture("Sheet2", "L1", "O10", "./test/images/excel.bmp") - if err != nil { - t.Log(err) - } - err = xlsx.AddPicture("Sheet1", "G21", "H25", "./test/images/excel.ico") - if err != nil { - t.Log(err) - } - // Test add picture to sheet with unsupport file type. - err = xlsx.AddPicture("Sheet1", "G21", "H25", "./test/images/excel.icon") + // Test add picture to sheet with offset. + err = xlsx.AddPicture("Sheet1", "F21", "./test/images/excel.png", 10, 10, 1, 1) if err != nil { t.Log(err) } // Test add picture to sheet with invalid file path. - err = xlsx.AddPicture("Sheet1", "G21", "H25", "./test/Workbook1.xlsx") + err = xlsx.AddPicture("Sheet1", "G21", "./test/images/excel.icon", 0, 0, 1, 1) + if err != nil { + t.Log(err) + } + // Test add picture to sheet with unsupport file type. + err = xlsx.AddPicture("Sheet1", "G21", "./test/Workbook1.xlsx", 0, 0, 1, 1) if err != nil { t.Log(err) } @@ -160,12 +156,12 @@ func TestCreateFile(t *testing.T) { xlsx.SetCellInt("Sheet2", "A23", 56) xlsx.SetCellStr("SHEET1", "B20", "42") xlsx.SetActiveSheet(0) - // Test add picture to sheet. - err := xlsx.AddPicture("Sheet1", "H2", "K12", "./test/images/excel.gif") + // Test add picture to sheet with scaling. + err := xlsx.AddPicture("Sheet1", "H2", "./test/images/excel.gif", 0, 0, 0.5, 0.5) if err != nil { t.Log(err) } - err = xlsx.AddPicture("Sheet1", "C2", "F12", "./test/images/excel.tif") + err = xlsx.AddPicture("Sheet1", "C2", "./test/images/excel.png", 0, 0, 1, 1) if err != nil { t.Log(err) } @@ -176,13 +172,10 @@ func TestCreateFile(t *testing.T) { } func TestSetColWidth(t *testing.T) { - xlsx, err := OpenFile("./test/Workbook1.xlsx") - if err != nil { - t.Log(err) - } + xlsx := CreateFile() xlsx.SetColWidth("sheet1", "B", "A", 12) xlsx.SetColWidth("sheet1", "A", "B", 12) - err = xlsx.Save() + err := xlsx.WriteTo("./test/Workbook_4.xlsx") if err != nil { t.Log(err) } diff --git a/picture.go b/picture.go index bbd7cf39..e29ffd3f 100644 --- a/picture.go +++ b/picture.go @@ -5,6 +5,7 @@ import ( "encoding/xml" "errors" "fmt" + "image" "io/ioutil" "os" "path" @@ -13,18 +14,42 @@ import ( "strings" ) -// AddPicture provide the method to add picture in a sheet by given xAxis, yAxis +// AddPicture provides the method to add picture in a sheet by given xAxis, yAxis // and file path. For example: // -// xlsx := excelize.CreateFile() -// err := xlsx.AddPicture("Sheet1", "A2", "H9", "./image.jpg") -// if err != nil { -// fmt.Println(err) -// os.Exit(1) +// package main +// +// import ( +// "fmt" +// "os" +// _ "image/gif" +// _ "image/jpeg" +// _ "image/png" +// +// "github.com/Luxurioust/excelize" +// ) +// +// func main() { +// xlsx := excelize.CreateFile() +// // Insert a picture. +// err := xlsx.AddPicture("Sheet1", "A2", "/tmp/image1.jpg", 0, 0, 1, 1) +// // Insert a picture to sheet with scaling. +// err = xlsx.AddPicture("Sheet1", "D2", "/tmp/image1.png", 0, 0, 0.5, 0.5) +// // Insert a picture offset in the cell. +// err = xlsx.AddPicture("Sheet1", "H2", "/tmp/image3.gif", 15, 10, 1, 1) +// if err != nil { +// fmt.Println(err) +// os.Exit(1) +// } +// err = xlsx.WriteTo("/tmp/Workbook.xlsx") +// 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"} +func (f *File) AddPicture(sheet, cell, picture string, offsetX, offsetY int, xScale, yScale float64) error { + var supportTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png"} var err error // Check picture exists first. if _, err = os.Stat(picture); os.IsNotExist(err) { @@ -34,6 +59,8 @@ func (f *File) AddPicture(sheet string, xAxis string, yAxis string, picture stri if !ok { return errors.New("Unsupported image extension") } + readFile, _ := os.Open(picture) + image, _, err := image.DecodeConfig(readFile) _, file := filepath.Split(picture) // Read sheet data. var xlsx xlsxWorksheet @@ -57,7 +84,7 @@ func (f *File) AddPicture(sheet string, xAxis string, yAxis string, picture stri f.addSheetDrawing(sheet, rID) } drawingRID = f.addDrawingRelationships(drawingID, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext) - f.addDrawing(drawingXML, xAxis, yAxis, file, drawingRID) + f.addDrawing(sheet, drawingXML, cell, file, offsetX, offsetY, image.Width, image.Height, drawingRID, xScale, yScale) f.addMedia(picture, ext) f.addDrawingContentTypePart(drawingID) return err @@ -127,17 +154,15 @@ func (f *File) countDrawings() int { // 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. -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) +func (f *File) addDrawing(sheet, drawingXML, cell, file string, offsetX, offsetY, width, height, rID int, xScale, yScale float64) { + cell = strings.ToUpper(cell) + fromCol := string(strings.Map(letterOnlyMapF, cell)) + fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) + row := fromRow - 1 + col := titleToNumber(fromCol) + width = int(float64(width) * xScale) + height = int(float64(height) * yScale) + colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, offsetX, offsetY, width, height) content := encodeWsDr{} content.WsDr.A = NameSpaceDrawingML content.WsDr.Xdr = NameSpaceSpreadSheetDrawing @@ -157,11 +182,15 @@ func (f *File) addDrawing(drawingXML string, xAxis string, yAxis string, file st twoCellAnchor := xlsxTwoCellAnchor{} twoCellAnchor.EditAs = "oneCell" from := xlsxFrom{} - from.Col = fromYAxis - from.Row = fromXAxis + from.Col = colStart + from.ColOff = offsetX * EMU + from.Row = rowStart + from.RowOff = offsetY * EMU to := xlsxTo{} - to.Col = ToYAxis - to.Row = ToXAxis + to.Col = colEnd + to.ColOff = x2 * EMU + to.Row = rowEnd + to.RowOff = y2 * EMU twoCellAnchor.From = &from twoCellAnchor.To = &to pic := xlsxPic{} @@ -245,7 +274,7 @@ func (f *File) addMedia(file string, ext string) { // in http://purl.oclc.org/ooxml/officeDocument/relationships/image and // appropriate content type. func (f *File) addDrawingContentTypePart(index int) { - var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false} + var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false} var content xlsxTypes xml.Unmarshal([]byte(f.readXML(`[Content_Types].xml`)), &content) for _, v := range content.Defaults { diff --git a/test/Workbook1.xlsx b/test/Workbook1.xlsx index f6f713c1..d287361c 100644 Binary files a/test/Workbook1.xlsx and b/test/Workbook1.xlsx differ diff --git a/test/images/excel.bmp b/test/images/excel.bmp deleted file mode 100644 index 424619c5..00000000 Binary files a/test/images/excel.bmp and /dev/null differ diff --git a/test/images/excel.gif b/test/images/excel.gif index 1a34a180..bf7499a0 100644 Binary files a/test/images/excel.gif and b/test/images/excel.gif differ diff --git a/test/images/excel.ico b/test/images/excel.ico deleted file mode 100644 index 9bb8d6c9..00000000 Binary files a/test/images/excel.ico and /dev/null differ diff --git a/test/images/excel.jpg b/test/images/excel.jpg index 67c84f35..49edfefc 100644 Binary files a/test/images/excel.jpg and b/test/images/excel.jpg differ diff --git a/test/images/excel.png b/test/images/excel.png index cbf36469..52c36db8 100644 Binary files a/test/images/excel.png and b/test/images/excel.png differ diff --git a/test/images/excel.tif b/test/images/excel.tif deleted file mode 100644 index 4eba87d2..00000000 Binary files a/test/images/excel.tif and /dev/null differ diff --git a/xmlDrawing.go b/xmlDrawing.go index efcfa813..1b809753 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -115,10 +115,11 @@ type xlsxBlipFill struct { Stretch xlsxStretch `xml:"a:stretch"` } -// xlsxSpPr directly maps the spPr (Shape Properties). This element specifies the visual -// shape properties that can be applied to a picture. These are the same properties that -// are allowed to describe the visual properties of a shape but are used here to describe -// the visual appearance of a picture within a document. +// xlsxSpPr directly maps the spPr (Shape Properties). This element specifies +// the visual shape properties that can be applied to a picture. These are the +// same properties that are allowed to describe the visual properties of a shape +// but are used here to describe the visual appearance of a picture within a +// document. type xlsxSpPr struct { Xfrm xlsxXfrm `xml:"a:xfrm"` PrstGeom xlsxPrstGeom `xml:"a:prstGeom"`