svg.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. // Package svg minifies SVG1.1 following the specifications at http://www.w3.org/TR/SVG11/.
  2. package svg // import "github.com/tdewolff/minify/svg"
  3. import (
  4. "bytes"
  5. "io"
  6. "github.com/tdewolff/minify"
  7. minifyCSS "github.com/tdewolff/minify/css"
  8. "github.com/tdewolff/parse"
  9. "github.com/tdewolff/parse/buffer"
  10. "github.com/tdewolff/parse/css"
  11. "github.com/tdewolff/parse/svg"
  12. "github.com/tdewolff/parse/xml"
  13. )
  14. var (
  15. voidBytes = []byte("/>")
  16. isBytes = []byte("=")
  17. spaceBytes = []byte(" ")
  18. cdataEndBytes = []byte("]]>")
  19. pathBytes = []byte("<path")
  20. dBytes = []byte("d")
  21. zeroBytes = []byte("0")
  22. cssMimeBytes = []byte("text/css")
  23. urlBytes = []byte("url(")
  24. )
  25. ////////////////////////////////////////////////////////////////
  26. // DefaultMinifier is the default minifier.
  27. var DefaultMinifier = &Minifier{Decimals: -1}
  28. // Minifier is an SVG minifier.
  29. type Minifier struct {
  30. Decimals int
  31. }
  32. // Minify minifies SVG data, it reads from r and writes to w.
  33. func Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error {
  34. return DefaultMinifier.Minify(m, w, r, params)
  35. }
  36. // Minify minifies SVG data, it reads from r and writes to w.
  37. func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]string) error {
  38. var tag svg.Hash
  39. defaultStyleType := cssMimeBytes
  40. defaultStyleParams := map[string]string(nil)
  41. defaultInlineStyleParams := map[string]string{"inline": "1"}
  42. p := NewPathData(o)
  43. minifyBuffer := buffer.NewWriter(make([]byte, 0, 64))
  44. attrByteBuffer := make([]byte, 0, 64)
  45. gStack := make([]bool, 0)
  46. l := xml.NewLexer(r)
  47. defer l.Restore()
  48. tb := NewTokenBuffer(l)
  49. for {
  50. t := *tb.Shift()
  51. SWITCH:
  52. switch t.TokenType {
  53. case xml.ErrorToken:
  54. if l.Err() == io.EOF {
  55. return nil
  56. }
  57. return l.Err()
  58. case xml.DOCTYPEToken:
  59. if len(t.Text) > 0 && t.Text[len(t.Text)-1] == ']' {
  60. if _, err := w.Write(t.Data); err != nil {
  61. return err
  62. }
  63. }
  64. case xml.TextToken:
  65. t.Data = parse.ReplaceMultipleWhitespace(parse.TrimWhitespace(t.Data))
  66. if tag == svg.Style && len(t.Data) > 0 {
  67. if err := m.MinifyMimetype(defaultStyleType, w, buffer.NewReader(t.Data), defaultStyleParams); err != nil {
  68. if err != minify.ErrNotExist {
  69. return err
  70. } else if _, err := w.Write(t.Data); err != nil {
  71. return err
  72. }
  73. }
  74. } else if _, err := w.Write(t.Data); err != nil {
  75. return err
  76. }
  77. case xml.CDATAToken:
  78. if tag == svg.Style {
  79. minifyBuffer.Reset()
  80. if err := m.MinifyMimetype(defaultStyleType, minifyBuffer, buffer.NewReader(t.Text), defaultStyleParams); err == nil {
  81. t.Data = append(t.Data[:9], minifyBuffer.Bytes()...)
  82. t.Text = t.Data[9:]
  83. t.Data = append(t.Data, cdataEndBytes...)
  84. } else if err != minify.ErrNotExist {
  85. return err
  86. }
  87. }
  88. var useText bool
  89. if t.Text, useText = xml.EscapeCDATAVal(&attrByteBuffer, t.Text); useText {
  90. t.Text = parse.ReplaceMultipleWhitespace(parse.TrimWhitespace(t.Text))
  91. if _, err := w.Write(t.Text); err != nil {
  92. return err
  93. }
  94. } else if _, err := w.Write(t.Data); err != nil {
  95. return err
  96. }
  97. case xml.StartTagPIToken:
  98. for {
  99. if t := *tb.Shift(); t.TokenType == xml.StartTagClosePIToken || t.TokenType == xml.ErrorToken {
  100. break
  101. }
  102. }
  103. case xml.StartTagToken:
  104. tag = t.Hash
  105. if containerTagMap[tag] { // skip empty containers
  106. i := 0
  107. for {
  108. next := tb.Peek(i)
  109. i++
  110. if next.TokenType == xml.EndTagToken && next.Hash == tag || next.TokenType == xml.StartTagCloseVoidToken || next.TokenType == xml.ErrorToken {
  111. for j := 0; j < i; j++ {
  112. tb.Shift()
  113. }
  114. break SWITCH
  115. } else if next.TokenType != xml.AttributeToken && next.TokenType != xml.StartTagCloseToken {
  116. break
  117. }
  118. }
  119. if tag == svg.G {
  120. if tb.Peek(0).TokenType == xml.StartTagCloseToken {
  121. gStack = append(gStack, false)
  122. tb.Shift()
  123. break
  124. }
  125. gStack = append(gStack, true)
  126. }
  127. } else if tag == svg.Metadata {
  128. skipTag(tb, tag)
  129. break
  130. } else if tag == svg.Line {
  131. o.shortenLine(tb, &t, p)
  132. } else if tag == svg.Rect && !o.shortenRect(tb, &t, p) {
  133. skipTag(tb, tag)
  134. break
  135. } else if tag == svg.Polygon || tag == svg.Polyline {
  136. o.shortenPoly(tb, &t, p)
  137. }
  138. if _, err := w.Write(t.Data); err != nil {
  139. return err
  140. }
  141. case xml.AttributeToken:
  142. if len(t.AttrVal) == 0 || t.Text == nil { // data is nil when attribute has been removed
  143. continue
  144. }
  145. attr := t.Hash
  146. val := t.AttrVal
  147. if n, m := parse.Dimension(val); n+m == len(val) && attr != svg.Version { // TODO: inefficient, temporary measure
  148. val, _ = o.shortenDimension(val)
  149. }
  150. if attr == svg.Xml_Space && bytes.Equal(val, []byte("preserve")) ||
  151. tag == svg.Svg && (attr == svg.Version && bytes.Equal(val, []byte("1.1")) ||
  152. attr == svg.X && bytes.Equal(val, []byte("0")) ||
  153. attr == svg.Y && bytes.Equal(val, []byte("0")) ||
  154. attr == svg.Width && bytes.Equal(val, []byte("100%")) ||
  155. attr == svg.Height && bytes.Equal(val, []byte("100%")) ||
  156. attr == svg.PreserveAspectRatio && bytes.Equal(val, []byte("xMidYMid meet")) ||
  157. attr == svg.BaseProfile && bytes.Equal(val, []byte("none")) ||
  158. attr == svg.ContentScriptType && bytes.Equal(val, []byte("application/ecmascript")) ||
  159. attr == svg.ContentStyleType && bytes.Equal(val, []byte("text/css"))) ||
  160. tag == svg.Style && attr == svg.Type && bytes.Equal(val, []byte("text/css")) {
  161. continue
  162. }
  163. if _, err := w.Write(spaceBytes); err != nil {
  164. return err
  165. }
  166. if _, err := w.Write(t.Text); err != nil {
  167. return err
  168. }
  169. if _, err := w.Write(isBytes); err != nil {
  170. return err
  171. }
  172. if tag == svg.Svg && attr == svg.ContentStyleType {
  173. val = minify.ContentType(val)
  174. defaultStyleType = val
  175. } else if attr == svg.Style {
  176. minifyBuffer.Reset()
  177. if err := m.MinifyMimetype(defaultStyleType, minifyBuffer, buffer.NewReader(val), defaultInlineStyleParams); err == nil {
  178. val = minifyBuffer.Bytes()
  179. } else if err != minify.ErrNotExist {
  180. return err
  181. }
  182. } else if attr == svg.D {
  183. val = p.ShortenPathData(val)
  184. } else if attr == svg.ViewBox {
  185. j := 0
  186. newVal := val[:0]
  187. for i := 0; i < 4; i++ {
  188. if i != 0 {
  189. if j >= len(val) || val[j] != ' ' && val[j] != ',' {
  190. newVal = append(newVal, val[j:]...)
  191. break
  192. }
  193. newVal = append(newVal, ' ')
  194. j++
  195. }
  196. if dim, n := o.shortenDimension(val[j:]); n > 0 {
  197. newVal = append(newVal, dim...)
  198. j += n
  199. } else {
  200. newVal = append(newVal, val[j:]...)
  201. break
  202. }
  203. }
  204. val = newVal
  205. } else if colorAttrMap[attr] && len(val) > 0 && (len(val) < 5 || !parse.EqualFold(val[:4], urlBytes)) {
  206. parse.ToLower(val)
  207. if val[0] == '#' {
  208. if name, ok := minifyCSS.ShortenColorHex[string(val)]; ok {
  209. val = name
  210. } else if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] {
  211. val[2] = val[3]
  212. val[3] = val[5]
  213. val = val[:4]
  214. }
  215. } else if hex, ok := minifyCSS.ShortenColorName[css.ToHash(val)]; ok {
  216. val = hex
  217. // } else if len(val) > 5 && bytes.Equal(val[:4], []byte("rgb(")) && val[len(val)-1] == ')' {
  218. // TODO: handle rgb(x, y, z) and hsl(x, y, z)
  219. }
  220. }
  221. // prefer single or double quotes depending on what occurs more often in value
  222. val = xml.EscapeAttrVal(&attrByteBuffer, val)
  223. if _, err := w.Write(val); err != nil {
  224. return err
  225. }
  226. case xml.StartTagCloseToken:
  227. next := tb.Peek(0)
  228. skipExtra := false
  229. if next.TokenType == xml.TextToken && parse.IsAllWhitespace(next.Data) {
  230. next = tb.Peek(1)
  231. skipExtra = true
  232. }
  233. if next.TokenType == xml.EndTagToken {
  234. // collapse empty tags to single void tag
  235. tb.Shift()
  236. if skipExtra {
  237. tb.Shift()
  238. }
  239. if _, err := w.Write(voidBytes); err != nil {
  240. return err
  241. }
  242. } else {
  243. if _, err := w.Write(t.Data); err != nil {
  244. return err
  245. }
  246. }
  247. case xml.StartTagCloseVoidToken:
  248. tag = 0
  249. if _, err := w.Write(t.Data); err != nil {
  250. return err
  251. }
  252. case xml.EndTagToken:
  253. tag = 0
  254. if t.Hash == svg.G && len(gStack) > 0 {
  255. if !gStack[len(gStack)-1] {
  256. gStack = gStack[:len(gStack)-1]
  257. break
  258. }
  259. gStack = gStack[:len(gStack)-1]
  260. }
  261. if len(t.Data) > 3+len(t.Text) {
  262. t.Data[2+len(t.Text)] = '>'
  263. t.Data = t.Data[:3+len(t.Text)]
  264. }
  265. if _, err := w.Write(t.Data); err != nil {
  266. return err
  267. }
  268. }
  269. }
  270. }
  271. func (o *Minifier) shortenDimension(b []byte) ([]byte, int) {
  272. if n, m := parse.Dimension(b); n > 0 {
  273. unit := b[n : n+m]
  274. b = minify.Number(b[:n], o.Decimals)
  275. if len(b) != 1 || b[0] != '0' {
  276. if m == 2 && unit[0] == 'p' && unit[1] == 'x' {
  277. unit = nil
  278. } else if m > 1 { // only percentage is length 1
  279. parse.ToLower(unit)
  280. }
  281. b = append(b, unit...)
  282. }
  283. return b, n + m
  284. }
  285. return b, 0
  286. }
  287. func (o *Minifier) shortenLine(tb *TokenBuffer, t *Token, p *PathData) {
  288. x1, y1, x2, y2 := zeroBytes, zeroBytes, zeroBytes, zeroBytes
  289. if attrs, replacee := tb.Attributes(svg.X1, svg.Y1, svg.X2, svg.Y2); replacee != nil {
  290. if attrs[0] != nil {
  291. x1 = minify.Number(attrs[0].AttrVal, o.Decimals)
  292. attrs[0].Text = nil
  293. }
  294. if attrs[1] != nil {
  295. y1 = minify.Number(attrs[1].AttrVal, o.Decimals)
  296. attrs[1].Text = nil
  297. }
  298. if attrs[2] != nil {
  299. x2 = minify.Number(attrs[2].AttrVal, o.Decimals)
  300. attrs[2].Text = nil
  301. }
  302. if attrs[3] != nil {
  303. y2 = minify.Number(attrs[3].AttrVal, o.Decimals)
  304. attrs[3].Text = nil
  305. }
  306. d := make([]byte, 0, 5+len(x1)+len(y1)+len(x2)+len(y2))
  307. d = append(d, 'M')
  308. d = append(d, x1...)
  309. d = append(d, ' ')
  310. d = append(d, y1...)
  311. d = append(d, 'L')
  312. d = append(d, x2...)
  313. d = append(d, ' ')
  314. d = append(d, y2...)
  315. d = append(d, 'z')
  316. d = p.ShortenPathData(d)
  317. t.Data = pathBytes
  318. replacee.Text = dBytes
  319. replacee.AttrVal = d
  320. }
  321. }
  322. func (o *Minifier) shortenRect(tb *TokenBuffer, t *Token, p *PathData) bool {
  323. if attrs, replacee := tb.Attributes(svg.X, svg.Y, svg.Width, svg.Height, svg.Rx, svg.Ry); replacee != nil && attrs[4] == nil && attrs[5] == nil {
  324. x, y, w, h := zeroBytes, zeroBytes, zeroBytes, zeroBytes
  325. if attrs[0] != nil {
  326. x = minify.Number(attrs[0].AttrVal, o.Decimals)
  327. attrs[0].Text = nil
  328. }
  329. if attrs[1] != nil {
  330. y = minify.Number(attrs[1].AttrVal, o.Decimals)
  331. attrs[1].Text = nil
  332. }
  333. if attrs[2] != nil {
  334. w = minify.Number(attrs[2].AttrVal, o.Decimals)
  335. attrs[2].Text = nil
  336. }
  337. if attrs[3] != nil {
  338. h = minify.Number(attrs[3].AttrVal, o.Decimals)
  339. attrs[3].Text = nil
  340. }
  341. if len(w) == 0 || w[0] == '0' || len(h) == 0 || h[0] == '0' {
  342. return false
  343. }
  344. d := make([]byte, 0, 6+2*len(x)+len(y)+len(w)+len(h))
  345. d = append(d, 'M')
  346. d = append(d, x...)
  347. d = append(d, ' ')
  348. d = append(d, y...)
  349. d = append(d, 'h')
  350. d = append(d, w...)
  351. d = append(d, 'v')
  352. d = append(d, h...)
  353. d = append(d, 'H')
  354. d = append(d, x...)
  355. d = append(d, 'z')
  356. d = p.ShortenPathData(d)
  357. t.Data = pathBytes
  358. replacee.Text = dBytes
  359. replacee.AttrVal = d
  360. }
  361. return true
  362. }
  363. func (o *Minifier) shortenPoly(tb *TokenBuffer, t *Token, p *PathData) {
  364. if attrs, replacee := tb.Attributes(svg.Points); replacee != nil && attrs[0] != nil {
  365. points := attrs[0].AttrVal
  366. i := 0
  367. for i < len(points) && !(points[i] == ' ' || points[i] == ',' || points[i] == '\n' || points[i] == '\r' || points[i] == '\t') {
  368. i++
  369. }
  370. for i < len(points) && (points[i] == ' ' || points[i] == ',' || points[i] == '\n' || points[i] == '\r' || points[i] == '\t') {
  371. i++
  372. }
  373. for i < len(points) && !(points[i] == ' ' || points[i] == ',' || points[i] == '\n' || points[i] == '\r' || points[i] == '\t') {
  374. i++
  375. }
  376. endMoveTo := i
  377. for i < len(points) && (points[i] == ' ' || points[i] == ',' || points[i] == '\n' || points[i] == '\r' || points[i] == '\t') {
  378. i++
  379. }
  380. startLineTo := i
  381. if i == len(points) {
  382. return
  383. }
  384. d := make([]byte, 0, len(points)+3)
  385. d = append(d, 'M')
  386. d = append(d, points[:endMoveTo]...)
  387. d = append(d, 'L')
  388. d = append(d, points[startLineTo:]...)
  389. if t.Hash == svg.Polygon {
  390. d = append(d, 'z')
  391. }
  392. d = p.ShortenPathData(d)
  393. t.Data = pathBytes
  394. replacee.Text = dBytes
  395. replacee.AttrVal = d
  396. }
  397. }
  398. ////////////////////////////////////////////////////////////////
  399. func skipTag(tb *TokenBuffer, tag svg.Hash) {
  400. for {
  401. if t := *tb.Shift(); (t.TokenType == xml.EndTagToken || t.TokenType == xml.StartTagCloseVoidToken) && t.Hash == tag || t.TokenType == xml.ErrorToken {
  402. break
  403. }
  404. }
  405. }