Skip to content

Commit d60bee3

Browse files
author
pegas
committed
Fixed AddPictureFromBytes SVG image
1 parent 78a8a43 commit d60bee3

File tree

1 file changed

+118
-3
lines changed

1 file changed

+118
-3
lines changed

picture.go

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import (
1515
"bytes"
1616
"encoding/xml"
1717
"image"
18+
"image/color"
1819
"io"
20+
"math"
1921
"os"
2022
"path"
2123
"path/filepath"
@@ -236,9 +238,19 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
236238
return ErrParameterInvalid
237239
}
238240
options := parseGraphicOptions(pic.Format)
239-
img, _, err := image.DecodeConfig(bytes.NewReader(pic.File))
240-
if err != nil {
241-
return err
241+
var img image.Config
242+
if ext == ".svg" {
243+
cfg, err := svgDecodeConfig(pic.File)
244+
if err != nil {
245+
return err
246+
}
247+
img = cfg
248+
} else {
249+
cfg, _, err := image.DecodeConfig(bytes.NewReader(pic.File))
250+
if err != nil {
251+
return err
252+
}
253+
img = cfg
242254
}
243255
// Read sheet data
244256
f.mu.Lock()
@@ -286,6 +298,109 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
286298
return err
287299
}
288300

301+
// --- SVG helpers ---
302+
303+
type svgRoot struct {
304+
Width string `xml:"width,attr"`
305+
Height string `xml:"height,attr"`
306+
ViewBox string `xml:"viewBox,attr"`
307+
}
308+
309+
// svgUnitToPx converts a numeric string with an optional unit suffix
310+
// (e.g. "16px", "12pt", "1in") to pixels using a 96-DPI scale,
311+
// which is the standard DPI for Office/Excel.
312+
func svgUnitToPx(s string) (float64, bool) {
313+
s = strings.TrimSpace(s)
314+
if s == "" {
315+
return 0, false
316+
}
317+
i := len(s)
318+
for i > 0 && (s[i-1] < '0' || s[i-1] > '9') && s[i-1] != '.' {
319+
i--
320+
}
321+
num := strings.TrimSpace(s[:i])
322+
unit := strings.ToLower(strings.TrimSpace(s[i:]))
323+
324+
v, err := strconv.ParseFloat(num, 64)
325+
if err != nil {
326+
return 0, false
327+
}
328+
switch unit {
329+
case "", "px":
330+
return v, true
331+
case "pt":
332+
return v * (96.0 / 72.0), true
333+
case "in":
334+
return v * 96.0, true
335+
case "mm":
336+
return v * (96.0 / 25.4), true
337+
case "cm":
338+
return v * (96.0 / 2.54), true
339+
default:
340+
return 0, false
341+
}
342+
}
343+
344+
// svgDecodeConfig extracts approximate image dimensions for SVG files
345+
// based on the <svg> element's width/height attributes or its viewBox.
346+
// Only the root <svg ...> element is parsed; the SVG content is not rendered.
347+
func svgDecodeConfig(b []byte) (image.Config, error) {
348+
var root svgRoot
349+
dec := xml.NewDecoder(bytes.NewReader(b))
350+
dec.Strict = false
351+
dec.AutoClose = xml.HTMLAutoClose
352+
dec.Entity = xml.HTMLEntity
353+
354+
// Read only the root <svg> element and decode its attributes.
355+
for {
356+
tok, err := dec.Token()
357+
if err != nil {
358+
return image.Config{}, err
359+
}
360+
if se, ok := tok.(xml.StartElement); ok && strings.EqualFold(se.Name.Local, "svg") {
361+
if err := dec.DecodeElement(&root, &se); err != nil && err != io.EOF {
362+
return image.Config{}, err
363+
}
364+
break
365+
}
366+
}
367+
368+
// 1) Use width/height attributes if both are present and valid.
369+
if wpx, okW := svgUnitToPx(root.Width); okW {
370+
if hpx, okH := svgUnitToPx(root.Height); okH {
371+
return image.Config{
372+
ColorModel: color.RGBAModel,
373+
Width: int(math.Max(1, math.Round(wpx))),
374+
Height: int(math.Max(1, math.Round(hpx))),
375+
}, nil
376+
}
377+
}
378+
379+
// 2) Otherwise, try to infer dimensions from the viewBox attribute:
380+
// "minX minY width height"
381+
if root.ViewBox != "" {
382+
parts := strings.Fields(root.ViewBox)
383+
if len(parts) == 4 {
384+
if vw, err1 := strconv.ParseFloat(parts[2], 64); err1 == nil && vw > 0 {
385+
if vh, err2 := strconv.ParseFloat(parts[3], 64); err2 == nil && vh > 0 {
386+
return image.Config{
387+
ColorModel: color.RGBAModel,
388+
Width: int(math.Max(1, math.Round(vw))),
389+
Height: int(math.Max(1, math.Round(vh))),
390+
}, nil
391+
}
392+
}
393+
}
394+
}
395+
396+
// 3) Fallback to a default icon-sized bounding box if nothing is specified.
397+
return image.Config{
398+
ColorModel: color.RGBAModel,
399+
Width: 16,
400+
Height: 16,
401+
}, nil
402+
}
403+
289404
// addSheetLegacyDrawing provides a function to add legacy drawing element to
290405
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
291406
func (f *File) addSheetLegacyDrawing(sheet string, rID int) {

0 commit comments

Comments
 (0)