pathdata.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. package svg
  2. import (
  3. strconvStdlib "strconv"
  4. "github.com/tdewolff/minify"
  5. "github.com/tdewolff/parse"
  6. "github.com/tdewolff/parse/strconv"
  7. )
  8. type PathData struct {
  9. o *Minifier
  10. x, y float64
  11. coords [][]byte
  12. coordFloats []float64
  13. state PathDataState
  14. curBuffer []byte
  15. altBuffer []byte
  16. coordBuffer []byte
  17. }
  18. type PathDataState struct {
  19. cmd byte
  20. prevDigit bool
  21. prevDigitIsInt bool
  22. }
  23. func NewPathData(o *Minifier) *PathData {
  24. return &PathData{
  25. o: o,
  26. }
  27. }
  28. func (p *PathData) ShortenPathData(b []byte) []byte {
  29. var x0, y0 float64
  30. var cmd byte
  31. p.x, p.y = 0.0, 0.0
  32. p.coords = p.coords[:0]
  33. p.coordFloats = p.coordFloats[:0]
  34. p.state = PathDataState{}
  35. j := 0
  36. for i := 0; i < len(b); i++ {
  37. c := b[i]
  38. if c == ' ' || c == ',' || c == '\n' || c == '\r' || c == '\t' {
  39. continue
  40. } else if c >= 'A' && (cmd == 0 || cmd != c || c == 'M' || c == 'm') { // any command
  41. if cmd != 0 {
  42. j += p.copyInstruction(b[j:], cmd)
  43. if cmd == 'M' || cmd == 'm' {
  44. x0 = p.x
  45. y0 = p.y
  46. } else if cmd == 'Z' || cmd == 'z' {
  47. p.x = x0
  48. p.y = y0
  49. }
  50. }
  51. cmd = c
  52. p.coords = p.coords[:0]
  53. p.coordFloats = p.coordFloats[:0]
  54. } else if n := parse.Number(b[i:]); n > 0 {
  55. f, _ := strconv.ParseFloat(b[i : i+n])
  56. p.coords = append(p.coords, b[i:i+n])
  57. p.coordFloats = append(p.coordFloats, f)
  58. i += n - 1
  59. }
  60. }
  61. if cmd != 0 {
  62. j += p.copyInstruction(b[j:], cmd)
  63. }
  64. return b[:j]
  65. }
  66. func (p *PathData) copyInstruction(b []byte, cmd byte) int {
  67. n := len(p.coords)
  68. if n == 0 {
  69. if cmd == 'Z' || cmd == 'z' {
  70. b[0] = 'z'
  71. return 1
  72. }
  73. return 0
  74. }
  75. isRelCmd := cmd >= 'a'
  76. // get new cursor coordinates
  77. di := 0
  78. if (cmd == 'M' || cmd == 'm' || cmd == 'L' || cmd == 'l' || cmd == 'T' || cmd == 't') && n%2 == 0 {
  79. di = 2
  80. // reprint M always, as the first pair is a move but subsequent pairs are L
  81. if cmd == 'M' || cmd == 'm' {
  82. p.state.cmd = byte(0)
  83. }
  84. } else if cmd == 'H' || cmd == 'h' || cmd == 'V' || cmd == 'v' {
  85. di = 1
  86. } else if (cmd == 'S' || cmd == 's' || cmd == 'Q' || cmd == 'q') && n%4 == 0 {
  87. di = 4
  88. } else if (cmd == 'C' || cmd == 'c') && n%6 == 0 {
  89. di = 6
  90. } else if (cmd == 'A' || cmd == 'a') && n%7 == 0 {
  91. di = 7
  92. } else {
  93. return 0
  94. }
  95. j := 0
  96. origCmd := cmd
  97. ax, ay := 0.0, 0.0
  98. for i := 0; i < n; i += di {
  99. // subsequent coordinate pairs for M are really L
  100. if i > 0 && (origCmd == 'M' || origCmd == 'm') {
  101. origCmd = 'L' + (origCmd - 'M')
  102. }
  103. cmd = origCmd
  104. coords := p.coords[i : i+di]
  105. coordFloats := p.coordFloats[i : i+di]
  106. if cmd == 'H' || cmd == 'h' {
  107. ax = coordFloats[di-1]
  108. if isRelCmd {
  109. ay = 0
  110. } else {
  111. ay = p.y
  112. }
  113. } else if cmd == 'V' || cmd == 'v' {
  114. if isRelCmd {
  115. ax = 0
  116. } else {
  117. ax = p.x
  118. }
  119. ay = coordFloats[di-1]
  120. } else {
  121. ax = coordFloats[di-2]
  122. ay = coordFloats[di-1]
  123. }
  124. // switch from L to H or V whenever possible
  125. if cmd == 'L' || cmd == 'l' {
  126. if isRelCmd {
  127. if coordFloats[0] == 0 {
  128. cmd = 'v'
  129. coords = coords[1:]
  130. coordFloats = coordFloats[1:]
  131. } else if coordFloats[1] == 0 {
  132. cmd = 'h'
  133. coords = coords[:1]
  134. coordFloats = coordFloats[:1]
  135. }
  136. } else {
  137. if coordFloats[0] == p.x {
  138. cmd = 'V'
  139. coords = coords[1:]
  140. coordFloats = coordFloats[1:]
  141. } else if coordFloats[1] == p.y {
  142. cmd = 'H'
  143. coords = coords[:1]
  144. coordFloats = coordFloats[:1]
  145. }
  146. }
  147. }
  148. // make a current and alternated path with absolute/relative altered
  149. var curState, altState PathDataState
  150. curState = p.shortenCurPosInstruction(cmd, coords)
  151. if isRelCmd {
  152. altState = p.shortenAltPosInstruction(cmd-'a'+'A', coordFloats, p.x, p.y)
  153. } else {
  154. altState = p.shortenAltPosInstruction(cmd-'A'+'a', coordFloats, -p.x, -p.y)
  155. }
  156. // choose shortest, relative or absolute path?
  157. if len(p.altBuffer) < len(p.curBuffer) {
  158. j += copy(b[j:], p.altBuffer)
  159. p.state = altState
  160. } else {
  161. j += copy(b[j:], p.curBuffer)
  162. p.state = curState
  163. }
  164. if isRelCmd {
  165. p.x += ax
  166. p.y += ay
  167. } else {
  168. p.x = ax
  169. p.y = ay
  170. }
  171. }
  172. return j
  173. }
  174. func (p *PathData) shortenCurPosInstruction(cmd byte, coords [][]byte) PathDataState {
  175. state := p.state
  176. p.curBuffer = p.curBuffer[:0]
  177. if cmd != state.cmd && !(state.cmd == 'M' && cmd == 'L' || state.cmd == 'm' && cmd == 'l') {
  178. p.curBuffer = append(p.curBuffer, cmd)
  179. state.cmd = cmd
  180. state.prevDigit = false
  181. state.prevDigitIsInt = false
  182. }
  183. for i, coord := range coords {
  184. isFlag := false
  185. if (cmd == 'A' || cmd == 'a') && (i%7 == 3 || i%7 == 4) {
  186. isFlag = true
  187. }
  188. coord = minify.Number(coord, p.o.Decimals)
  189. state.copyNumber(&p.curBuffer, coord, isFlag)
  190. }
  191. return state
  192. }
  193. func (p *PathData) shortenAltPosInstruction(cmd byte, coordFloats []float64, x, y float64) PathDataState {
  194. state := p.state
  195. p.altBuffer = p.altBuffer[:0]
  196. if cmd != state.cmd && !(state.cmd == 'M' && cmd == 'L' || state.cmd == 'm' && cmd == 'l') {
  197. p.altBuffer = append(p.altBuffer, cmd)
  198. state.cmd = cmd
  199. state.prevDigit = false
  200. state.prevDigitIsInt = false
  201. }
  202. for i, f := range coordFloats {
  203. isFlag := false
  204. 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' {
  205. if i%2 == 0 {
  206. f += x
  207. } else {
  208. f += y
  209. }
  210. } else if cmd == 'H' || cmd == 'h' {
  211. f += x
  212. } else if cmd == 'V' || cmd == 'v' {
  213. f += y
  214. } else if cmd == 'A' || cmd == 'a' {
  215. if i%7 == 5 {
  216. f += x
  217. } else if i%7 == 6 {
  218. f += y
  219. } else if i%7 == 3 || i%7 == 4 {
  220. isFlag = true
  221. }
  222. }
  223. p.coordBuffer = strconvStdlib.AppendFloat(p.coordBuffer[:0], f, 'g', -1, 64)
  224. coord := minify.Number(p.coordBuffer, p.o.Decimals)
  225. state.copyNumber(&p.altBuffer, coord, isFlag)
  226. }
  227. return state
  228. }
  229. func (state *PathDataState) copyNumber(buffer *[]byte, coord []byte, isFlag bool) {
  230. if state.prevDigit && (coord[0] >= '0' && coord[0] <= '9' || coord[0] == '.' && state.prevDigitIsInt) {
  231. if coord[0] == '0' && !state.prevDigitIsInt {
  232. if isFlag {
  233. *buffer = append(*buffer, ' ', '0')
  234. state.prevDigitIsInt = true
  235. } else {
  236. *buffer = append(*buffer, '.', '0') // aggresively add dot so subsequent numbers could drop leading space
  237. // prevDigit stays true and prevDigitIsInt stays false
  238. }
  239. return
  240. }
  241. *buffer = append(*buffer, ' ')
  242. }
  243. state.prevDigit = true
  244. state.prevDigitIsInt = true
  245. if len(coord) > 2 && coord[len(coord)-2] == '0' && coord[len(coord)-1] == '0' {
  246. coord[len(coord)-2] = 'e'
  247. coord[len(coord)-1] = '2'
  248. state.prevDigitIsInt = false
  249. } else {
  250. for _, c := range coord {
  251. if c == '.' || c == 'e' || c == 'E' {
  252. state.prevDigitIsInt = false
  253. break
  254. }
  255. }
  256. }
  257. *buffer = append(*buffer, coord...)
  258. }