pathdata.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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. // ShortenPathData takes a full pathdata string and returns a shortened version. The original string is overwritten.
  29. // It parses all commands (M, A, Z, ...) and coordinates (numbers) and calls copyInstruction for each command.
  30. func (p *PathData) ShortenPathData(b []byte) []byte {
  31. var x0, y0 float64
  32. var cmd byte
  33. p.x, p.y = 0.0, 0.0
  34. p.coords = p.coords[:0]
  35. p.coordFloats = p.coordFloats[:0]
  36. p.state = PathDataState{}
  37. j := 0
  38. for i := 0; i < len(b); i++ {
  39. c := b[i]
  40. if c == ' ' || c == ',' || c == '\n' || c == '\r' || c == '\t' {
  41. continue
  42. } else if c >= 'A' && (cmd == 0 || cmd != c || c == 'M' || c == 'm') { // any command
  43. if cmd != 0 {
  44. j += p.copyInstruction(b[j:], cmd)
  45. if cmd == 'M' || cmd == 'm' {
  46. x0 = p.x
  47. y0 = p.y
  48. } else if cmd == 'Z' || cmd == 'z' {
  49. p.x = x0
  50. p.y = y0
  51. }
  52. }
  53. cmd = c
  54. p.coords = p.coords[:0]
  55. p.coordFloats = p.coordFloats[:0]
  56. } else if n := parse.Number(b[i:]); n > 0 {
  57. f, _ := strconv.ParseFloat(b[i : i+n])
  58. p.coords = append(p.coords, b[i:i+n])
  59. p.coordFloats = append(p.coordFloats, f)
  60. i += n - 1
  61. }
  62. }
  63. if cmd != 0 {
  64. j += p.copyInstruction(b[j:], cmd)
  65. }
  66. return b[:j]
  67. }
  68. // copyInstruction copies pathdata of a single command, but may be comprised of multiple sets for that command. For example, L takes two coordinates, but this function may process 2*N coordinates. Lowercase commands are relative commands, where the coordinates are relative to the previous point. Uppercase commands have absolute coordinates.
  69. // We update p.x and p.y (the current coordinates) according to the commands given. For each set of coordinates we call shortenCurPosInstruction and shortenAltPosInstruction. The former just minifies the coordinates, the latter will inverse the lowercase/uppercase of the command, and see if the coordinates get smaller due to that. The shortest is chosen and copied to `b`.
  70. func (p *PathData) copyInstruction(b []byte, cmd byte) int {
  71. n := len(p.coords)
  72. if n == 0 {
  73. if cmd == 'Z' || cmd == 'z' {
  74. b[0] = 'z'
  75. return 1
  76. }
  77. return 0
  78. }
  79. isRelCmd := cmd >= 'a'
  80. // get new cursor coordinates
  81. di := 0
  82. if (cmd == 'M' || cmd == 'm' || cmd == 'L' || cmd == 'l' || cmd == 'T' || cmd == 't') && n%2 == 0 {
  83. di = 2
  84. // reprint M always, as the first pair is a move but subsequent pairs are L
  85. if cmd == 'M' || cmd == 'm' {
  86. p.state.cmd = byte(0)
  87. }
  88. } else if cmd == 'H' || cmd == 'h' || cmd == 'V' || cmd == 'v' {
  89. di = 1
  90. } else if (cmd == 'S' || cmd == 's' || cmd == 'Q' || cmd == 'q') && n%4 == 0 {
  91. di = 4
  92. } else if (cmd == 'C' || cmd == 'c') && n%6 == 0 {
  93. di = 6
  94. } else if (cmd == 'A' || cmd == 'a') && n%7 == 0 {
  95. di = 7
  96. } else {
  97. return 0
  98. }
  99. j := 0
  100. origCmd := cmd
  101. ax, ay := 0.0, 0.0
  102. for i := 0; i < n; i += di {
  103. // subsequent coordinate pairs for M are really L
  104. if i > 0 && (origCmd == 'M' || origCmd == 'm') {
  105. origCmd = 'L' + (origCmd - 'M')
  106. }
  107. cmd = origCmd
  108. coords := p.coords[i : i+di]
  109. coordFloats := p.coordFloats[i : i+di]
  110. if cmd == 'H' || cmd == 'h' {
  111. ax = coordFloats[di-1]
  112. if isRelCmd {
  113. ay = 0
  114. } else {
  115. ay = p.y
  116. }
  117. } else if cmd == 'V' || cmd == 'v' {
  118. if isRelCmd {
  119. ax = 0
  120. } else {
  121. ax = p.x
  122. }
  123. ay = coordFloats[di-1]
  124. } else {
  125. ax = coordFloats[di-2]
  126. ay = coordFloats[di-1]
  127. }
  128. // switch from L to H or V whenever possible
  129. if cmd == 'L' || cmd == 'l' {
  130. if isRelCmd {
  131. if coordFloats[0] == 0 {
  132. cmd = 'v'
  133. coords = coords[1:]
  134. coordFloats = coordFloats[1:]
  135. } else if coordFloats[1] == 0 {
  136. cmd = 'h'
  137. coords = coords[:1]
  138. coordFloats = coordFloats[:1]
  139. }
  140. } else {
  141. if coordFloats[0] == p.x {
  142. cmd = 'V'
  143. coords = coords[1:]
  144. coordFloats = coordFloats[1:]
  145. } else if coordFloats[1] == p.y {
  146. cmd = 'H'
  147. coords = coords[:1]
  148. coordFloats = coordFloats[:1]
  149. }
  150. }
  151. }
  152. // make a current and alternated path with absolute/relative altered
  153. var curState, altState PathDataState
  154. curState = p.shortenCurPosInstruction(cmd, coords)
  155. if isRelCmd {
  156. altState = p.shortenAltPosInstruction(cmd-'a'+'A', coordFloats, p.x, p.y)
  157. } else {
  158. altState = p.shortenAltPosInstruction(cmd-'A'+'a', coordFloats, -p.x, -p.y)
  159. }
  160. // choose shortest, relative or absolute path?
  161. if len(p.altBuffer) < len(p.curBuffer) {
  162. j += copy(b[j:], p.altBuffer)
  163. p.state = altState
  164. } else {
  165. j += copy(b[j:], p.curBuffer)
  166. p.state = curState
  167. }
  168. if isRelCmd {
  169. p.x += ax
  170. p.y += ay
  171. } else {
  172. p.x = ax
  173. p.y = ay
  174. }
  175. }
  176. return j
  177. }
  178. // shortenCurPosInstruction only minifies the coordinates.
  179. func (p *PathData) shortenCurPosInstruction(cmd byte, coords [][]byte) PathDataState {
  180. state := p.state
  181. p.curBuffer = p.curBuffer[:0]
  182. if cmd != state.cmd && !(state.cmd == 'M' && cmd == 'L' || state.cmd == 'm' && cmd == 'l') {
  183. p.curBuffer = append(p.curBuffer, cmd)
  184. state.cmd = cmd
  185. state.prevDigit = false
  186. state.prevDigitIsInt = false
  187. }
  188. for i, coord := range coords {
  189. isFlag := false
  190. // Arc has boolean flags that can only be 0 or 1. Setting isFlag prevents from adding a dot before a zero (instead of a space). However, when the dot already was there, the command is malformed and could make the path longer than before, introducing bugs.
  191. if (cmd == 'A' || cmd == 'a') && (i%7 == 3 || i%7 == 4) && coord[0] != '.' {
  192. isFlag = true
  193. }
  194. coord = minify.Number(coord, p.o.Decimals)
  195. state.copyNumber(&p.curBuffer, coord, isFlag)
  196. }
  197. return state
  198. }
  199. // shortenAltPosInstruction toggles the command between absolute / relative coordinates and minifies the coordinates.
  200. func (p *PathData) shortenAltPosInstruction(cmd byte, coordFloats []float64, x, y float64) PathDataState {
  201. state := p.state
  202. p.altBuffer = p.altBuffer[:0]
  203. if cmd != state.cmd && !(state.cmd == 'M' && cmd == 'L' || state.cmd == 'm' && cmd == 'l') {
  204. p.altBuffer = append(p.altBuffer, cmd)
  205. state.cmd = cmd
  206. state.prevDigit = false
  207. state.prevDigitIsInt = false
  208. }
  209. for i, f := range coordFloats {
  210. isFlag := false
  211. 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' {
  212. if i%2 == 0 {
  213. f += x
  214. } else {
  215. f += y
  216. }
  217. } else if cmd == 'H' || cmd == 'h' {
  218. f += x
  219. } else if cmd == 'V' || cmd == 'v' {
  220. f += y
  221. } else if cmd == 'A' || cmd == 'a' {
  222. if i%7 == 5 {
  223. f += x
  224. } else if i%7 == 6 {
  225. f += y
  226. } else if i%7 == 3 || i%7 == 4 {
  227. isFlag = true
  228. }
  229. }
  230. p.coordBuffer = strconvStdlib.AppendFloat(p.coordBuffer[:0], f, 'g', -1, 64)
  231. coord := minify.Number(p.coordBuffer, p.o.Decimals)
  232. state.copyNumber(&p.altBuffer, coord, isFlag)
  233. }
  234. return state
  235. }
  236. // copyNumber will copy a number to the destination buffer, taking into account space or dot insertion to guarantee the shortest pathdata.
  237. func (state *PathDataState) copyNumber(buffer *[]byte, coord []byte, isFlag bool) {
  238. if state.prevDigit && (coord[0] >= '0' && coord[0] <= '9' || coord[0] == '.' && state.prevDigitIsInt) {
  239. if coord[0] == '0' && !state.prevDigitIsInt {
  240. if isFlag {
  241. *buffer = append(*buffer, ' ', '0')
  242. state.prevDigitIsInt = true
  243. } else {
  244. *buffer = append(*buffer, '.', '0') // aggresively add dot so subsequent numbers could drop leading space
  245. // prevDigit stays true and prevDigitIsInt stays false
  246. }
  247. return
  248. }
  249. *buffer = append(*buffer, ' ')
  250. }
  251. state.prevDigit = true
  252. state.prevDigitIsInt = true
  253. if len(coord) > 2 && coord[len(coord)-2] == '0' && coord[len(coord)-1] == '0' {
  254. coord[len(coord)-2] = 'e'
  255. coord[len(coord)-1] = '2'
  256. state.prevDigitIsInt = false
  257. } else {
  258. for _, c := range coord {
  259. if c == '.' || c == 'e' || c == 'E' {
  260. state.prevDigitIsInt = false
  261. break
  262. }
  263. }
  264. }
  265. *buffer = append(*buffer, coord...)
  266. }