2019-01-01 13:20:14 +08:00
|
|
|
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
|
2018-09-14 00:44:23 +08:00
|
|
|
// this source code is governed by a BSD-style license that can be found in
|
|
|
|
// the LICENSE file.
|
|
|
|
//
|
2018-09-12 15:47:56 +08:00
|
|
|
// Package excelize providing a set of functions that allow you to write to
|
|
|
|
// and read from XLSX files. Support reads and writes XLSX file generated by
|
|
|
|
// Microsoft Excel™ 2007 and later. Support save file without losing original
|
|
|
|
// charts of XLSX. This library needs Go version 1.8 or later.
|
2018-09-14 00:58:48 +08:00
|
|
|
|
2017-01-17 19:06:42 +08:00
|
|
|
package excelize
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2017-02-28 12:59:04 +08:00
|
|
|
"encoding/json"
|
2017-01-17 19:06:42 +08:00
|
|
|
"encoding/xml"
|
|
|
|
"errors"
|
2017-01-22 16:16:03 +08:00
|
|
|
"image"
|
2017-01-17 19:06:42 +08:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// parseFormatPictureSet provides a function to parse the format settings of
|
|
|
|
// the picture with default value.
|
2018-05-27 11:25:55 +08:00
|
|
|
func parseFormatPictureSet(formatSet string) (*formatPicture, error) {
|
2017-03-06 12:05:41 +08:00
|
|
|
format := formatPicture{
|
2017-02-28 12:59:04 +08:00
|
|
|
FPrintsWithSheet: true,
|
|
|
|
FLocksWithSheet: false,
|
|
|
|
NoChangeAspect: false,
|
|
|
|
OffsetX: 0,
|
|
|
|
OffsetY: 0,
|
|
|
|
XScale: 1.0,
|
|
|
|
YScale: 1.0,
|
|
|
|
}
|
2018-07-13 17:40:47 +08:00
|
|
|
err := json.Unmarshal(parseFormatSet(formatSet), &format)
|
2018-05-27 11:25:55 +08:00
|
|
|
return &format, err
|
2017-02-28 12:59:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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:
|
2017-01-17 19:06:42 +08:00
|
|
|
//
|
2017-01-22 19:20:33 +08:00
|
|
|
// package main
|
2017-01-22 16:16:03 +08:00
|
|
|
//
|
2017-01-22 19:20:33 +08:00
|
|
|
// import (
|
|
|
|
// "fmt"
|
|
|
|
// _ "image/gif"
|
|
|
|
// _ "image/jpeg"
|
|
|
|
// _ "image/png"
|
2017-01-22 16:16:03 +08:00
|
|
|
//
|
2019-04-30 18:39:27 +08:00
|
|
|
// "github.com/360EntSecGroup-Skylar/excelize/v2"
|
2017-01-22 19:20:33 +08:00
|
|
|
// )
|
2017-01-22 16:16:03 +08:00
|
|
|
//
|
2017-01-22 19:20:33 +08:00
|
|
|
// func main() {
|
2019-04-20 14:57:50 +08:00
|
|
|
// f := excelize.NewFile()
|
2017-01-22 16:16:03 +08:00
|
|
|
// // Insert a picture.
|
2019-04-20 14:57:50 +08:00
|
|
|
// err := f.AddPicture("Sheet1", "A2", "./image1.jpg", "")
|
2017-02-12 19:03:24 +08:00
|
|
|
// if err != nil {
|
|
|
|
// fmt.Println(err)
|
|
|
|
// }
|
2018-02-03 15:02:37 +08:00
|
|
|
// // Insert a picture scaling in the cell with location hyperlink.
|
2019-04-20 14:57:50 +08:00
|
|
|
// err = f.AddPicture("Sheet1", "D2", "./image1.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`)
|
2017-02-12 19:03:24 +08:00
|
|
|
// if err != nil {
|
|
|
|
// fmt.Println(err)
|
|
|
|
// }
|
2018-04-26 11:41:13 +08:00
|
|
|
// // Insert a picture offset in the cell with external hyperlink, printing and positioning support.
|
2019-04-20 14:57:50 +08:00
|
|
|
// err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`)
|
2017-01-22 16:16:03 +08:00
|
|
|
// if err != nil {
|
|
|
|
// fmt.Println(err)
|
|
|
|
// }
|
2019-04-20 14:57:50 +08:00
|
|
|
// err = f.SaveAs("./Book1.xlsx")
|
2017-01-22 16:16:03 +08:00
|
|
|
// if err != nil {
|
|
|
|
// fmt.Println(err)
|
|
|
|
// }
|
2017-01-17 19:06:42 +08:00
|
|
|
// }
|
|
|
|
//
|
2018-02-03 15:02:37 +08:00
|
|
|
// LinkType defines two types of hyperlink "External" for web site or
|
|
|
|
// "Location" for moving to one of cell in this workbook. When the
|
|
|
|
// "hyperlink_type" is "Location", coordinates need to start with "#".
|
2018-04-26 11:41:13 +08:00
|
|
|
//
|
|
|
|
// Positioning defines two types of the position of a picture in an Excel
|
|
|
|
// spreadsheet, "oneCell" (Move but don't size with cells) or "absolute"
|
|
|
|
// (Don't move or size with cells). If you don't set this parameter, default
|
|
|
|
// positioning is move and size with cells.
|
2017-02-28 12:59:04 +08:00
|
|
|
func (f *File) AddPicture(sheet, cell, picture, format string) error {
|
2017-01-17 19:06:42 +08:00
|
|
|
var err error
|
|
|
|
// Check picture exists first.
|
|
|
|
if _, err = os.Stat(picture); os.IsNotExist(err) {
|
|
|
|
return err
|
|
|
|
}
|
2017-03-28 21:18:06 +08:00
|
|
|
ext, ok := supportImageTypes[path.Ext(picture)]
|
2017-01-17 19:06:42 +08:00
|
|
|
if !ok {
|
2018-09-12 15:47:56 +08:00
|
|
|
return errors.New("unsupported image extension")
|
2017-01-17 19:06:42 +08:00
|
|
|
}
|
2018-09-14 00:24:49 +08:00
|
|
|
file, _ := ioutil.ReadFile(picture)
|
|
|
|
_, name := filepath.Split(picture)
|
|
|
|
return f.AddPictureFromBytes(sheet, cell, format, name, ext, file)
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddPictureFromBytes provides the method to add picture in a sheet by given
|
|
|
|
// picture format set (such as offset, scale, aspect ratio setting and print
|
|
|
|
// settings), file base name, extension name and file bytes. For example:
|
|
|
|
//
|
|
|
|
// package main
|
|
|
|
//
|
|
|
|
// import (
|
|
|
|
// "fmt"
|
|
|
|
// _ "image/jpeg"
|
|
|
|
// "io/ioutil"
|
|
|
|
//
|
2019-04-30 18:39:27 +08:00
|
|
|
// "github.com/360EntSecGroup-Skylar/excelize/v2"
|
2018-09-14 00:24:49 +08:00
|
|
|
// )
|
|
|
|
//
|
|
|
|
// func main() {
|
2019-04-20 14:57:50 +08:00
|
|
|
// f := excelize.NewFile()
|
2018-09-14 00:24:49 +08:00
|
|
|
//
|
|
|
|
// file, err := ioutil.ReadFile("./image1.jpg")
|
|
|
|
// if err != nil {
|
|
|
|
// fmt.Println(err)
|
|
|
|
// }
|
2019-04-20 14:57:50 +08:00
|
|
|
// err = f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file)
|
2018-09-14 00:24:49 +08:00
|
|
|
// if err != nil {
|
|
|
|
// fmt.Println(err)
|
|
|
|
// }
|
2019-04-20 14:57:50 +08:00
|
|
|
// err = f.SaveAs("./Book1.xlsx")
|
2018-09-14 00:24:49 +08:00
|
|
|
// if err != nil {
|
|
|
|
// fmt.Println(err)
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, file []byte) error {
|
|
|
|
var drawingHyperlinkRID int
|
|
|
|
var hyperlinkType string
|
|
|
|
ext, ok := supportImageTypes[extension]
|
|
|
|
if !ok {
|
|
|
|
return errors.New("unsupported image extension")
|
|
|
|
}
|
|
|
|
formatSet, err := parseFormatPictureSet(format)
|
2018-09-12 15:47:56 +08:00
|
|
|
if err != nil {
|
2018-08-20 16:53:51 +08:00
|
|
|
return err
|
|
|
|
}
|
Huge refactorig for consistent col/row numbering (#356)
* Huge refactorig for consistent col/row numbering
Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes.
But have to go deeper, do fixes, after do related fixes and again and again.
Major improvements:
1. Tests made stronger again (But still be weak).
2. "Empty" returns for incorrect input replaces with panic.
3. Check for correct col/row/cell naming & addressing by default.
4. Removed huge amount of duplicated code.
5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all,
and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName().
6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc).
* Formatting fixes
2019-03-20 00:14:41 +08:00
|
|
|
img, _, err := image.DecodeConfig(bytes.NewReader(file))
|
2018-05-27 11:25:55 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-01-17 19:06:42 +08:00
|
|
|
// Read sheet data.
|
2019-04-15 11:22:57 +08:00
|
|
|
xlsx, err := f.workSheetReader(sheet)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-01-17 19:06:42 +08:00
|
|
|
// Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder.
|
|
|
|
drawingID := f.countDrawings() + 1
|
|
|
|
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
|
2017-05-24 14:17:35 +08:00
|
|
|
drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML)
|
2019-03-24 09:07:57 +08:00
|
|
|
mediaStr := ".." + strings.TrimPrefix(f.addMedia(file, ext), "xl")
|
|
|
|
drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipImage, mediaStr, hyperlinkType)
|
2018-02-03 15:02:37 +08:00
|
|
|
// Add picture with hyperlink.
|
|
|
|
if formatSet.Hyperlink != "" && formatSet.HyperlinkType != "" {
|
|
|
|
if formatSet.HyperlinkType == "External" {
|
|
|
|
hyperlinkType = formatSet.HyperlinkType
|
|
|
|
}
|
|
|
|
drawingHyperlinkRID = f.addDrawingRelationships(drawingID, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType)
|
|
|
|
}
|
2019-03-23 20:08:06 +08:00
|
|
|
err = f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, formatSet)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-05-24 14:17:35 +08:00
|
|
|
f.addContentTypePart(drawingID, "drawings")
|
2017-01-17 19:06:42 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// addSheetRelationships provides a function to add
|
2017-09-13 22:00:33 +08:00
|
|
|
// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name, relationship
|
|
|
|
// type and target.
|
2017-01-19 14:05:32 +08:00
|
|
|
func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) int {
|
2017-10-20 14:40:57 +08:00
|
|
|
name, ok := f.sheetMap[trimSheetName(sheet)]
|
|
|
|
if !ok {
|
|
|
|
name = strings.ToLower(sheet) + ".xml"
|
|
|
|
}
|
|
|
|
var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
2019-02-26 14:21:44 +08:00
|
|
|
sheetRels := f.workSheetRelsReader(rels)
|
|
|
|
if sheetRels == nil {
|
|
|
|
sheetRels = &xlsxWorkbookRels{}
|
|
|
|
}
|
2017-01-17 19:06:42 +08:00
|
|
|
var rID = 1
|
|
|
|
var ID bytes.Buffer
|
|
|
|
ID.WriteString("rId")
|
|
|
|
ID.WriteString(strconv.Itoa(rID))
|
2019-02-26 14:21:44 +08:00
|
|
|
ID.Reset()
|
|
|
|
rID = len(sheetRels.Relationships) + 1
|
|
|
|
ID.WriteString("rId")
|
|
|
|
ID.WriteString(strconv.Itoa(rID))
|
2017-01-17 19:06:42 +08:00
|
|
|
sheetRels.Relationships = append(sheetRels.Relationships, xlsxWorkbookRelation{
|
2017-01-19 14:05:32 +08:00
|
|
|
ID: ID.String(),
|
|
|
|
Type: relType,
|
|
|
|
Target: target,
|
|
|
|
TargetMode: targetMode,
|
2017-01-17 19:06:42 +08:00
|
|
|
})
|
2019-02-26 14:21:44 +08:00
|
|
|
f.WorkSheetRels[rels] = sheetRels
|
2017-01-17 19:06:42 +08:00
|
|
|
return rID
|
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// deleteSheetRelationships provides a function to delete relationships in
|
|
|
|
// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
|
|
|
|
// relationship index.
|
2017-07-24 10:26:02 +08:00
|
|
|
func (f *File) deleteSheetRelationships(sheet, rID string) {
|
2017-10-20 14:40:57 +08:00
|
|
|
name, ok := f.sheetMap[trimSheetName(sheet)]
|
|
|
|
if !ok {
|
|
|
|
name = strings.ToLower(sheet) + ".xml"
|
|
|
|
}
|
|
|
|
var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
2019-02-26 14:21:44 +08:00
|
|
|
sheetRels := f.workSheetRelsReader(rels)
|
|
|
|
if sheetRels == nil {
|
|
|
|
sheetRels = &xlsxWorkbookRels{}
|
|
|
|
}
|
2017-07-24 10:26:02 +08:00
|
|
|
for k, v := range sheetRels.Relationships {
|
2017-09-30 17:07:59 +08:00
|
|
|
if v.ID == rID {
|
|
|
|
sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...)
|
2017-07-24 10:26:02 +08:00
|
|
|
}
|
|
|
|
}
|
2019-02-26 14:21:44 +08:00
|
|
|
f.WorkSheetRels[rels] = sheetRels
|
2017-07-24 10:26:02 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// addSheetLegacyDrawing provides a function to add legacy drawing element to
|
2017-09-13 22:00:33 +08:00
|
|
|
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
|
2017-05-13 13:28:21 +08:00
|
|
|
func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
|
2019-04-15 11:22:57 +08:00
|
|
|
xlsx, _ := f.workSheetReader(sheet)
|
2017-05-13 13:28:21 +08:00
|
|
|
xlsx.LegacyDrawing = &xlsxLegacyDrawing{
|
|
|
|
RID: "rId" + strconv.Itoa(rID),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// addSheetDrawing provides a function to add drawing element to
|
2017-09-13 22:00:33 +08:00
|
|
|
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
|
2017-01-17 19:06:42 +08:00
|
|
|
func (f *File) addSheetDrawing(sheet string, rID int) {
|
2019-04-15 11:22:57 +08:00
|
|
|
xlsx, _ := f.workSheetReader(sheet)
|
2017-01-17 19:06:42 +08:00
|
|
|
xlsx.Drawing = &xlsxDrawing{
|
|
|
|
RID: "rId" + strconv.Itoa(rID),
|
|
|
|
}
|
2017-01-24 18:29:02 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// addSheetPicture provides a function to add picture element to
|
2017-09-13 22:00:33 +08:00
|
|
|
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
|
2017-01-24 18:29:02 +08:00
|
|
|
func (f *File) addSheetPicture(sheet string, rID int) {
|
2019-04-15 11:22:57 +08:00
|
|
|
xlsx, _ := f.workSheetReader(sheet)
|
2017-01-24 18:29:02 +08:00
|
|
|
xlsx.Picture = &xlsxPicture{
|
|
|
|
RID: "rId" + strconv.Itoa(rID),
|
|
|
|
}
|
2017-01-17 19:06:42 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// countDrawings provides a function to get drawing files count storage in the
|
2017-01-18 16:05:01 +08:00
|
|
|
// folder xl/drawings.
|
2017-01-17 19:06:42 +08:00
|
|
|
func (f *File) countDrawings() int {
|
2019-03-22 14:26:43 +08:00
|
|
|
c1, c2 := 0, 0
|
2017-01-17 19:06:42 +08:00
|
|
|
for k := range f.XLSX {
|
|
|
|
if strings.Contains(k, "xl/drawings/drawing") {
|
2019-03-22 14:26:43 +08:00
|
|
|
c1++
|
2017-01-17 19:06:42 +08:00
|
|
|
}
|
|
|
|
}
|
2019-03-22 14:26:43 +08:00
|
|
|
for rel := range f.Drawings {
|
|
|
|
if strings.Contains(rel, "xl/drawings/drawing") {
|
|
|
|
c2++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if c1 < c2 {
|
|
|
|
return c2
|
|
|
|
}
|
|
|
|
return c1
|
2017-01-17 19:06:42 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// addDrawingPicture provides a function to add picture by given sheet,
|
2017-04-23 00:10:23 +08:00
|
|
|
// drawingXML, cell, file name, width, height relationship index and format
|
2017-05-24 14:17:35 +08:00
|
|
|
// sets.
|
2019-03-23 20:08:06 +08:00
|
|
|
func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID, hyperlinkRID int, formatSet *formatPicture) error {
|
|
|
|
col, row, err := CellNameToCoordinates(cell)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-02-28 12:59:04 +08:00
|
|
|
width = int(float64(width) * formatSet.XScale)
|
|
|
|
height = int(float64(height) * formatSet.YScale)
|
2019-03-20 16:52:33 +08:00
|
|
|
col--
|
|
|
|
row--
|
Huge refactorig for consistent col/row numbering (#356)
* Huge refactorig for consistent col/row numbering
Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes.
But have to go deeper, do fixes, after do related fixes and again and again.
Major improvements:
1. Tests made stronger again (But still be weak).
2. "Empty" returns for incorrect input replaces with panic.
3. Check for correct col/row/cell naming & addressing by default.
4. Removed huge amount of duplicated code.
5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all,
and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName().
6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc).
* Formatting fixes
2019-03-20 00:14:41 +08:00
|
|
|
colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 :=
|
|
|
|
f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height)
|
2019-02-25 22:14:34 +08:00
|
|
|
content, cNvPrID := f.drawingParser(drawingXML)
|
2017-04-30 20:03:43 +08:00
|
|
|
twoCellAnchor := xdrCellAnchor{}
|
2018-04-26 11:41:13 +08:00
|
|
|
twoCellAnchor.EditAs = formatSet.Positioning
|
2017-01-17 19:06:42 +08:00
|
|
|
from := xlsxFrom{}
|
2017-01-22 16:16:03 +08:00
|
|
|
from.Col = colStart
|
2017-02-28 12:59:04 +08:00
|
|
|
from.ColOff = formatSet.OffsetX * EMU
|
2017-01-22 16:16:03 +08:00
|
|
|
from.Row = rowStart
|
2017-02-28 12:59:04 +08:00
|
|
|
from.RowOff = formatSet.OffsetY * EMU
|
2017-01-17 19:06:42 +08:00
|
|
|
to := xlsxTo{}
|
2017-01-22 16:16:03 +08:00
|
|
|
to.Col = colEnd
|
|
|
|
to.ColOff = x2 * EMU
|
|
|
|
to.Row = rowEnd
|
|
|
|
to.RowOff = y2 * EMU
|
2017-01-17 19:06:42 +08:00
|
|
|
twoCellAnchor.From = &from
|
|
|
|
twoCellAnchor.To = &to
|
|
|
|
pic := xlsxPic{}
|
2017-02-28 12:59:04 +08:00
|
|
|
pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect
|
2017-04-30 20:03:43 +08:00
|
|
|
pic.NvPicPr.CNvPr.ID = f.countCharts() + f.countMedia() + 1
|
2017-01-17 19:06:42 +08:00
|
|
|
pic.NvPicPr.CNvPr.Descr = file
|
|
|
|
pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID)
|
2018-02-03 15:02:37 +08:00
|
|
|
if hyperlinkRID != 0 {
|
|
|
|
pic.NvPicPr.CNvPr.HlinkClick = &xlsxHlinkClick{
|
|
|
|
R: SourceRelationship,
|
|
|
|
RID: "rId" + strconv.Itoa(hyperlinkRID),
|
|
|
|
}
|
|
|
|
}
|
2017-01-17 19:06:42 +08:00
|
|
|
pic.BlipFill.Blip.R = SourceRelationship
|
|
|
|
pic.BlipFill.Blip.Embed = "rId" + strconv.Itoa(rID)
|
|
|
|
pic.SpPr.PrstGeom.Prst = "rect"
|
|
|
|
|
|
|
|
twoCellAnchor.Pic = &pic
|
2017-04-30 20:03:43 +08:00
|
|
|
twoCellAnchor.ClientData = &xdrClientData{
|
2017-02-28 12:59:04 +08:00
|
|
|
FLocksWithSheet: formatSet.FLocksWithSheet,
|
|
|
|
FPrintsWithSheet: formatSet.FPrintsWithSheet,
|
2017-01-17 19:06:42 +08:00
|
|
|
}
|
2017-04-30 20:03:43 +08:00
|
|
|
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
|
2019-02-25 22:14:34 +08:00
|
|
|
f.Drawings[drawingXML] = content
|
2019-03-23 20:08:06 +08:00
|
|
|
return err
|
2017-01-17 19:06:42 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// addDrawingRelationships provides a function to add image part relationships
|
|
|
|
// in the file xl/drawings/_rels/drawing%d.xml.rels by given drawing index,
|
2017-01-18 16:05:01 +08:00
|
|
|
// relationship type and target.
|
2018-02-03 15:02:37 +08:00
|
|
|
func (f *File) addDrawingRelationships(index int, relType, target, targetMode string) int {
|
2017-01-17 19:06:42 +08:00
|
|
|
var rels = "xl/drawings/_rels/drawing" + strconv.Itoa(index) + ".xml.rels"
|
|
|
|
var rID = 1
|
|
|
|
var ID bytes.Buffer
|
|
|
|
ID.WriteString("rId")
|
|
|
|
ID.WriteString(strconv.Itoa(rID))
|
2019-02-25 22:14:34 +08:00
|
|
|
drawingRels := f.drawingRelsReader(rels)
|
|
|
|
if drawingRels == nil {
|
|
|
|
drawingRels = &xlsxWorkbookRels{}
|
2017-01-17 19:06:42 +08:00
|
|
|
}
|
2019-02-25 22:14:34 +08:00
|
|
|
ID.Reset()
|
|
|
|
rID = len(drawingRels.Relationships) + 1
|
|
|
|
ID.WriteString("rId")
|
|
|
|
ID.WriteString(strconv.Itoa(rID))
|
2017-01-17 19:06:42 +08:00
|
|
|
drawingRels.Relationships = append(drawingRels.Relationships, xlsxWorkbookRelation{
|
2018-02-03 15:02:37 +08:00
|
|
|
ID: ID.String(),
|
|
|
|
Type: relType,
|
|
|
|
Target: target,
|
|
|
|
TargetMode: targetMode,
|
2017-01-17 19:06:42 +08:00
|
|
|
})
|
2019-02-25 22:14:34 +08:00
|
|
|
f.DrawingRels[rels] = drawingRels
|
2017-01-17 19:06:42 +08:00
|
|
|
return rID
|
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// countMedia provides a function to get media files count storage in the
|
|
|
|
// folder xl/media/image.
|
2017-01-17 19:06:42 +08:00
|
|
|
func (f *File) countMedia() int {
|
|
|
|
count := 0
|
|
|
|
for k := range f.XLSX {
|
|
|
|
if strings.Contains(k, "xl/media/image") {
|
|
|
|
count++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return count
|
|
|
|
}
|
|
|
|
|
2019-03-24 09:07:57 +08:00
|
|
|
// addMedia provides a function to add a picture into folder xl/media/image by
|
|
|
|
// given file and extension name. Duplicate images are only actually stored once
|
|
|
|
// and drawings that use it will reference the same image.
|
|
|
|
func (f *File) addMedia(file []byte, ext string) string {
|
2017-01-17 19:06:42 +08:00
|
|
|
count := f.countMedia()
|
2019-03-24 09:07:57 +08:00
|
|
|
for name, existing := range f.XLSX {
|
|
|
|
if !strings.HasPrefix(name, "xl/media/image") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if bytes.Equal(file, existing) {
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
}
|
2017-01-17 19:06:42 +08:00
|
|
|
media := "xl/media/image" + strconv.Itoa(count+1) + ext
|
2018-09-14 00:24:49 +08:00
|
|
|
f.XLSX[media] = file
|
2019-03-24 09:07:57 +08:00
|
|
|
return media
|
2017-01-17 19:06:42 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// setContentTypePartImageExtensions provides a function to set the content
|
|
|
|
// type for relationship parts and the Main Document part.
|
2017-01-24 18:29:02 +08:00
|
|
|
func (f *File) setContentTypePartImageExtensions() {
|
2019-06-27 21:58:14 +08:00
|
|
|
var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false}
|
2017-04-01 13:56:39 +08:00
|
|
|
content := f.contentTypesReader()
|
2017-01-17 19:06:42 +08:00
|
|
|
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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2017-01-24 18:29:02 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// setContentTypePartVMLExtensions provides a function to set the content type
|
2017-05-13 13:28:21 +08:00
|
|
|
// 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",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// addContentTypePart provides a function to add content type part
|
|
|
|
// relationships in the file [Content_Types].xml by given index.
|
2017-05-24 14:17:35 +08:00
|
|
|
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()
|
2017-01-17 19:06:42 +08:00
|
|
|
}
|
2017-05-13 13:28:21 +08:00
|
|
|
content := f.contentTypesReader()
|
|
|
|
for _, v := range content.Overrides {
|
2017-05-24 14:17:35 +08:00
|
|
|
if v.PartName == partNames[contentType] {
|
2017-05-13 13:28:21 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
content.Overrides = append(content.Overrides, xlsxOverride{
|
2017-05-24 14:17:35 +08:00
|
|
|
PartName: partNames[contentType],
|
|
|
|
ContentType: contentTypes[contentType],
|
2017-05-13 13:28:21 +08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// getSheetRelationshipsTargetByID provides a function to get Target attribute
|
2017-09-13 22:00:33 +08:00
|
|
|
// value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
|
2017-01-18 16:05:01 +08:00
|
|
|
// relationship index.
|
2017-06-12 18:05:09 +08:00
|
|
|
func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
|
2017-10-31 16:33:36 +08:00
|
|
|
name, ok := f.sheetMap[trimSheetName(sheet)]
|
|
|
|
if !ok {
|
|
|
|
name = strings.ToLower(sheet) + ".xml"
|
|
|
|
}
|
|
|
|
var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
|
2019-02-26 14:21:44 +08:00
|
|
|
sheetRels := f.workSheetRelsReader(rels)
|
|
|
|
if sheetRels == nil {
|
|
|
|
sheetRels = &xlsxWorkbookRels{}
|
|
|
|
}
|
2017-01-17 19:06:42 +08:00
|
|
|
for _, v := range sheetRels.Relationships {
|
|
|
|
if v.ID == rID {
|
|
|
|
return v.Target
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
2017-03-28 21:18:06 +08:00
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// GetPicture provides a 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:
|
2017-03-28 21:18:06 +08:00
|
|
|
//
|
2019-04-20 14:57:50 +08:00
|
|
|
// f, err := excelize.OpenFile("./Book1.xlsx")
|
2017-03-28 21:18:06 +08:00
|
|
|
// if err != nil {
|
|
|
|
// fmt.Println(err)
|
2017-10-20 14:40:57 +08:00
|
|
|
// return
|
2017-03-28 21:18:06 +08:00
|
|
|
// }
|
2019-04-20 14:57:50 +08:00
|
|
|
// file, raw, err := f.GetPicture("Sheet1", "A2")
|
|
|
|
// if err != nil {
|
|
|
|
// fmt.Println(err)
|
2017-10-20 14:40:57 +08:00
|
|
|
// return
|
2017-03-28 21:18:06 +08:00
|
|
|
// }
|
2019-04-20 14:57:50 +08:00
|
|
|
// err = ioutil.WriteFile(file, raw, 0644)
|
2017-03-30 11:06:51 +08:00
|
|
|
// if err != nil {
|
2017-03-28 21:18:06 +08:00
|
|
|
// fmt.Println(err)
|
2017-03-30 11:06:51 +08:00
|
|
|
// }
|
2017-03-28 21:18:06 +08:00
|
|
|
//
|
2019-03-23 20:08:06 +08:00
|
|
|
func (f *File) GetPicture(sheet, cell string) (string, []byte, error) {
|
|
|
|
col, row, err := CellNameToCoordinates(cell)
|
|
|
|
if err != nil {
|
2019-06-20 00:00:40 +08:00
|
|
|
return "", nil, err
|
2019-03-23 20:08:06 +08:00
|
|
|
}
|
2019-03-20 16:52:33 +08:00
|
|
|
col--
|
|
|
|
row--
|
2019-04-15 11:22:57 +08:00
|
|
|
xlsx, err := f.workSheetReader(sheet)
|
|
|
|
if err != nil {
|
2019-06-20 00:00:40 +08:00
|
|
|
return "", nil, err
|
2019-04-15 11:22:57 +08:00
|
|
|
}
|
2017-03-28 21:18:06 +08:00
|
|
|
if xlsx.Drawing == nil {
|
2019-06-20 00:00:40 +08:00
|
|
|
return "", nil, err
|
2017-03-28 21:18:06 +08:00
|
|
|
}
|
|
|
|
target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID)
|
|
|
|
drawingXML := strings.Replace(target, "..", "xl", -1)
|
2019-06-20 00:00:40 +08:00
|
|
|
_, ok := f.XLSX[drawingXML]
|
|
|
|
if !ok {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
Huge refactorig for consistent col/row numbering (#356)
* Huge refactorig for consistent col/row numbering
Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes.
But have to go deeper, do fixes, after do related fixes and again and again.
Major improvements:
1. Tests made stronger again (But still be weak).
2. "Empty" returns for incorrect input replaces with panic.
3. Check for correct col/row/cell naming & addressing by default.
4. Removed huge amount of duplicated code.
5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all,
and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName().
6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc).
* Formatting fixes
2019-03-20 00:14:41 +08:00
|
|
|
drawingRelationships := strings.Replace(
|
|
|
|
strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1)
|
|
|
|
|
2019-06-20 00:00:40 +08:00
|
|
|
return f.getPicture(row, col, drawingXML, drawingRelationships)
|
|
|
|
}
|
Huge refactorig for consistent col/row numbering (#356)
* Huge refactorig for consistent col/row numbering
Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes.
But have to go deeper, do fixes, after do related fixes and again and again.
Major improvements:
1. Tests made stronger again (But still be weak).
2. "Empty" returns for incorrect input replaces with panic.
3. Check for correct col/row/cell naming & addressing by default.
4. Removed huge amount of duplicated code.
5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all,
and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName().
6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc).
* Formatting fixes
2019-03-20 00:14:41 +08:00
|
|
|
|
2019-06-20 00:00:40 +08:00
|
|
|
// getPicture provides a function to get picture base name and raw content
|
|
|
|
// embed in XLSX by given coordinates and drawing relationships.
|
|
|
|
func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (string, []byte, error) {
|
|
|
|
wsDr, _ := f.drawingParser(drawingXML)
|
2019-02-25 22:14:34 +08:00
|
|
|
for _, anchor := range wsDr.TwoCellAnchor {
|
|
|
|
if anchor.From != nil && anchor.Pic != nil {
|
|
|
|
if anchor.From.Col == col && anchor.From.Row == row {
|
Huge refactorig for consistent col/row numbering (#356)
* Huge refactorig for consistent col/row numbering
Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes.
But have to go deeper, do fixes, after do related fixes and again and again.
Major improvements:
1. Tests made stronger again (But still be weak).
2. "Empty" returns for incorrect input replaces with panic.
3. Check for correct col/row/cell naming & addressing by default.
4. Removed huge amount of duplicated code.
5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all,
and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName().
6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc).
* Formatting fixes
2019-03-20 00:14:41 +08:00
|
|
|
xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships,
|
|
|
|
anchor.Pic.BlipFill.Blip.Embed)
|
2019-02-25 22:14:34 +08:00
|
|
|
_, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)]
|
|
|
|
if ok {
|
Huge refactorig for consistent col/row numbering (#356)
* Huge refactorig for consistent col/row numbering
Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes.
But have to go deeper, do fixes, after do related fixes and again and again.
Major improvements:
1. Tests made stronger again (But still be weak).
2. "Empty" returns for incorrect input replaces with panic.
3. Check for correct col/row/cell naming & addressing by default.
4. Removed huge amount of duplicated code.
5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all,
and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName().
6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc).
* Formatting fixes
2019-03-20 00:14:41 +08:00
|
|
|
return filepath.Base(xlsxWorkbookRelation.Target),
|
|
|
|
[]byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target,
|
2019-06-20 00:00:40 +08:00
|
|
|
"..", "xl", -1)]), nil
|
2019-02-25 22:14:34 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
Huge refactorig for consistent col/row numbering (#356)
* Huge refactorig for consistent col/row numbering
Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes.
But have to go deeper, do fixes, after do related fixes and again and again.
Major improvements:
1. Tests made stronger again (But still be weak).
2. "Empty" returns for incorrect input replaces with panic.
3. Check for correct col/row/cell naming & addressing by default.
4. Removed huge amount of duplicated code.
5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all,
and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName().
6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc).
* Formatting fixes
2019-03-20 00:14:41 +08:00
|
|
|
|
2019-02-25 22:14:34 +08:00
|
|
|
decodeWsDr := decodeWsDr{}
|
|
|
|
_ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr)
|
2017-03-28 21:18:06 +08:00
|
|
|
for _, anchor := range decodeWsDr.TwoCellAnchor {
|
|
|
|
decodeTwoCellAnchor := decodeTwoCellAnchor{}
|
2018-05-27 11:25:55 +08:00
|
|
|
_ = xml.Unmarshal([]byte("<decodeTwoCellAnchor>"+anchor.Content+"</decodeTwoCellAnchor>"), &decodeTwoCellAnchor)
|
2017-09-30 17:07:59 +08:00
|
|
|
if decodeTwoCellAnchor.From != nil && decodeTwoCellAnchor.Pic != nil {
|
|
|
|
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 {
|
2019-06-20 00:00:40 +08:00
|
|
|
return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]), nil
|
2017-09-30 17:07:59 +08:00
|
|
|
}
|
2017-03-28 21:18:06 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-06-20 00:00:40 +08:00
|
|
|
return "", nil, nil
|
2017-03-28 21:18:06 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 10:21:24 +08:00
|
|
|
// getDrawingRelationships provides a function to get drawing relationships
|
|
|
|
// from xl/drawings/_rels/drawing%s.xml.rels by given file name and
|
|
|
|
// relationship ID.
|
2017-03-28 21:18:06 +08:00
|
|
|
func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation {
|
2019-02-25 22:14:34 +08:00
|
|
|
if drawingRels := f.drawingRelsReader(rels); drawingRels != nil {
|
|
|
|
for _, v := range drawingRels.Relationships {
|
|
|
|
if v.ID == rID {
|
|
|
|
return &v
|
|
|
|
}
|
2017-03-28 21:18:06 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2019-02-25 22:14:34 +08:00
|
|
|
|
|
|
|
// drawingRelsReader provides a function to get the pointer to the structure
|
|
|
|
// after deserialization of xl/drawings/_rels/drawing%d.xml.rels.
|
|
|
|
func (f *File) drawingRelsReader(rel string) *xlsxWorkbookRels {
|
|
|
|
if f.DrawingRels[rel] == nil {
|
|
|
|
_, ok := f.XLSX[rel]
|
|
|
|
if ok {
|
|
|
|
d := xlsxWorkbookRels{}
|
|
|
|
_ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rel)), &d)
|
|
|
|
f.DrawingRels[rel] = &d
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return f.DrawingRels[rel]
|
|
|
|
}
|
|
|
|
|
|
|
|
// drawingRelsWriter provides a function to save
|
|
|
|
// xl/drawings/_rels/drawing%d.xml.rels after serialize structure.
|
|
|
|
func (f *File) drawingRelsWriter() {
|
|
|
|
for path, d := range f.DrawingRels {
|
|
|
|
if d != nil {
|
|
|
|
v, _ := xml.Marshal(d)
|
|
|
|
f.saveFileList(path, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// drawingsWriter provides a function to save xl/drawings/drawing%d.xml after
|
|
|
|
// serialize structure.
|
|
|
|
func (f *File) drawingsWriter() {
|
|
|
|
for path, d := range f.Drawings {
|
|
|
|
if d != nil {
|
|
|
|
v, _ := xml.Marshal(d)
|
|
|
|
f.saveFileList(path, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|