| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- package svg
- import (
- strconvStdlib "strconv"
- "github.com/tdewolff/minify"
- "github.com/tdewolff/parse"
- "github.com/tdewolff/parse/strconv"
- )
- type PathData struct {
- o *Minifier
- x, y float64
- coords [][]byte
- coordFloats []float64
- state PathDataState
- curBuffer []byte
- altBuffer []byte
- coordBuffer []byte
- }
- type PathDataState struct {
- cmd byte
- prevDigit bool
- prevDigitIsInt bool
- }
- func NewPathData(o *Minifier) *PathData {
- return &PathData{
- o: o,
- }
- }
- func (p *PathData) ShortenPathData(b []byte) []byte {
- var x0, y0 float64
- var cmd byte
- p.x, p.y = 0.0, 0.0
- p.coords = p.coords[:0]
- p.coordFloats = p.coordFloats[:0]
- p.state = PathDataState{}
- j := 0
- for i := 0; i < len(b); i++ {
- c := b[i]
- if c == ' ' || c == ',' || c == '\n' || c == '\r' || c == '\t' {
- continue
- } else if c >= 'A' && (cmd == 0 || cmd != c || c == 'M' || c == 'm') { // any command
- if cmd != 0 {
- j += p.copyInstruction(b[j:], cmd)
- if cmd == 'M' || cmd == 'm' {
- x0 = p.x
- y0 = p.y
- } else if cmd == 'Z' || cmd == 'z' {
- p.x = x0
- p.y = y0
- }
- }
- cmd = c
- p.coords = p.coords[:0]
- p.coordFloats = p.coordFloats[:0]
- } else if n := parse.Number(b[i:]); n > 0 {
- f, _ := strconv.ParseFloat(b[i : i+n])
- p.coords = append(p.coords, b[i:i+n])
- p.coordFloats = append(p.coordFloats, f)
- i += n - 1
- }
- }
- if cmd != 0 {
- j += p.copyInstruction(b[j:], cmd)
- }
- return b[:j]
- }
- func (p *PathData) copyInstruction(b []byte, cmd byte) int {
- n := len(p.coords)
- if n == 0 {
- if cmd == 'Z' || cmd == 'z' {
- b[0] = 'z'
- return 1
- }
- return 0
- }
- isRelCmd := cmd >= 'a'
- // get new cursor coordinates
- di := 0
- if (cmd == 'M' || cmd == 'm' || cmd == 'L' || cmd == 'l' || cmd == 'T' || cmd == 't') && n%2 == 0 {
- di = 2
- // reprint M always, as the first pair is a move but subsequent pairs are L
- if cmd == 'M' || cmd == 'm' {
- p.state.cmd = byte(0)
- }
- } else if cmd == 'H' || cmd == 'h' || cmd == 'V' || cmd == 'v' {
- di = 1
- } else if (cmd == 'S' || cmd == 's' || cmd == 'Q' || cmd == 'q') && n%4 == 0 {
- di = 4
- } else if (cmd == 'C' || cmd == 'c') && n%6 == 0 {
- di = 6
- } else if (cmd == 'A' || cmd == 'a') && n%7 == 0 {
- di = 7
- } else {
- return 0
- }
- j := 0
- origCmd := cmd
- ax, ay := 0.0, 0.0
- for i := 0; i < n; i += di {
- // subsequent coordinate pairs for M are really L
- if i > 0 && (origCmd == 'M' || origCmd == 'm') {
- origCmd = 'L' + (origCmd - 'M')
- }
- cmd = origCmd
- coords := p.coords[i : i+di]
- coordFloats := p.coordFloats[i : i+di]
- if cmd == 'H' || cmd == 'h' {
- ax = coordFloats[di-1]
- if isRelCmd {
- ay = 0
- } else {
- ay = p.y
- }
- } else if cmd == 'V' || cmd == 'v' {
- if isRelCmd {
- ax = 0
- } else {
- ax = p.x
- }
- ay = coordFloats[di-1]
- } else {
- ax = coordFloats[di-2]
- ay = coordFloats[di-1]
- }
- // switch from L to H or V whenever possible
- if cmd == 'L' || cmd == 'l' {
- if isRelCmd {
- if coordFloats[0] == 0 {
- cmd = 'v'
- coords = coords[1:]
- coordFloats = coordFloats[1:]
- } else if coordFloats[1] == 0 {
- cmd = 'h'
- coords = coords[:1]
- coordFloats = coordFloats[:1]
- }
- } else {
- if coordFloats[0] == p.x {
- cmd = 'V'
- coords = coords[1:]
- coordFloats = coordFloats[1:]
- } else if coordFloats[1] == p.y {
- cmd = 'H'
- coords = coords[:1]
- coordFloats = coordFloats[:1]
- }
- }
- }
- // make a current and alternated path with absolute/relative altered
- var curState, altState PathDataState
- curState = p.shortenCurPosInstruction(cmd, coords)
- if isRelCmd {
- altState = p.shortenAltPosInstruction(cmd-'a'+'A', coordFloats, p.x, p.y)
- } else {
- altState = p.shortenAltPosInstruction(cmd-'A'+'a', coordFloats, -p.x, -p.y)
- }
- // choose shortest, relative or absolute path?
- if len(p.altBuffer) < len(p.curBuffer) {
- j += copy(b[j:], p.altBuffer)
- p.state = altState
- } else {
- j += copy(b[j:], p.curBuffer)
- p.state = curState
- }
- if isRelCmd {
- p.x += ax
- p.y += ay
- } else {
- p.x = ax
- p.y = ay
- }
- }
- return j
- }
- func (p *PathData) shortenCurPosInstruction(cmd byte, coords [][]byte) PathDataState {
- state := p.state
- p.curBuffer = p.curBuffer[:0]
- if cmd != state.cmd && !(state.cmd == 'M' && cmd == 'L' || state.cmd == 'm' && cmd == 'l') {
- p.curBuffer = append(p.curBuffer, cmd)
- state.cmd = cmd
- state.prevDigit = false
- state.prevDigitIsInt = false
- }
- for i, coord := range coords {
- isFlag := false
- if (cmd == 'A' || cmd == 'a') && (i%7 == 3 || i%7 == 4) {
- isFlag = true
- }
- coord = minify.Number(coord, p.o.Decimals)
- state.copyNumber(&p.curBuffer, coord, isFlag)
- }
- return state
- }
- func (p *PathData) shortenAltPosInstruction(cmd byte, coordFloats []float64, x, y float64) PathDataState {
- state := p.state
- p.altBuffer = p.altBuffer[:0]
- if cmd != state.cmd && !(state.cmd == 'M' && cmd == 'L' || state.cmd == 'm' && cmd == 'l') {
- p.altBuffer = append(p.altBuffer, cmd)
- state.cmd = cmd
- state.prevDigit = false
- state.prevDigitIsInt = false
- }
- for i, f := range coordFloats {
- isFlag := false
- if cmd == 'L' || cmd == 'l' || cmd == 'C' || cmd == 'c' || cmd == 'S' || cmd == 's' || cmd == 'Q' || cmd == 'q' || cmd == 'T' || cmd == 't' || cmd == 'M' || cmd == 'm' {
- if i%2 == 0 {
- f += x
- } else {
- f += y
- }
- } else if cmd == 'H' || cmd == 'h' {
- f += x
- } else if cmd == 'V' || cmd == 'v' {
- f += y
- } else if cmd == 'A' || cmd == 'a' {
- if i%7 == 5 {
- f += x
- } else if i%7 == 6 {
- f += y
- } else if i%7 == 3 || i%7 == 4 {
- isFlag = true
- }
- }
- p.coordBuffer = strconvStdlib.AppendFloat(p.coordBuffer[:0], f, 'g', -1, 64)
- coord := minify.Number(p.coordBuffer, p.o.Decimals)
- state.copyNumber(&p.altBuffer, coord, isFlag)
- }
- return state
- }
- func (state *PathDataState) copyNumber(buffer *[]byte, coord []byte, isFlag bool) {
- if state.prevDigit && (coord[0] >= '0' && coord[0] <= '9' || coord[0] == '.' && state.prevDigitIsInt) {
- if coord[0] == '0' && !state.prevDigitIsInt {
- if isFlag {
- *buffer = append(*buffer, ' ', '0')
- state.prevDigitIsInt = true
- } else {
- *buffer = append(*buffer, '.', '0') // aggresively add dot so subsequent numbers could drop leading space
- // prevDigit stays true and prevDigitIsInt stays false
- }
- return
- }
- *buffer = append(*buffer, ' ')
- }
- state.prevDigit = true
- state.prevDigitIsInt = true
- if len(coord) > 2 && coord[len(coord)-2] == '0' && coord[len(coord)-1] == '0' {
- coord[len(coord)-2] = 'e'
- coord[len(coord)-1] = '2'
- state.prevDigitIsInt = false
- } else {
- for _, c := range coord {
- if c == '.' || c == 'e' || c == 'E' {
- state.prevDigitIsInt = false
- break
- }
- }
- }
- *buffer = append(*buffer, coord...)
- }
|