Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
module github.com/xuri/excelize/v2

go 1.24.0
go 1.23.3

require (
github.com/richardlehane/mscfb v1.0.4
github.com/stretchr/testify v1.11.1
github.com/tiendc/go-deepcopy v1.7.1
github.com/xuri/efp v0.0.1
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9
golang.org/x/crypto v0.43.0
golang.org/x/crypto v0.41.0
golang.org/x/image v0.25.0
golang.org/x/net v0.46.0
golang.org/x/text v0.30.0
golang.org/x/net v0.43.0
golang.org/x/text v0.28.0
)

require (
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
121 changes: 118 additions & 3 deletions picture.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import (
"bytes"
"encoding/xml"
"image"
"image/color"
"io"
"math"
"os"
"path"
"path/filepath"
Expand Down Expand Up @@ -236,9 +238,19 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
return ErrParameterInvalid
}
options := parseGraphicOptions(pic.Format)
img, _, err := image.DecodeConfig(bytes.NewReader(pic.File))
if err != nil {
return err
var img image.Config
if ext == ".svg" {
cfg, err := svgDecodeConfig(pic.File)
if err != nil {
return err
}
img = cfg
} else {
cfg, _, err := image.DecodeConfig(bytes.NewReader(pic.File))
if err != nil {
return err
}
img = cfg
}
// Read sheet data
f.mu.Lock()
Expand Down Expand Up @@ -286,6 +298,109 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
return err
}

// --- SVG helpers ---

type svgRoot struct {
Width string `xml:"width,attr"`
Height string `xml:"height,attr"`
ViewBox string `xml:"viewBox,attr"`
}

// svgUnitToPx converts a numeric string with an optional unit suffix
// (e.g. "16px", "12pt", "1in") to pixels using a 96-DPI scale,
// which is the standard DPI for Office/Excel.
func svgUnitToPx(s string) (float64, bool) {
s = strings.TrimSpace(s)
if s == "" {
return 0, false
}
i := len(s)
for i > 0 && (s[i-1] < '0' || s[i-1] > '9') && s[i-1] != '.' {
i--
}
num := strings.TrimSpace(s[:i])
unit := strings.ToLower(strings.TrimSpace(s[i:]))

v, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, false
}
switch unit {
case "", "px":
return v, true
case "pt":
return v * (96.0 / 72.0), true
case "in":
return v * 96.0, true
case "mm":
return v * (96.0 / 25.4), true
case "cm":
return v * (96.0 / 2.54), true
default:
return 0, false
}
}

// svgDecodeConfig extracts approximate image dimensions for SVG files
// based on the <svg> element's width/height attributes or its viewBox.
// Only the root <svg ...> element is parsed; the SVG content is not rendered.
func svgDecodeConfig(b []byte) (image.Config, error) {
var root svgRoot
dec := xml.NewDecoder(bytes.NewReader(b))
dec.Strict = false
dec.AutoClose = xml.HTMLAutoClose
dec.Entity = xml.HTMLEntity

// Read only the root <svg> element and decode its attributes.
for {
tok, err := dec.Token()
if err != nil {
return image.Config{}, err
}
if se, ok := tok.(xml.StartElement); ok && strings.EqualFold(se.Name.Local, "svg") {
if err := dec.DecodeElement(&root, &se); err != nil && err != io.EOF {
return image.Config{}, err
}
break
}
}

// 1) Use width/height attributes if both are present and valid.
if wpx, okW := svgUnitToPx(root.Width); okW {
if hpx, okH := svgUnitToPx(root.Height); okH {
return image.Config{
ColorModel: color.RGBAModel,
Width: int(math.Max(1, math.Round(wpx))),
Height: int(math.Max(1, math.Round(hpx))),
}, nil
}
}

// 2) Otherwise, try to infer dimensions from the viewBox attribute:
// "minX minY width height"
if root.ViewBox != "" {
parts := strings.Fields(root.ViewBox)
if len(parts) == 4 {
if vw, err1 := strconv.ParseFloat(parts[2], 64); err1 == nil && vw > 0 {
if vh, err2 := strconv.ParseFloat(parts[3], 64); err2 == nil && vh > 0 {
return image.Config{
ColorModel: color.RGBAModel,
Width: int(math.Max(1, math.Round(vw))),
Height: int(math.Max(1, math.Round(vh))),
}, nil
}
}
}
}

// 3) Fallback to a default icon-sized bounding box if nothing is specified.
return image.Config{
ColorModel: color.RGBAModel,
Width: 16,
Height: 16,
}, nil
}

// addSheetLegacyDrawing provides a function to add legacy drawing element to
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
Expand Down