288 lines
7.7 KiB
Go
288 lines
7.7 KiB
Go
// Copyright 2017 The oksvg Authors. All rights reserved.
|
|
// created: 2/12/2017 by S.R.Wiley
|
|
//
|
|
// utils.go implements translation of an SVG2.0 path into a rasterx Path.
|
|
|
|
package oksvg
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"image/color"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/srwiley/rasterx"
|
|
"golang.org/x/image/colornames"
|
|
"golang.org/x/net/html/charset"
|
|
)
|
|
|
|
// ReadIconStream reads the Icon from the given io.Reader.
|
|
// This only supports a sub-set of SVG, but
|
|
// is enough to draw many icons. If errMode is provided,
|
|
// the first value determines if the icon ignores, errors out, or logs a warning
|
|
// if it does not handle an element found in the icon file. Ignore warnings is
|
|
// the default if no ErrorMode value is provided.
|
|
func ReadIconStream(stream io.Reader, errMode ...ErrorMode) (*SvgIcon, error) {
|
|
icon := &SvgIcon{Defs: make(map[string][]definition), Grads: make(map[string]*rasterx.Gradient), Transform: rasterx.Identity}
|
|
cursor := &IconCursor{StyleStack: []PathStyle{DefaultStyle}, icon: icon}
|
|
if len(errMode) > 0 {
|
|
cursor.ErrorMode = errMode[0]
|
|
}
|
|
classInfo := ""
|
|
decoder := xml.NewDecoder(stream)
|
|
decoder.CharsetReader = charset.NewReaderLabel
|
|
for {
|
|
t, err := decoder.Token()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return icon, err
|
|
}
|
|
// Inspect the type of the XML token
|
|
switch se := t.(type) {
|
|
case xml.StartElement:
|
|
// Reads all recognized style attributes from the start element
|
|
// and places it on top of the styleStack
|
|
err = cursor.PushStyle(se.Attr)
|
|
if err != nil {
|
|
return icon, err
|
|
}
|
|
err = cursor.readStartElement(se)
|
|
if err != nil {
|
|
return icon, err
|
|
}
|
|
if se.Name.Local == "style" && cursor.inDefs {
|
|
cursor.inDefsStyle = true
|
|
}
|
|
case xml.EndElement:
|
|
// pop style
|
|
cursor.StyleStack = cursor.StyleStack[:len(cursor.StyleStack)-1]
|
|
switch se.Name.Local {
|
|
case "g":
|
|
if cursor.inDefs {
|
|
cursor.currentDef = append(cursor.currentDef, definition{
|
|
Tag: "endg",
|
|
})
|
|
}
|
|
case "title":
|
|
cursor.inTitleText = false
|
|
case "desc":
|
|
cursor.inDescText = false
|
|
case "defs":
|
|
if len(cursor.currentDef) > 0 {
|
|
cursor.icon.Defs[cursor.currentDef[0].ID] = cursor.currentDef
|
|
cursor.currentDef = make([]definition, 0)
|
|
}
|
|
cursor.inDefs = false
|
|
case "radialGradient", "linearGradient":
|
|
cursor.inGrad = false
|
|
|
|
case "style":
|
|
if cursor.inDefsStyle {
|
|
icon.classes, err = parseClasses(classInfo)
|
|
if err != nil {
|
|
return icon, err
|
|
}
|
|
cursor.inDefsStyle = false
|
|
}
|
|
}
|
|
case xml.CharData:
|
|
if cursor.inTitleText {
|
|
icon.Titles[len(icon.Titles)-1] += string(se)
|
|
}
|
|
if cursor.inDescText {
|
|
icon.Descriptions[len(icon.Descriptions)-1] += string(se)
|
|
}
|
|
if cursor.inDefsStyle {
|
|
classInfo = string(se)
|
|
}
|
|
}
|
|
}
|
|
return icon, nil
|
|
}
|
|
|
|
// ReadReplacingCurrentColor replaces currentColor value with specified value and loads SvgIcon as ReadIconStream do.
|
|
// currentColor value should be valid hex, rgb or named color value.
|
|
func ReadReplacingCurrentColor(stream io.Reader, currentColor string, errMode ...ErrorMode) (icon *SvgIcon, err error) {
|
|
var (
|
|
data []byte
|
|
)
|
|
|
|
if data, err = ioutil.ReadAll(stream); err != nil {
|
|
return nil, fmt.Errorf("%w: read data: %v", errParamMismatch, err)
|
|
}
|
|
|
|
if currentColor != "" && strings.Contains(string(data), "currentColor") {
|
|
data = []byte(strings.ReplaceAll(string(data), "currentColor", currentColor))
|
|
}
|
|
|
|
if icon, err = ReadIconStream(bytes.NewBuffer(data), errMode...); err != nil {
|
|
return nil, fmt.Errorf("%w: load: %v", errParamMismatch, err)
|
|
}
|
|
|
|
return icon, nil
|
|
}
|
|
|
|
// ReadIcon reads the Icon from the named file.
|
|
// This only supports a sub-set of SVG, but is enough to draw many icons.
|
|
// If errMode is provided, the first value determines if the icon ignores, errors out, or logs a warning
|
|
// if it does not handle an element found in the icon file.
|
|
// Ignore warnings is the default if no ErrorMode value is provided.
|
|
func ReadIcon(iconFile string, errMode ...ErrorMode) (*SvgIcon, error) {
|
|
fin, errf := os.Open(iconFile)
|
|
if errf != nil {
|
|
return nil, errf
|
|
}
|
|
defer fin.Close()
|
|
return ReadIconStream(fin, errMode...)
|
|
}
|
|
|
|
// ParseSVGColorNum reads the SFG color string e.g. #FBD9BD
|
|
func ParseSVGColorNum(colorStr string) (r, g, b uint8, err error) {
|
|
colorStr = strings.TrimPrefix(colorStr, "#")
|
|
var t uint64
|
|
if len(colorStr) != 6 {
|
|
if len(colorStr) != 3 {
|
|
err = fmt.Errorf("color string %s is not length 3 or 6 as required by SVG specification",
|
|
colorStr)
|
|
return
|
|
}
|
|
// SVG specs say duplicate characters in case of 3 digit hex number
|
|
colorStr = string([]byte{colorStr[0], colorStr[0],
|
|
colorStr[1], colorStr[1], colorStr[2], colorStr[2]})
|
|
}
|
|
for _, v := range []struct {
|
|
c *uint8
|
|
s string
|
|
}{
|
|
{&r, colorStr[0:2]},
|
|
{&g, colorStr[2:4]},
|
|
{&b, colorStr[4:6]}} {
|
|
t, err = strconv.ParseUint(v.s, 16, 8)
|
|
if err != nil {
|
|
return
|
|
}
|
|
*v.c = uint8(t)
|
|
}
|
|
return
|
|
}
|
|
|
|
// ParseSVGColor parses an SVG color string in all forms
|
|
// including all SVG1.1 names, obtained from the image.colornames package
|
|
func ParseSVGColor(colorStr string) (color.Color, error) {
|
|
// _, _, _, a := curColor.RGBA()
|
|
v := strings.ToLower(colorStr)
|
|
if strings.HasPrefix(v, "url") { // We are not handling urls
|
|
// and gradients and stuff at this point
|
|
return color.NRGBA{0, 0, 0, 255}, nil
|
|
}
|
|
switch v {
|
|
case "none", "":
|
|
// nil signals that the function (fill or stroke) is off;
|
|
// not the same as black
|
|
return nil, nil
|
|
default:
|
|
cn, ok := colornames.Map[v]
|
|
if ok {
|
|
r, g, b, a := cn.RGBA()
|
|
return color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)}, nil
|
|
}
|
|
}
|
|
cStr := strings.TrimPrefix(colorStr, "rgb(")
|
|
if cStr != colorStr {
|
|
cStr := strings.TrimSuffix(cStr, ")")
|
|
vals := strings.Split(cStr, ",")
|
|
if len(vals) != 3 {
|
|
return color.NRGBA{}, errParamMismatch
|
|
}
|
|
var cvals [3]uint8
|
|
var err error
|
|
for i := range cvals {
|
|
cvals[i], err = parseColorValue(vals[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return color.NRGBA{cvals[0], cvals[1], cvals[2], 0xFF}, nil
|
|
}
|
|
|
|
cStr = strings.TrimPrefix(colorStr, "hsl(")
|
|
if cStr != colorStr {
|
|
cStr := strings.TrimSuffix(cStr, ")")
|
|
vals := strings.Split(cStr, ",")
|
|
if len(vals) != 3 {
|
|
return color.NRGBA{}, errParamMismatch
|
|
}
|
|
|
|
H, err := strconv.ParseInt(strings.TrimSpace(vals[0]), 10, 64)
|
|
if err != nil {
|
|
return color.NRGBA{}, fmt.Errorf("invalid hue in hsl: '%s' (%s)", vals[0], err)
|
|
}
|
|
|
|
S, err := strconv.ParseFloat(strings.TrimSpace(vals[1][:len(vals[1])-1]), 64)
|
|
if err != nil {
|
|
return color.NRGBA{}, fmt.Errorf("invalid saturation in hsl: '%s' (%s)", vals[1], err)
|
|
}
|
|
S = S / 100
|
|
|
|
L, err := strconv.ParseFloat(strings.TrimSpace(vals[2][:len(vals[2])-1]), 64)
|
|
if err != nil {
|
|
return color.NRGBA{}, fmt.Errorf("invalid lightness in hsl: '%s' (%s)", vals[2], err)
|
|
}
|
|
L = L / 100
|
|
|
|
C := (1 - math.Abs((2*L)-1)) * S
|
|
X := C * (1 - math.Abs(math.Mod((float64(H)/60), 2)-1))
|
|
m := L - C/2
|
|
|
|
var rp, gp, bp float64
|
|
if H < 60 {
|
|
rp, gp, bp = float64(C), float64(X), float64(0)
|
|
} else if H < 120 {
|
|
rp, gp, bp = float64(X), float64(C), float64(0)
|
|
} else if H < 180 {
|
|
rp, gp, bp = float64(0), float64(C), float64(X)
|
|
} else if H < 240 {
|
|
rp, gp, bp = float64(0), float64(X), float64(C)
|
|
} else if H < 300 {
|
|
rp, gp, bp = float64(X), float64(0), float64(C)
|
|
} else {
|
|
rp, gp, bp = float64(C), float64(0), float64(X)
|
|
}
|
|
|
|
r, g, b := math.Round((rp+m)*255), math.Round((gp+m)*255), math.Round((bp+m)*255)
|
|
if r > 255 {
|
|
r = 255
|
|
}
|
|
if g > 255 {
|
|
g = 255
|
|
}
|
|
if b > 255 {
|
|
b = 255
|
|
}
|
|
|
|
return color.NRGBA{
|
|
uint8(r),
|
|
uint8(g),
|
|
uint8(b),
|
|
0xFF,
|
|
}, nil
|
|
}
|
|
|
|
if colorStr[0] == '#' {
|
|
r, g, b, err := ParseSVGColorNum(colorStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return color.NRGBA{r, g, b, 0xFF}, nil
|
|
}
|
|
return nil, errParamMismatch
|
|
}
|