svg.el 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. ;;; svg.el --- SVG image creation functions -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2016-2017 Free Software Foundation, Inc.
  3. ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
  4. ;; Keywords: image
  5. ;; This file is part of GNU Emacs.
  6. ;; GNU Emacs is free software: you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; GNU Emacs is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;;; Code:
  18. (require 'cl-lib)
  19. (require 'xml)
  20. (require 'dom)
  21. (require 'subr-x)
  22. (defun svg-create (width height &rest args)
  23. "Create a new, empty SVG image with dimensions WIDTHxHEIGHT.
  24. ARGS can be used to provide `stroke' and `stroke-width' parameters to
  25. any further elements added."
  26. (dom-node 'svg
  27. `((width . ,width)
  28. (height . ,height)
  29. (version . "1.1")
  30. (xmlns . "http://www.w3.org/2000/svg")
  31. ,@(svg--arguments nil args))))
  32. (defun svg-gradient (svg id type stops)
  33. "Add a gradient with ID to SVG.
  34. TYPE is `linear' or `radial'. STOPS is a list of percentage/color
  35. pairs."
  36. (svg--def
  37. svg
  38. (apply
  39. 'dom-node
  40. (if (eq type 'linear)
  41. 'linearGradient
  42. 'radialGradient)
  43. `((id . ,id)
  44. (x1 . 0)
  45. (x2 . 0)
  46. (y1 . 0)
  47. (y2 . 1))
  48. (mapcar
  49. (lambda (stop)
  50. (dom-node 'stop `((offset . ,(format "%s%%" (car stop)))
  51. (stop-color . ,(cdr stop)))))
  52. stops))))
  53. (defun svg-rectangle (svg x y width height &rest args)
  54. "Create a rectangle on SVG, starting at position X/Y, of WIDTH/HEIGHT.
  55. ARGS is a plist of modifiers. Possible values are
  56. :stroke-width PIXELS. The line width.
  57. :stroke-color COLOR. The line color.
  58. :gradient ID. The gradient ID to use."
  59. (svg--append
  60. svg
  61. (dom-node 'rect
  62. `((width . ,width)
  63. (height . ,height)
  64. (x . ,x)
  65. (y . ,y)
  66. ,@(svg--arguments svg args)))))
  67. (defun svg-circle (svg x y radius &rest args)
  68. "Create a circle of RADIUS on SVG.
  69. X/Y denote the center of the circle."
  70. (svg--append
  71. svg
  72. (dom-node 'circle
  73. `((cx . ,x)
  74. (cy . ,y)
  75. (r . ,radius)
  76. ,@(svg--arguments svg args)))))
  77. (defun svg-ellipse (svg x y x-radius y-radius &rest args)
  78. "Create an ellipse of X-RADIUS/Y-RADIUS on SVG.
  79. X/Y denote the center of the ellipse."
  80. (svg--append
  81. svg
  82. (dom-node 'ellipse
  83. `((cx . ,x)
  84. (cy . ,y)
  85. (rx . ,x-radius)
  86. (ry . ,y-radius)
  87. ,@(svg--arguments svg args)))))
  88. (defun svg-line (svg x1 y1 x2 y2 &rest args)
  89. "Create a line of starting in X1/Y1, ending at X2/Y2 in SVG."
  90. (svg--append
  91. svg
  92. (dom-node 'line
  93. `((x1 . ,x1)
  94. (x2 . ,y1)
  95. (y1 . ,x2)
  96. (y2 . ,y2)
  97. ,@(svg--arguments svg args)))))
  98. (defun svg-polyline (svg points &rest args)
  99. "Create a polyline going through POINTS on SVG.
  100. POINTS is a list of x/y pairs."
  101. (svg--append
  102. svg
  103. (dom-node
  104. 'polyline
  105. `((points . ,(mapconcat (lambda (pair)
  106. (format "%s %s" (car pair) (cdr pair)))
  107. points
  108. ", "))
  109. ,@(svg--arguments svg args)))))
  110. (defun svg-polygon (svg points &rest args)
  111. "Create a polygon going through POINTS on SVG.
  112. POINTS is a list of x/y pairs."
  113. (svg--append
  114. svg
  115. (dom-node
  116. 'polygon
  117. `((points . ,(mapconcat (lambda (pair)
  118. (format "%s %s" (car pair) (cdr pair)))
  119. points
  120. ", "))
  121. ,@(svg--arguments svg args)))))
  122. (defun svg-embed (svg image image-type datap &rest args)
  123. "Insert IMAGE into the SVG structure.
  124. IMAGE should be a file name if DATAP is nil, and a binary string
  125. otherwise. IMAGE-TYPE should be a MIME image type, like
  126. \"image/jpeg\" or the like."
  127. (svg--append
  128. svg
  129. (dom-node
  130. 'image
  131. `((xlink:href . ,(svg--image-data image image-type datap))
  132. ,@(svg--arguments svg args)))))
  133. (defun svg-text (svg text &rest args)
  134. "Add TEXT to SVG."
  135. (svg--append
  136. svg
  137. (dom-node
  138. 'text
  139. `(,@(svg--arguments svg args))
  140. text)))
  141. (defun svg--append (svg node)
  142. (let ((old (and (dom-attr node 'id)
  143. (dom-by-id svg
  144. (concat "\\`" (regexp-quote (dom-attr node 'id))
  145. "\\'")))))
  146. (if old
  147. (setcdr (car old) (cdr node))
  148. (dom-append-child svg node)))
  149. (svg-possibly-update-image svg))
  150. (defun svg--image-data (image image-type datap)
  151. (with-temp-buffer
  152. (set-buffer-multibyte nil)
  153. (if datap
  154. (insert image)
  155. (insert-file-contents image))
  156. (base64-encode-region (point-min) (point-max) t)
  157. (goto-char (point-min))
  158. (insert "data:" image-type ";base64,")
  159. (buffer-string)))
  160. (defun svg--arguments (svg args)
  161. (let ((stroke-width (or (plist-get args :stroke-width)
  162. (dom-attr svg 'stroke-width)))
  163. (stroke-color (or (plist-get args :stroke-color)
  164. (dom-attr svg 'stroke-color)))
  165. (fill-color (plist-get args :fill-color))
  166. attr)
  167. (when stroke-width
  168. (push (cons 'stroke-width stroke-width) attr))
  169. (when stroke-color
  170. (push (cons 'stroke stroke-color) attr))
  171. (when fill-color
  172. (push (cons 'fill fill-color) attr))
  173. (when (plist-get args :gradient)
  174. (setq attr
  175. (append
  176. ;; We need a way to specify the gradient direction here...
  177. `((x1 . 0)
  178. (x2 . 0)
  179. (y1 . 0)
  180. (y2 . 1)
  181. (fill . ,(format "url(#%s)"
  182. (plist-get args :gradient))))
  183. attr)))
  184. (cl-loop for (key value) on args by #'cddr
  185. unless (memq key '(:stroke-color :stroke-width :gradient
  186. :fill-color))
  187. ;; Drop the leading colon.
  188. do (push (cons (intern (substring (symbol-name key) 1) obarray)
  189. value)
  190. attr))
  191. attr))
  192. (defun svg--def (svg def)
  193. (dom-append-child
  194. (or (dom-by-tag svg 'defs)
  195. (let ((node (dom-node 'defs)))
  196. (dom-add-child-before svg node)
  197. node))
  198. def)
  199. svg)
  200. (defun svg-image (svg)
  201. "Return an image object from SVG."
  202. (create-image
  203. (with-temp-buffer
  204. (svg-print svg)
  205. (buffer-string))
  206. 'svg t))
  207. (defun svg-insert-image (svg)
  208. "Insert SVG as an image at point.
  209. If the SVG is later changed, the image will also be updated."
  210. (let ((image (svg-image svg))
  211. (marker (point-marker)))
  212. (insert-image image)
  213. (dom-set-attribute svg :image marker)))
  214. (defun svg-possibly-update-image (svg)
  215. (let ((marker (dom-attr svg :image)))
  216. (when (and marker
  217. (buffer-live-p (marker-buffer marker)))
  218. (with-current-buffer (marker-buffer marker)
  219. (put-text-property marker (1+ marker) 'display (svg-image svg))))))
  220. (defun svg-print (dom)
  221. "Convert DOM into a string containing the xml representation."
  222. (if (stringp dom)
  223. (insert dom)
  224. (insert (format "<%s" (car dom)))
  225. (dolist (attr (nth 1 dom))
  226. ;; Ignore attributes that start with a colon.
  227. (unless (= (aref (format "%s" (car attr)) 0) ?:)
  228. (insert (format " %s=\"%s\"" (car attr) (cdr attr)))))
  229. (insert ">")
  230. (dolist (elem (nthcdr 2 dom))
  231. (insert " ")
  232. (svg-print elem))
  233. (insert (format "</%s>" (car dom)))))
  234. (defun svg-remove (svg id)
  235. "Remove the element identified by ID from SVG."
  236. (when-let ((node (car (dom-by-id
  237. svg
  238. (concat "\\`" (regexp-quote id)
  239. "\\'")))))
  240. (dom-remove-node svg node)))
  241. (provide 'svg)
  242. ;;; svg.el ends here