Re: Go SVG Parser?

1,836 views
Skip to first unread message

Rob Lapensee

unread,
Apr 4, 2013, 1:18:31 PM4/4/13
to golan...@googlegroups.com, brand...@gmail.com
I would also be interested in an SVG parser.
I needed to manipulate SVG files,
so I wrote a program (in Go) to put the SVG in to a generic tree (using the xml parser),
then manipulated it,
then dumped it back out again.
but the structure that holds it is strictly generic,
it would be good to have a proper in-memory model of the svg.
If I were to write one, I would review and or contact ajstarks to see how close it is to already having a parser,
or what it would take to enhance it to have a parser as well as the generator.

Rob

On Thursday, April 4, 2013 1:09:18 PM UTC-4, brand...@gmail.com wrote:
Does anyone know of an SVG parser written in Go? There is https://github.com/ajstarks/svgo but it looks like that that only generates SVG. I currently use the xml.NewDecoder and attempt to only read Path elements, but I would like to support all svg shapes

Hotei

unread,
Apr 4, 2013, 5:04:32 PM4/4/13
to golan...@googlegroups.com, brand...@gmail.com
I ran into an SVG optimizer at https://github.com/svg/svgo I never looked at the inner workings of it but Iwould think it has some parsing.  It's written in NodeJs so I took a pass, but it might provide some ideas...

Daniel Jo

unread,
Apr 4, 2013, 5:06:46 PM4/4/13
to brand...@gmail.com, golang-nuts

I tried writing one several months ago, but I started getting frustrated with the very loose syntax of styles and paths. My package was able to translate some basic SVG commands to calls to the draw2d package. It was for this that I wrote a simple gradient rendering package.

-Daniel

On 2013-04-04 11:09 AM, <brand...@gmail.com> wrote:
Does anyone know of an SVG parser written in Go? There is https://github.com/ajstarks/svgo but it looks like that that only generates SVG. I currently use the xml.NewDecoder and attempt to only read Path elements, but I would like to support all svg shapes

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Gerard

unread,
Apr 7, 2013, 3:47:53 AM4/7/13
to golan...@googlegroups.com, brand...@gmail.com
One small remark: I would use an io.Reader for func ParseSvgFile and name it Decode, as in package image/png.

Op zaterdag 6 april 2013 23:36:12 UTC+2 schreef brand...@gmail.com het volgende:
This is what I came up with for now, it's based on the javascript library canvg, any comments on if this could be organized differently to make it better Go code? Having and error return value instead of using panics everywhere would probably be more idiomatic

// Used to decode xml data into a readable struct
type Path struct {
    Style string `xml:"style,attr"`
    Data  string `xml:"d,attr"`
}

// All supported Path Commands
type PathCommand int

const (
    NotAValidCommand PathCommand = iota
    MoveToAbsolute
    MoveToRelative
    ClosePath
    LineToAbsolute
    LineToRelative
)

// PathCommand ToString
func (command PathCommand) String() string {
    switch command {
    case NotAValidCommand:
        return "NotAValidCommand"
    case MoveToAbsolute:
        return "MoveToAbsolute"
    case MoveToRelative:
        return "MoveToRelative"
    case ClosePath:
        return "ClosePath"
    case LineToAbsolute:
        return "LineToAbsolute"
    case LineToRelative:
        return "LineToRelative"
    }
    return "UNKNOWN"
}

// True if the given PathCommand is relative
func (command PathCommand) IsRelative() bool {
    switch command {
    case MoveToRelative, LineToRelative:
        return true
    default:
        return false
    }
    panic("Not reachable")
}

// Convert string to command, returns NotAValidCommand if not valid
func ParseCommand(commandString string) PathCommand {

    switch commandString {
    case "M":
        return MoveToAbsolute
    case "m":
        return MoveToRelative
    case "Z":
    case "z":
        return ClosePath
    case "L":
        return LineToAbsolute
    case "l":
        return LineToRelative
    default:
        return NotAValidCommand
    }
    panic("Not reachable")
}

// Used to parse a path string
type PathParser struct {
    // All of the tokens, strings could be numbers or commands
    tokens []string

    // The token the parser is currently at
    tokenIndex int

    // The last PathCommand that was seen
    currentCommand PathCommand

    // Track current position for relative moves
    currentPosition Coordinate

    // The coordinates read for the path
    coordinates []Coordinate
}

// Create new parser
func NewParser(originalPathData string) (parser *PathParser) {

    parser = &PathParser{}

    seperateLetters, _ := regexp.Compile(`([^\s])?([MmZzLlHhVvCcSsQqTtAa])([^\s])?`)
    seperateNumbers, _ := regexp.Compile(`([0-9])([+\-])`)

    pathData := seperateLetters.ReplaceAllString(originalPathData, "$1 $2 $3")
    pathData = seperateNumbers.ReplaceAllString(pathData, "$1 $2")
    pathData = strings.Replace(pathData, ",", " ", -1)
    parser.tokens = strings.Fields(pathData)

    parser.coordinates = make([]Coordinate, 0)

    return parser
}

// Parse the data
func (this *PathParser) Parse() []Coordinate {

    for this.ReadCommand() {

        switch this.currentCommand {
        case MoveToAbsolute, MoveToRelative:
            this.ReadCoord(true)
            for this.PeekHasMoreArguments() { // can have multiple implicit line coords
                this.ReadCoord(false)
            }

        case LineToAbsolute, LineToRelative:
            for this.PeekHasMoreArguments() {
                this.ReadCoord(false)
            }

        default:
            panic(fmt.Sprint("Unsupported command:", this.currentCommand))
        }
    }

    return this.coordinates
}

// Move to next token
func (this *PathParser) ReadCommand() bool {

    if this.tokenIndex >= len(this.tokens) {
        return false
    }

    commandString := this.tokens[this.tokenIndex]
    this.tokenIndex++
    this.currentCommand = ParseCommand(commandString)
    if this.currentCommand == NotAValidCommand {
        panic(fmt.Sprint("Unexpected command, saw ", commandString))
    }

    return true
}

// Return if the next token is a command or not
func (this *PathParser) PeekHasMoreArguments() bool {

    if this.tokenIndex >= len(this.tokens) {
        return false
    }
    return ParseCommand(this.tokens[this.tokenIndex]) == NotAValidCommand
}

// Read two strings as a pair of doubles
func (this *PathParser) ReadCoord(penUp bool) {

    if this.tokenIndex >= len(this.tokens)-1 {
        panic(fmt.Sprint("Not enough tokens to ReadCoord, at ", this.tokenIndex, " of ", len(this.tokens)))
    }

    number := this.tokens[this.tokenIndex]
    this.tokenIndex++
    x, err := strconv.ParseFloat(number, 64)
    if err != nil {
        panic(fmt.Sprint("Expected a parseable number, but saw", number, "which got parse error", err))
    }

    number = this.tokens[this.tokenIndex]
    this.tokenIndex++
    y, err := strconv.ParseFloat(number, 64)
    if err != nil {
        panic(fmt.Sprint("Expected a parseable number, but saw", number, "which got parse error", err))
    }

    if this.currentCommand.IsRelative() {
        x += this.currentPosition.X
        y += this.currentPosition.Y
    }

    this.currentPosition = Coordinate{X: x, Y: y, PenUp: penUp}
    this.coordinates = append(this.coordinates, this.currentPosition)
}

// read a file and parse its Gcode
func ParseSvgFile(fileName string) (data []Coordinate) {

    file, err := os.Open(fileName)
    if err != nil {
        panic(err)
    }

    data = make([]Coordinate, 0)
    decoder := xml.NewDecoder(file)
    for {
        t, _ := decoder.Token()
        if t == nil {
            break
        }

        switch se := t.(type) {
        case xml.StartElement:

            if se.Name.Local == "path" {
                var pathData Path
                decoder.DecodeElement(&pathData, &se)

                parser := NewParser(pathData.Data)

                data = append(data, parser.Parse()...)
            }
        }
    }

    if len(data) == 0 {
        panic("SVG contained no Path elements! Only Paths are supported")
    }

    return data
}


Reply all
Reply to author
Forward
0 new messages