svg.go 11 KB

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