pathdata.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. package svg
  2. import (
  3. "math"
  4. strconvStdlib "strconv"
  5. "github.com/tdewolff/minify/v2"
  6. "github.com/tdewolff/parse/v2"
  7. "github.com/tdewolff/parse/v2/strconv"
  8. )
  9. // PathData represents a path data string.
  10. type PathData struct {
  11. o *Minifier
  12. x, y float64
  13. x0, y0 float64
  14. coords [][]byte
  15. coordFloats []float64
  16. cx, cy float64 // last control point for cubic bezier
  17. qx, qy float64 // last control point for quadratic bezier
  18. state PathDataState
  19. curBuffer []byte
  20. altBuffer []byte
  21. coordBuffer []byte
  22. }
  23. // PathDataState is the state of the current path.
  24. type PathDataState struct {
  25. cmd byte
  26. prevDigit bool
  27. prevDigitIsInt bool
  28. prevFlag bool
  29. }
  30. // NewPathData returns a new PathData.
  31. func NewPathData(o *Minifier) *PathData {
  32. return &PathData{
  33. o: o,
  34. cx: math.NaN(),
  35. cy: math.NaN(),
  36. qx: math.NaN(),
  37. qy: math.NaN(),
  38. }
  39. }
  40. var pathCmds = map[byte]bool{
  41. 'M': true,
  42. 'm': true,
  43. 'L': true,
  44. 'l': true,
  45. 'H': true,
  46. 'h': true,
  47. 'V': true,
  48. 'v': true,
  49. 'Q': true,
  50. 'q': true,
  51. 'T': true,
  52. 't': true,
  53. 'C': true,
  54. 'c': true,
  55. 'S': true,
  56. 's': true,
  57. 'A': true,
  58. 'a': true,
  59. 'Z': true,
  60. 'z': true,
  61. }
  62. // ShortenPathData takes a full pathdata string and returns a shortened version. The original string is overwritten.
  63. // It parses all commands (M, A, Z, ...) and coordinates (numbers) and calls copyInstruction for each command.
  64. func (p *PathData) ShortenPathData(b []byte) []byte {
  65. if 100000 < len(b) {
  66. // prevent extremely long paths for being too costly (OSS-Fuzz)
  67. return b
  68. }
  69. var cmd byte
  70. p.x, p.y = 0.0, 0.0
  71. p.coords = p.coords[:0]
  72. p.coordFloats = p.coordFloats[:0]
  73. p.state = PathDataState{}
  74. j := 0
  75. for i := 0; i < len(b); i++ {
  76. c := b[i]
  77. if c == ' ' || c == ',' || c == '\n' || c == '\r' || c == '\t' {
  78. continue
  79. } else if pathCmds[c] && (cmd == 0 || cmd != c || c == 'M' || c == 'm') { // any command
  80. if cmd != 0 {
  81. j += p.copyInstruction(b[j:], cmd)
  82. }
  83. cmd = c
  84. p.coords = p.coords[:0]
  85. p.coordFloats = p.coordFloats[:0]
  86. } else if (cmd == 'A' || cmd == 'a') && (len(p.coordFloats)%7 == 3 || len(p.coordFloats)%7 == 4) {
  87. // boolean flags for arc command
  88. if c == '1' {
  89. p.coords = append(p.coords, b[i:i+1])
  90. p.coordFloats = append(p.coordFloats, 1.0)
  91. } else if c == '0' {
  92. p.coords = append(p.coords, b[i:i+1])
  93. p.coordFloats = append(p.coordFloats, 0.0)
  94. } else {
  95. cmd = 0 // bad format, don't minify
  96. }
  97. } else if n := parse.Number(b[i:]); n > 0 {
  98. f, _ := strconv.ParseFloat(b[i : i+n])
  99. p.coords = append(p.coords, b[i:i+n])
  100. p.coordFloats = append(p.coordFloats, f)
  101. i += n - 1
  102. }
  103. }
  104. if cmd == 0 {
  105. return b
  106. }
  107. j += p.copyInstruction(b[j:], cmd)
  108. return b[:j]
  109. }
  110. // 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.
  111. // 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, i.e. b is the destination and is not read from.
  112. func (p *PathData) copyInstruction(b []byte, cmd byte) int {
  113. n := len(p.coords)
  114. if n == 0 {
  115. if cmd == 'Z' || cmd == 'z' {
  116. p.x = p.x0
  117. p.y = p.y0
  118. b[0] = 'z'
  119. return 1
  120. }
  121. return 0
  122. }
  123. isRelCmd := cmd >= 'a'
  124. // get new cursor coordinates
  125. di := 0
  126. if (cmd == 'M' || cmd == 'm' || cmd == 'L' || cmd == 'l' || cmd == 'T' || cmd == 't') && n%2 == 0 {
  127. di = 2
  128. // reprint M always, as the first pair is a move but subsequent pairs are L
  129. if cmd == 'M' || cmd == 'm' {
  130. p.state.cmd = byte(0)
  131. }
  132. } else if cmd == 'H' || cmd == 'h' || cmd == 'V' || cmd == 'v' {
  133. di = 1
  134. } else if (cmd == 'S' || cmd == 's' || cmd == 'Q' || cmd == 'q') && n%4 == 0 {
  135. di = 4
  136. } else if (cmd == 'C' || cmd == 'c') && n%6 == 0 {
  137. di = 6
  138. } else if (cmd == 'A' || cmd == 'a') && n%7 == 0 {
  139. di = 7
  140. } else {
  141. return 0
  142. }
  143. j := 0
  144. origCmd := cmd
  145. for i := 0; i < n; i += di {
  146. // subsequent coordinate pairs for M are really L
  147. if i > 0 && (origCmd == 'M' || origCmd == 'm') {
  148. origCmd = 'L' + (origCmd - 'M')
  149. }
  150. cmd = origCmd
  151. coords := p.coords[i : i+di]
  152. coordFloats := p.coordFloats[i : i+di]
  153. // set next coordinate
  154. var ax, ay float64
  155. if cmd == 'H' || cmd == 'h' {
  156. ax = coordFloats[di-1]
  157. if isRelCmd {
  158. ax += p.x
  159. }
  160. ay = p.y
  161. } else if cmd == 'V' || cmd == 'v' {
  162. ax = p.x
  163. ay = coordFloats[di-1]
  164. if isRelCmd {
  165. ay += p.y
  166. }
  167. } else {
  168. ax = coordFloats[di-2]
  169. ay = coordFloats[di-1]
  170. if isRelCmd {
  171. ax += p.x
  172. ay += p.y
  173. }
  174. }
  175. // switch from C to S whenever possible
  176. if cmd == 'C' || cmd == 'c' || cmd == 'S' || cmd == 's' {
  177. if math.IsNaN(p.cx) {
  178. p.cx, p.cy = p.x, p.y
  179. } else {
  180. p.cx, p.cy = 2*p.x-p.cx, 2*p.y-p.cy
  181. }
  182. var cp1x, cp1y float64
  183. cp2x, cp2y := coordFloats[di-4], coordFloats[di-3]
  184. if isRelCmd {
  185. cp2x += p.x
  186. cp2y += p.y
  187. }
  188. if cmd == 'C' || cmd == 'c' {
  189. cp1x, cp1y = coordFloats[di-6], coordFloats[di-5]
  190. if isRelCmd {
  191. cp1x += p.x
  192. cp1y += p.y
  193. }
  194. if cp1x == p.cx && cp1y == p.cy {
  195. if isRelCmd {
  196. cmd = 's'
  197. } else {
  198. cmd = 'S'
  199. }
  200. coords = coords[2:]
  201. coordFloats = coordFloats[2:]
  202. }
  203. } else {
  204. cp1x, cp1y = p.cx, p.cy
  205. }
  206. // if control points overlap begin/end points, this is a straight line
  207. // even though if the control points would be along the straight line, we won't minify that as the control points influence the speed along the curve (important for dashes for example)
  208. // only change to a lines if we start with s or S and none follow
  209. if (cmd == 'C' || cmd == 'c' || i == 0 && i+di >= n) && (cp1x == p.x && cp1y == p.y || cp1x == ax && cp1y == ay) && (cp2x == p.x && cp2y == p.y || cp2x == ax && cp2y == ay) {
  210. if isRelCmd {
  211. cmd = 'l'
  212. } else {
  213. cmd = 'L'
  214. }
  215. coords = coords[len(coords)-2:]
  216. coordFloats = coordFloats[len(coordFloats)-2:]
  217. cp2x, cp2y = math.NaN(), math.NaN()
  218. }
  219. p.cx, p.cy = cp2x, cp2y
  220. } else {
  221. p.cx, p.cy = math.NaN(), math.NaN()
  222. }
  223. // switch from Q to T whenever possible
  224. if cmd == 'Q' || cmd == 'q' || cmd == 'T' || cmd == 't' {
  225. if math.IsNaN(p.qx) {
  226. p.qx, p.qy = p.x, p.y
  227. } else {
  228. p.qx, p.qy = 2*p.x-p.qx, 2*p.y-p.qy
  229. }
  230. var cpx, cpy float64
  231. if cmd == 'Q' || cmd == 'q' {
  232. cpx, cpy = coordFloats[di-4], coordFloats[di-3]
  233. if isRelCmd {
  234. cpx += p.x
  235. cpy += p.y
  236. }
  237. if cpx == p.qx && cpy == p.qy {
  238. if isRelCmd {
  239. cmd = 't'
  240. } else {
  241. cmd = 'T'
  242. }
  243. coords = coords[2:]
  244. coordFloats = coordFloats[2:]
  245. }
  246. } else {
  247. cpx, cpy = p.qx, p.qy
  248. }
  249. // if control point overlaps begin/end points, this is a straight line
  250. // even if the control point would be along the straight line, we won't minify that as the control point influences the speed along the curve (important for dashes for example)
  251. // only change to line if we start with t or T and none follow
  252. if (cmd == 'Q' || cmd == 'q' || i == 0 && i+di >= n) && (cpx == p.x && cpy == p.y || cpx == ax && cpy == ay) {
  253. if isRelCmd {
  254. cmd = 'l'
  255. } else {
  256. cmd = 'L'
  257. }
  258. coords = coords[len(coords)-2:]
  259. coordFloats = coordFloats[len(coordFloats)-2:]
  260. cpx, cpy = math.NaN(), math.NaN()
  261. }
  262. p.qx, p.qy = cpx, cpy
  263. } else {
  264. p.qx, p.qy = math.NaN(), math.NaN()
  265. }
  266. // switch from L to H or V whenever possible
  267. if cmd == 'L' || cmd == 'l' {
  268. if ax == p.x && ay == p.y {
  269. continue
  270. } else if ax == p.x {
  271. if isRelCmd {
  272. cmd = 'v'
  273. } else {
  274. cmd = 'V'
  275. }
  276. coords = coords[1:]
  277. coordFloats = coordFloats[1:]
  278. } else if ay == p.y {
  279. if isRelCmd {
  280. cmd = 'h'
  281. } else {
  282. cmd = 'H'
  283. }
  284. coords = coords[:1]
  285. coordFloats = coordFloats[:1]
  286. }
  287. }
  288. // make a current and alternated path with absolute/relative altered
  289. var curState, altState PathDataState
  290. curState = p.shortenCurPosInstruction(cmd, coords)
  291. if isRelCmd {
  292. altState = p.shortenAltPosInstruction(cmd-'a'+'A', coordFloats, p.x, p.y)
  293. } else {
  294. altState = p.shortenAltPosInstruction(cmd-'A'+'a', coordFloats, -p.x, -p.y)
  295. }
  296. // choose shortest, relative or absolute path?
  297. if len(p.altBuffer) < len(p.curBuffer) {
  298. j += copy(b[j:], p.altBuffer)
  299. p.state = altState
  300. } else {
  301. j += copy(b[j:], p.curBuffer)
  302. p.state = curState
  303. }
  304. p.x = ax
  305. p.y = ay
  306. if i == 0 && (origCmd == 'M' || origCmd == 'm') {
  307. p.x0 = p.x
  308. p.y0 = p.y
  309. }
  310. }
  311. return j
  312. }
  313. // shortenCurPosInstruction only minifies the coordinates.
  314. func (p *PathData) shortenCurPosInstruction(cmd byte, coords [][]byte) PathDataState {
  315. state := p.state
  316. p.curBuffer = p.curBuffer[:0]
  317. if cmd != state.cmd && !(state.cmd == 'M' && cmd == 'L' || state.cmd == 'm' && cmd == 'l') {
  318. p.curBuffer = append(p.curBuffer, cmd)
  319. state.cmd = cmd
  320. state.prevDigit = false
  321. state.prevDigitIsInt = false
  322. }
  323. for i, coord := range coords {
  324. // Arc has boolean flags that can only be 0 or 1. copyFlag 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.
  325. if (cmd == 'A' || cmd == 'a') && (i%7 == 3 || i%7 == 4) {
  326. state.copyFlag(&p.curBuffer, coord[0] == '1')
  327. continue
  328. }
  329. coord = minify.Number(coord, p.o.Precision)
  330. state.copyNumber(&p.curBuffer, coord)
  331. }
  332. return state
  333. }
  334. // shortenAltPosInstruction toggles the command between absolute / relative coordinates and minifies the coordinates.
  335. func (p *PathData) shortenAltPosInstruction(cmd byte, coordFloats []float64, x, y float64) PathDataState {
  336. state := p.state
  337. p.altBuffer = p.altBuffer[:0]
  338. if cmd != state.cmd && !(state.cmd == 'M' && cmd == 'L' || state.cmd == 'm' && cmd == 'l') {
  339. p.altBuffer = append(p.altBuffer, cmd)
  340. state.cmd = cmd
  341. state.prevDigit = false
  342. state.prevDigitIsInt = false
  343. }
  344. for i, f := range coordFloats {
  345. 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' {
  346. if i%2 == 0 {
  347. f += x
  348. } else {
  349. f += y
  350. }
  351. } else if cmd == 'H' || cmd == 'h' {
  352. f += x
  353. } else if cmd == 'V' || cmd == 'v' {
  354. f += y
  355. } else if cmd == 'A' || cmd == 'a' {
  356. if i%7 == 5 {
  357. f += x
  358. } else if i%7 == 6 {
  359. f += y
  360. } else if i%7 == 3 || i%7 == 4 {
  361. state.copyFlag(&p.altBuffer, f == 1.0)
  362. continue
  363. }
  364. }
  365. p.coordBuffer = strconvStdlib.AppendFloat(p.coordBuffer[:0], f, 'g', -1, 64)
  366. coord := minify.Number(p.coordBuffer, p.o.newPrecision)
  367. state.copyNumber(&p.altBuffer, coord)
  368. }
  369. return state
  370. }
  371. // copyNumber will copy a number to the destination buffer, taking into account space or dot insertion to guarantee the shortest pathdata.
  372. func (state *PathDataState) copyNumber(buffer *[]byte, coord []byte) {
  373. if state.prevDigit && (coord[0] >= '0' && coord[0] <= '9' || coord[0] == '.' && state.prevDigitIsInt) {
  374. if coord[0] == '0' && !state.prevDigitIsInt {
  375. *buffer = append(*buffer, '.', '0') // aggresively add dot so subsequent numbers could drop leading space
  376. // prevDigit stays true and prevDigitIsInt stays false
  377. return
  378. }
  379. *buffer = append(*buffer, ' ')
  380. }
  381. state.prevDigit = true
  382. state.prevDigitIsInt = true
  383. if len(coord) > 2 && coord[len(coord)-2] == '0' && coord[len(coord)-1] == '0' {
  384. coord[len(coord)-2] = 'e'
  385. coord[len(coord)-1] = '2'
  386. state.prevDigitIsInt = false
  387. } else {
  388. for _, c := range coord {
  389. if c == '.' || c == 'e' || c == 'E' {
  390. state.prevDigitIsInt = false
  391. break
  392. }
  393. }
  394. }
  395. *buffer = append(*buffer, coord...)
  396. state.prevFlag = false
  397. }
  398. func (state *PathDataState) copyFlag(buffer *[]byte, flag bool) {
  399. if !state.prevFlag {
  400. if flag {
  401. *buffer = append(*buffer, ' ', '1')
  402. } else {
  403. *buffer = append(*buffer, ' ', '0')
  404. }
  405. } else {
  406. if flag {
  407. *buffer = append(*buffer, '1')
  408. } else {
  409. *buffer = append(*buffer, '0')
  410. }
  411. }
  412. state.prevFlag = true
  413. state.prevDigit = false
  414. state.prevDigitIsInt = false
  415. }