1 ;;; svg.el --- svg image creation functions
3 ;; Copyright (C) 2014 Free Software Foundation, Inc.
5 ;; Maintainer: Lars Magne Ingebrigtsen <larsi@gnus.org>
9 ;; This program is free software: you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
14 ;; This program is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
24 ;; This pacakge allows creating SVG images in Emacs. SVG images are
25 ;; vector-based XML files, really, so you could create them directly
26 ;; as XML. However, that's really tedious, as there are some fiddly
29 ;; In addition, the `svg-insert-image' function allows inserting an
30 ;; SVG image into a buffer that's updated "on the fly" as you
31 ;; add/alter elements to the image, which is useful when composing the
34 ;; Here are some usage examples:
36 ;; Create the base image structure, add a gradient spec, and insert it
38 ;; (setq svg (svg-create 800 800 :stroke "orange" :stroke-width 5))
39 ;; (svg-gradient svg "gradient" 'linear '(0 . "red") '(100 . "blue"))
40 ;; (save-excursion (goto-char (point-max)) (svg-insert-image svg))
42 ;; Then add various elements to the structure:
43 ;; (svg-rectangle svg 100 100 500 500 :gradient "gradient" :id "rec1")
44 ;; (svg-circle svg 500 500 100 :id "circle1")
45 ;; (svg-ellipse svg 100 100 50 90 :stroke "red" :id "ellipse1")
46 ;; (svg-line svg 100 190 50 100 :id "line1" :stroke "yellow")
47 ;; (svg-polyline svg '((200 . 100) (500 . 450) (80 . 100))
48 ;; :stroke "green" :id "poly1")
49 ;; (svg-polygon svg '((100 . 100) (200 . 150) (150 . 90))
50 ;; :stroke "blue" :fill "red" :id "gon1")
58 (defun svg-create (width height &rest args)
59 "Create a new, empty SVG image with dimentions WIDTHxHEIGHT.
60 ARGS can be used to provide `stroke' and `stroke-width' parameters to
61 any further elements added."
66 (xmlsn . "http://www.w3.org/2000/svg")
67 ,@(svg-arguments nil args))))
69 (defun svg-gradient (svg id type &rest stops)
70 "Add a gradient with ID to SVG.
71 TYPE is `linear' or `gradient'. STOPS is a list of percentage/color
87 (dom-node 'stop `((offset . ,(format "%s%%" (car stop)))
88 (stop-color . ,(cdr stop)))))
91 (defun svg-rectangle (svg x y width height &rest args)
92 "Create a rectangle on SVG."
100 ,@(svg-arguments svg args)))))
102 (defun svg-circle (svg x y radius &rest args)
103 "Create a circle of RADIUS on SVG.
104 X/Y denote the center of the circle."
111 ,@(svg-arguments svg args)))))
113 (defun svg-ellipse (svg x y x-radius y-radius &rest args)
114 "Create an ellipse of X-RADIUS/Y-RADIUS on SVG.
115 X/Y denote the center of the ellipse."
123 ,@(svg-arguments svg args)))))
125 (defun svg-line (svg x1 y1 x2 y2 &rest args)
126 "Create a line of starting in X1/Y1, ending at X2/Y2 on SVG."
134 ,@(svg-arguments svg args)))))
136 (defun svg-polyline (svg points &rest args)
137 "Create a polyline going through POINTS on SVG.
138 POINTS is a list of x/y pairs."
143 `((points . ,(mapconcat (lambda (pair)
144 (format "%s %s" (car pair) (cdr pair)))
147 ,@(svg-arguments svg args)))))
149 (defun svg-polygon (svg points &rest args)
150 "Create a polygon going through POINTS on SVG.
151 POINTS is a list of x/y pairs."
156 `((points . ,(mapconcat (lambda (pair)
157 (format "%s %s" (car pair) (cdr pair)))
160 ,@(svg-arguments svg args)))))
162 (defun svg-append (svg node)
163 (let ((old (and (dom-attr node 'id)
164 (dom-by-id svg (concat "\\`" (regexp-quote (dom-attr node 'id))
167 (dom-set-attributes old (dom-attributes node))
168 (dom-append-child svg node)))
169 (svg-possibly-update-image svg))
171 (defun svg-arguments (svg args)
172 (let ((stroke-width (or (plist-get args :stroke-width)
173 (dom-attr svg 'stroke-width)))
174 (stroke (or (plist-get args :stroke)
175 (dom-attr svg 'stroke)))
178 (push (cons 'stroke-width stroke-width) attr))
180 (push (cons 'stroke stroke) attr))
181 (when (plist-get args :gradient)
184 ;; We need a way to specify the gradient direction here...
189 (fill . ,(format "url(#%s)"
190 (plist-get args :gradient))))
192 (cl-loop for (key value) on args by #'cddr
193 unless (memq key '(:stroke :stroke-width :gradient))
194 ;; Drop the leading colon.
195 do (push (cons (intern (substring (symbol-name key) 1) obarray)
200 (defun svg-def (svg def)
202 (or (dom-by-tag svg 'defs)
203 (let ((node (dom-node 'defs)))
204 (dom-add-child-before svg node)
209 (defun svg-image (svg)
210 "Return an image object from SVG."
217 (defun svg-insert-image (svg)
218 "Insert SVG as an image at point.
219 If the SVG is later changed, the image will also be updated."
220 (let ((image (svg-image svg))
221 (marker (point-marker)))
223 (dom-set-attribute svg :image marker)))
225 (defun svg-possibly-update-image (svg)
226 (let ((marker (dom-attr svg :image)))
228 (buffer-live-p (marker-buffer marker)))
229 (with-current-buffer (marker-buffer marker)
230 (put-text-property marker (1+ marker) 'display (svg-image svg))))))
232 (defun svg-print (dom)
233 "Convert DOM into a string containing the xml representation."
234 (insert (format "<%s" (car dom)))
235 (dolist (attr (nth 1 dom))
236 ;; Ignore attributes that start with a colon.
237 (unless (= (aref (format "%s" (car attr)) 0) ?:)
238 (insert (format " %s=\"%s\"" (car attr) (cdr attr)))))
240 (dolist (elem (nthcdr 2 dom))
243 (insert (format "</%s>" (car dom))))