svg.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. // Package svg minifies SVG1.1 following the specifications at http://www.w3.org/TR/SVG11/.
  2. package svg
  3. import (
  4. "bytes"
  5. "io"
  6. "github.com/tdewolff/minify/v2"
  7. "github.com/tdewolff/minify/v2/css"
  8. minifyXML "github.com/tdewolff/minify/v2/xml"
  9. "github.com/tdewolff/parse/v2"
  10. "github.com/tdewolff/parse/v2/buffer"
  11. "github.com/tdewolff/parse/v2/xml"
  12. )
  13. var (
  14. voidBytes = []byte("/>")
  15. isBytes = []byte("=")
  16. spaceBytes = []byte(" ")
  17. cdataEndBytes = []byte("]]>")
  18. zeroBytes = []byte("0")
  19. cssMimeBytes = []byte("text/css")
  20. noneBytes = []byte("none")
  21. urlBytes = []byte("url(")
  22. )
  23. ////////////////////////////////////////////////////////////////
  24. // Minifier is an SVG minifier.
  25. type Minifier struct {
  26. KeepComments bool
  27. Precision int // number of significant digits
  28. newPrecision int // precision for new numbers
  29. }
  30. // Minify minifies SVG data, it reads from r and writes to w.
  31. func Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error {
  32. return (&Minifier{}).Minify(m, w, r, params)
  33. }
  34. // Minify minifies SVG data, it reads from r and writes to w.
  35. func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]string) error {
  36. o.newPrecision = o.Precision
  37. if o.newPrecision <= 0 || 15 < o.newPrecision {
  38. o.newPrecision = 15 // minimum number of digits a double can represent exactly
  39. }
  40. var tag Hash
  41. defaultStyleType := cssMimeBytes
  42. defaultStyleParams := map[string]string(nil)
  43. defaultInlineStyleParams := map[string]string{"inline": "1"}
  44. p := NewPathData(o)
  45. minifyBuffer := buffer.NewWriter(make([]byte, 0, 64))
  46. attrByteBuffer := make([]byte, 0, 64)
  47. z := parse.NewInput(r)
  48. defer z.Restore()
  49. l := xml.NewLexer(z)
  50. tb := NewTokenBuffer(z, l)
  51. for {
  52. t := *tb.Shift()
  53. switch t.TokenType {
  54. case xml.ErrorToken:
  55. if _, err := w.Write(nil); err != nil {
  56. return err
  57. }
  58. if l.Err() == io.EOF {
  59. return nil
  60. }
  61. return l.Err()
  62. case xml.CommentToken:
  63. if o.KeepComments {
  64. w.Write(t.Data)
  65. }
  66. case xml.DOCTYPEToken:
  67. if len(t.Text) > 0 && t.Text[len(t.Text)-1] == ']' {
  68. w.Write(t.Data)
  69. }
  70. case xml.TextToken:
  71. t.Data = parse.ReplaceMultipleWhitespaceAndEntities(t.Data, minifyXML.EntitiesMap, nil)
  72. t.Data = parse.TrimWhitespace(t.Data)
  73. if tag == Style && len(t.Data) > 0 {
  74. if err := m.MinifyMimetype(defaultStyleType, w, buffer.NewReader(t.Data), defaultStyleParams); err != nil {
  75. if err != minify.ErrNotExist {
  76. return minify.UpdateErrorPosition(err, z, t.Offset)
  77. }
  78. w.Write(t.Data)
  79. }
  80. } else {
  81. w.Write(t.Data)
  82. }
  83. case xml.CDATAToken:
  84. if tag == Style {
  85. minifyBuffer.Reset()
  86. if err := m.MinifyMimetype(defaultStyleType, minifyBuffer, buffer.NewReader(t.Text), defaultStyleParams); err == nil {
  87. t.Data = append(t.Data[:9], minifyBuffer.Bytes()...)
  88. t.Text = t.Data[9:]
  89. t.Data = append(t.Data, cdataEndBytes...)
  90. } else if err != minify.ErrNotExist {
  91. return minify.UpdateErrorPosition(err, z, t.Offset)
  92. }
  93. }
  94. var useText bool
  95. if t.Text, useText = xml.EscapeCDATAVal(&attrByteBuffer, t.Text); useText {
  96. t.Text = parse.ReplaceMultipleWhitespace(t.Text)
  97. t.Text = parse.TrimWhitespace(t.Text)
  98. w.Write(t.Text)
  99. } else {
  100. w.Write(t.Data)
  101. }
  102. case xml.StartTagPIToken:
  103. for {
  104. if t := *tb.Shift(); t.TokenType == xml.StartTagClosePIToken || t.TokenType == xml.ErrorToken {
  105. break
  106. }
  107. }
  108. case xml.StartTagToken:
  109. tag = t.Hash
  110. if tag == Metadata {
  111. t.Data = nil
  112. }
  113. if t.Data == nil {
  114. skipTag(tb)
  115. } else {
  116. w.Write(t.Data)
  117. }
  118. case xml.AttributeToken:
  119. if t.Text == nil { // data is nil when attribute has been removed
  120. continue
  121. }
  122. attr := t.Hash
  123. val := t.AttrVal
  124. if n, m := parse.Dimension(val); n+m == len(val) && attr != Version { // TODO: inefficient, temporary measure
  125. val, _ = o.shortenDimension(val)
  126. }
  127. if attr == Xml_Space && bytes.Equal(val, []byte("preserve")) ||
  128. tag == Svg && (attr == Version && bytes.Equal(val, []byte("1.1")) ||
  129. attr == X && bytes.Equal(val, zeroBytes) ||
  130. attr == Y && bytes.Equal(val, zeroBytes) ||
  131. attr == PreserveAspectRatio && bytes.Equal(val, []byte("xMidYMid meet")) ||
  132. attr == BaseProfile && bytes.Equal(val, noneBytes) ||
  133. attr == ContentScriptType && bytes.Equal(val, []byte("application/ecmascript")) ||
  134. attr == ContentStyleType && bytes.Equal(val, cssMimeBytes)) ||
  135. tag == Style && attr == Type && bytes.Equal(val, cssMimeBytes) {
  136. continue
  137. }
  138. w.Write(spaceBytes)
  139. w.Write(t.Text)
  140. w.Write(isBytes)
  141. if tag == Svg && attr == ContentStyleType {
  142. val = minify.Mediatype(val)
  143. defaultStyleType = val
  144. } else if attr == Style {
  145. minifyBuffer.Reset()
  146. if err := m.MinifyMimetype(defaultStyleType, minifyBuffer, buffer.NewReader(val), defaultInlineStyleParams); err == nil {
  147. val = minifyBuffer.Bytes()
  148. } else if err != minify.ErrNotExist {
  149. return minify.UpdateErrorPosition(err, z, t.Offset)
  150. }
  151. } else if attr == D {
  152. val = p.ShortenPathData(val)
  153. } else if attr == ViewBox {
  154. j := 0
  155. newVal := val[:0]
  156. for i := 0; i < 4; i++ {
  157. if i != 0 {
  158. if j >= len(val) || val[j] != ' ' && val[j] != ',' {
  159. newVal = append(newVal, val[j:]...)
  160. break
  161. }
  162. newVal = append(newVal, ' ')
  163. j++
  164. }
  165. if dim, n := o.shortenDimension(val[j:]); n > 0 {
  166. newVal = append(newVal, dim...)
  167. j += n
  168. } else {
  169. newVal = append(newVal, val[j:]...)
  170. break
  171. }
  172. }
  173. val = newVal
  174. } else if colorAttrMap[attr] && len(val) > 0 && (len(val) < 5 || !parse.EqualFold(val[:4], urlBytes)) {
  175. parse.ToLower(val)
  176. if val[0] == '#' {
  177. if name, ok := css.ShortenColorHex[string(val)]; ok {
  178. val = name
  179. } else if len(val) == 7 && val[1] == val[2] && val[3] == val[4] && val[5] == val[6] {
  180. val[2] = val[3]
  181. val[3] = val[5]
  182. val = val[:4]
  183. }
  184. } else if hex, ok := css.ShortenColorName[css.ToHash(val)]; ok {
  185. val = hex
  186. // } else if len(val) > 5 && bytes.Equal(val[:4], []byte("rgb(")) && val[len(val)-1] == ')' {
  187. // TODO: handle rgb(x, y, z) and hsl(x, y, z)
  188. }
  189. }
  190. // prefer single or double quotes depending on what occurs more often in value
  191. val = xml.EscapeAttrVal(&attrByteBuffer, val)
  192. w.Write(val)
  193. case xml.StartTagCloseToken:
  194. next := tb.Peek(0)
  195. skipExtra := false
  196. if next.TokenType == xml.TextToken && parse.IsAllWhitespace(next.Data) {
  197. next = tb.Peek(1)
  198. skipExtra = true
  199. }
  200. if next.TokenType == xml.EndTagToken {
  201. // collapse empty tags to single void tag
  202. tb.Shift()
  203. if skipExtra {
  204. tb.Shift()
  205. }
  206. w.Write(voidBytes)
  207. } else {
  208. w.Write(t.Data)
  209. }
  210. if tag == ForeignObject {
  211. printTag(w, tb, tag)
  212. }
  213. case xml.StartTagCloseVoidToken:
  214. tag = 0
  215. w.Write(t.Data)
  216. case xml.EndTagToken:
  217. tag = 0
  218. if len(t.Data) > 3+len(t.Text) {
  219. t.Data[2+len(t.Text)] = '>'
  220. t.Data = t.Data[:3+len(t.Text)]
  221. }
  222. w.Write(t.Data)
  223. }
  224. }
  225. }
  226. func (o *Minifier) shortenDimension(b []byte) ([]byte, int) {
  227. if n, m := parse.Dimension(b); n > 0 {
  228. unit := b[n : n+m]
  229. b = minify.Number(b[:n], o.Precision)
  230. if len(b) != 1 || b[0] != '0' {
  231. if m == 2 && unit[0] == 'p' && unit[1] == 'x' {
  232. unit = nil
  233. } else if m > 1 { // only percentage is length 1
  234. parse.ToLower(unit)
  235. }
  236. b = append(b, unit...)
  237. }
  238. return b, n + m
  239. }
  240. return b, 0
  241. }
  242. ////////////////////////////////////////////////////////////////
  243. func printTag(w io.Writer, tb *TokenBuffer, tag Hash) {
  244. level := 0
  245. inStartTag := false
  246. for {
  247. t := *tb.Peek(0)
  248. switch t.TokenType {
  249. case xml.ErrorToken:
  250. return
  251. case xml.StartTagToken:
  252. inStartTag = t.Hash == tag
  253. if t.Hash == tag {
  254. level++
  255. }
  256. case xml.StartTagCloseVoidToken:
  257. if inStartTag {
  258. if level == 0 {
  259. return
  260. }
  261. level--
  262. }
  263. case xml.EndTagToken:
  264. if t.Hash == tag {
  265. if level == 0 {
  266. return
  267. }
  268. level--
  269. }
  270. }
  271. w.Write(t.Data)
  272. tb.Shift()
  273. }
  274. }
  275. func skipTag(tb *TokenBuffer) {
  276. level := 0
  277. for {
  278. if t := *tb.Shift(); t.TokenType == xml.ErrorToken {
  279. break
  280. } else if t.TokenType == xml.EndTagToken || t.TokenType == xml.StartTagCloseVoidToken {
  281. if level == 0 {
  282. break
  283. }
  284. level--
  285. } else if t.TokenType == xml.StartTagToken {
  286. level++
  287. }
  288. }
  289. }