changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / lisp/lib/dat/xml/svg.lisp

changeset 698: 96958d3eb5b0
parent: 2db3aa3a1b31
author: Richard Westhaver <ellis@rwest.io>
date: Fri, 04 Oct 2024 22:04:59 -0400
permissions: -rw-r--r--
description: fixes
1 ;;; dat/xml/svg.lisp --- SVG data encoders
2 
3 ;; based on https://github.com/orthecreedence/cl-svg-polygon
4 
5 ;;; Code:
6 (in-package :dat/svg)
7 
8 ;;; MATRIX
9 (defun id-matrix (dims)
10  "Return a square identity matrix with the specified "
11  (let ((array (make-array (* dims dims) :initial-element 0.0 :element-type 'single-float)))
12  (dotimes (d dims)
13  (setf (aref array (* d (1+ dims))) 1.0))
14  array))
15 
16 (defun mat* (m1 m2)
17  "Multiply 3x3 matrices m1 by m2."
18  (let ((new (make-array 9 :initial-element 0.0 :element-type 'single-float)))
19  (dotimes (x 3)
20  (dotimes (y 3)
21  (let ((prod (+ (* (aref m1 (* x 3)) (aref m2 y))
22  (* (aref m1 (+ (* x 3) 1)) (aref m2 (+ y 3)))
23  (* (aref m1 (+ (* x 3) 2)) (aref m2 (+ y 6))))))
24  (setf (aref new (+ y (* x 3))) (coerce prod 'single-float)))))
25  new))
26 
27 (defun matv* (m v)
28  "Multiple a matrix by a vector, return the resulting vector."
29  (let ((new (make-list 3))
30  (vx (car v))
31  (vy (cadr v))
32  (vz 1))
33  (dotimes (i 3)
34  (setf (nth i new) (+ (* vx (aref m (* i 3)))
35  (* vy (aref m (+ (* i 3) 1)))
36  (* vz (aref m (+ (* i 3) 2))))))
37  new))
38 
39 (defun m-rotate (degrees &key reverse)
40  "Generate a rotation matrix."
41  (let* ((matrix (id-matrix 3))
42  (angle-rad (* (mod degrees 360) (/ PI 180)))
43  (cos (coerce (cos angle-rad) 'single-float))
44  (sin (coerce (sin angle-rad) 'single-float)))
45  (setf (aref matrix 0) cos
46  (aref matrix 1) (if reverse sin (- sin))
47  (aref matrix 3) (if reverse (- sin) sin)
48  (aref matrix 4) cos)
49  matrix))
50 
51 (defun m-scale (x y)
52  "Generate a scaling matrix."
53  (let ((matrix (id-matrix 3)))
54  (setf (aref matrix 0) (coerce x 'single-float)
55  (aref matrix 4) (coerce y 'single-float))
56  matrix))
57 
58 (defun m-translate (x y)
59  "Generate a translation matrix."
60  (let ((translatrix (id-matrix 3)))
61  (setf (aref translatrix 2) (coerce x 'single-float)
62  (aref translatrix 5) (coerce y 'single-float))
63  translatrix))
64 
65 (defun m-skew (degrees &key (axis :x))
66  "Generate a skew matrix along the :axis axis (:x or :y)."
67  (let ((matrix (id-matrix 3))
68  (angle-rad (* (mod degrees 360) (/ PI 180)))
69  (idx (if (equal axis :x) 1 3)))
70  (setf (aref matrix idx) (coerce (tan angle-rad) 'single-float))
71  matrix))
72 
73 ;;; VECTOR
74 (defun norm (v)
75  "Calculate a vector norm."
76  (expt (loop for x in v sum (expt x 2)) .5))
77 
78 (defun normalize (v)
79  "Normalize a 2D vector"
80  (let ((x (car v))
81  (y (cadr v)))
82  (let ((norm (norm v)))
83  (list (/ x norm) (/ y norm)))))
84 
85 (defun dot-prod (v1 v2)
86  "Give the dot product of two 2D vectors."
87  (+ (* (car v1) (car v2))
88  (* (cadr v1) (cadr v2))))
89 
90 ;;; TRANSFORMATIONS
91 (defun parse-transform (transform)
92  "Turn a transform(...) into an easily-parsable list structure."
93  ;; convert "translate(-10,-20) scale(2) rotate(45) translate(5,10)" into
94  ;; "(translate -10 -20) (scale 2) (rotate 45) (translate 5 10)"
95  ;; (ie read-from-string'able)
96  (let* ((transform (cl-ppcre::regex-replace-all "([a-z]+)\\(" transform "(\\1 "))
97  (transform (cl-ppcre::regex-replace-all "," transform " ")))
98  (read-from-string (format nil "( ~a )" transform))))
99 
100 (defun get-transformations (object groups)
101  "Given an SVG object and a tree of groups, grab all transformations, starting
102  from the top down, into a flat list so they can be applied sequentially."
103  (let ((object-transform (getf object :transform))
104  (object-group (getf object :group))
105  (transformations nil))
106  (labels ((traverse-groups (path groups)
107  (dolist (group groups)
108  (when (eql (car (getf group :group)) (car path))
109  (let* ((groups (getf group :groups))
110  (transform (getf group :transform))
111  (transform (if (listp transform) (car transform) transform)))
112  (when transform
113  (push transform transformations))
114  (when groups
115  (traverse-groups (cdr path) groups)))))))
116  (traverse-groups object-group groups))
117  (when object-transform
118  (push object-transform transformations))
119  transformations))
120 
121 (defun get-matrix-from-transformation (transformation)
122  "Given a transformation in list form (FN ARG1 ARG2 ...), turn it into a matrix
123  which can be multipled to give the overall transformation for an object."
124  (macrolet ((idx (var idx default)
125  (let ((name (gensym)))
126  `(let ((,name (nth ,idx ,var)))
127  (if ,name ,name ,default)))))
128  (let ((transformation (if (listp (car transformation))
129  (car transformation)
130  transformation)))
131  (case (intern (write-to-string (car transformation)) :dat/svg)
132  (matrix (vector (nth 1 transformation) (nth 3 transformation) (nth 5 transformation)
133  (nth 2 transformation) (nth 4 transformation) (nth 6 transformation)
134  0 0 1))
135  (translate (m-translate (nth 1 transformation) (idx transformation 2 0)))
136  (scale (m-scale (nth 1 transformation) (idx transformation 2 0)))
137  (rotate (let ((angle (nth 1 transformation))
138  (center-x (idx transformation 2 0))
139  (center-y (idx transformation 3 0)))
140  (if (and (eq 0 center-x) (eq 0 center-y))
141  ;; just rotate, no offset funny business
142  (m-rotate angle)
143  (mat* (mat* (m-translate center-x center-y) (m-rotate angle)) (m-translate (- center-x) (- center-y))))))
144  (skewx (m-skew (nth 1 transformation) :axis :x))
145  (skewy (m-skew (nth 1 transformation) :axis :y))))))
146 
147 (defun apply-transformations (points object groups &key scale)
148  "Apply all transformations for an object, starting from its top-level group
149  and working down to the object itself."
150  (let ((transformations (get-transformations object groups))
151  (matrix (id-matrix 3))
152  (trans-points nil))
153  (dolist (transform transformations)
154  (setf matrix (mat* (get-matrix-from-transformation transform) matrix)))
155  (when scale
156  (setf matrix (mat* (m-scale (car scale) (cadr scale)) matrix)))
157  (loop for p across points do
158  (push (butlast (matv* matrix (append p '(1)))) trans-points))
159  (values (reverse trans-points)
160  matrix)))
161 ;;; PATHS
162 (define-condition unsupported-path-command (error)
163  ((text :initarg :text :reader text))
164  (:documentation "Thrown when an unsupported action/feature is parsed in a path."))
165 
166 (defun points-close-equal-p (point1 point2 &key (precision 10))
167  "Determine if two points are (about) the same. Yes, this is open to
168  interpretation, which is why it takes a precision argument =]."
169  (flet ((round-point (point)
170  (mapcar (lambda (x) (/ (floor (* x precision)) precision)) point)))
171  (equal (round-point point1) (round-point point2))))
172 
173 (defun replace-char (char rep str)
174  "Replace all instances of char with rep in str (non-destructive)."
175  (let ((new-str (make-string (length str))))
176  (loop for i from 0
177  for c across str do
178  (setf (aref new-str i) (if (eq c char)
179  rep
180  c)))
181  new-str))
182 
183 (defmacro cmd-repeat (args-and-count &body body)
184  "Some commands can repeat values with the command, namely the curve commands:
185  c,1,2,4,4,5,5 c,8,8,3,4,3,1
186  can be written as
187  c,1,2,4,4,5,5,8,8,3,4,3,1
188  yay. This macro helps alleviate some of the issues caused by this wonderful
189  feature in the get-points-from-path function."
190  (let ((i (gensym))
191  (a (gensym))
192  (args (car args-and-count))
193  (count (cadr args-and-count)))
194  `(dotimes (,i (floor (/ (length ,args) ,count)))
195  ,@body
196  (setf cur-x (car cur-point)
197  cur-y (cadr cur-point))
198  (dotimes (,a ,count)
199  (setf ,args (cdr ,args))))))
200 (defun get-points-from-path (str-data &key (curve-resolution 10))
201  "Given a string describing an SVG path, do our best to retrieve points along
202  that path. Bezier curves are approximated as accurately as needed (defined by
203  :curve-resolution).
204 
205  If the path generates an arc between x1,y1 and x2,y2, we just ignore the whole
206  arc thing and set x2,y2 as the next point in the path.
207 
208  If Z/z ends the path in the middle, we silently return the current set of
209  points without continuing the path. The idea here is we are generating
210  polygons so breaks or cutouts are not acceptable."
211  (let ((commands (print (split "(?=[a-zA-Z])" str-data)))
212  (scanner-empty-p (cl-ppcre:create-scanner (concatenate 'string "[" *whitespaces* "]") :multi-line-mode t))
213  (points nil)
214  (parts nil)
215  (first-point nil)
216  (cur-point '(0 0))
217  (last-anchor nil)
218  (disconnected nil))
219  (dolist (cmd-str commands)
220  ;; this (let) splits the command from "M-113-20" to
221  ;; ("M" "-113" "-20")
222  (let* ((cmd-parts (cl-ppcre:split "( |,|(?<=[A-Za-z])|(?=\-))" cmd-str))
223  (cmd (aref (car cmd-parts) 0))
224  ;(forget (format t "cmd: ~s~%" cmd-parts))
225  (args (remove-if #'null (mapcar (lambda (a)
226  (if (cl-ppcre:scan scanner-empty-p a)
227  nil
228  (read-from-string a)))
229  (cdr cmd-parts))))
230  (cur-x (car cur-point))
231  (cur-y (cadr cur-point)))
232  ;; process the commands (http://www.w3.org/TR/SVG/paths.html)
233  (case (if (eq cmd #\z)
234  (aref (string-upcase cmd) 0)
235  cmd)
236  (#\M
237  (cmd-repeat (args 2)
238  (setf cur-point args)
239  (push cur-point points)))
240  (#\m
241  (cmd-repeat (args 2)
242  (setf cur-point (list (+ cur-x (car args))
243  (+ cur-y (cadr args))))
244  (push cur-point points)))
245  (#\L
246  (cmd-repeat (args 2)
247  (setf cur-point args)
248  (push cur-point points)))
249  (#\l
250  (cmd-repeat (args 2)
251  (setf cur-point (list (+ cur-x (car args))
252  (+ cur-y (cadr args))))
253  (push cur-point points)))
254  (#\H
255  (cmd-repeat (args 1)
256  (setf (car cur-point) (car args))
257  (push cur-point points)))
258  (#\h
259  (cmd-repeat (args 1)
260  (setf (car cur-point) (+ cur-x (car args)))
261  (push cur-point points)))
262  (#\V
263  (cmd-repeat (args 1)
264  (setf (cadr cur-point) (car args))
265  (push cur-point points)))
266  (#\v
267  (cmd-repeat (args 1)
268  (setf (cadr cur-point) (+ cur-y (car args)))
269  (push cur-point points)))
270  (#\C
271  (cmd-repeat (args 6)
272  (let ((x1 (car args))
273  (y1 (cadr args))
274  (x2 (nth 2 args))
275  (y2 (nth 3 args))
276  (x (nth 4 args))
277  (y (nth 5 args)))
278  (setf points (append (bezier-cubic cur-x cur-y x y x1 y1 x2 y2 :resolution curve-resolution) points)
279  last-anchor (list x2 y2)
280  cur-point (list x y)))))
281  (#\c
282  (cmd-repeat (args 6)
283  (let ((x1 (+ (car args) cur-x))
284  (y1 (+ (cadr args) cur-y))
285  (x2 (+ (nth 2 args) cur-x))
286  (y2 (+ (nth 3 args) cur-y))
287  (x (+ (nth 4 args) cur-x))
288  (y (+ (nth 5 args) cur-y)))
289  (setf points (append (bezier-cubic cur-x cur-y x y x1 y1 x2 y2 :resolution curve-resolution) points)
290  last-anchor (list x2 y2)
291  cur-point (list x y)))))
292  (#\S
293  (cmd-repeat (args 4)
294  (let ((x1 (+ cur-x (- cur-x (car last-anchor))))
295  (y1 (+ cur-y (- cur-y (cadr last-anchor))))
296  (x2 (car args))
297  (y2 (cadr args))
298  (x (nth 2 args))
299  (y (nth 3 args)))
300  (setf points (append (bezier-cubic cur-x cur-y x y x1 y1 x2 y2 :resolution curve-resolution) points)
301  last-anchor (list x2 y2)
302  cur-point (list x y)))))
303  (#\s
304  (cmd-repeat (args 4)
305  (let ((x1 (+ cur-x (- cur-x (car last-anchor))))
306  (y1 (+ cur-y (- cur-y (cadr last-anchor))))
307  (x2 (+ (car args) cur-x))
308  (y2 (+ (cadr args) cur-y))
309  (x (+ (nth 2 args) cur-x))
310  (y (+ (nth 3 args) cur-y)))
311  (setf points (append (bezier-cubic cur-x cur-y x y x1 y1 x2 y2 :resolution curve-resolution) points)
312  last-anchor (list x2 y2)
313  cur-point (list x y)))))
314  (#\Q
315  (cmd-repeat (args 4)
316  (let ((x1 (car args))
317  (y1 (cadr args))
318  (x (nth 2 args))
319  (y (nth 3 args)))
320  (setf points (append (bezier-quadratic cur-x cur-y x y x1 y1 :resolution curve-resolution) points)
321  last-anchor (list x1 y1)
322  cur-point (list x y)))))
323  (#\q
324  (cmd-repeat (args 4)
325  (let ((x1 (+ (car args) cur-x))
326  (y1 (+ (cadr args) cur-y))
327  (x (+ (nth 2 args) cur-x))
328  (y (+ (nth 3 args) cur-y)))
329  (setf points (append (bezier-quadratic cur-x cur-y x y x1 y1 :resolution curve-resolution) points)
330  last-anchor (list x1 y1)
331  cur-point (list x y)))))
332  (#\T
333  (cmd-repeat (args 2)
334  (let ((x1 (+ cur-x (- cur-x (car last-anchor))))
335  (y1 (+ cur-y (- cur-y (cadr last-anchor))))
336  (x (car args))
337  (y (cadr args)))
338  (setf points (append (bezier-quadratic cur-x cur-y x y x1 y1 :resolution curve-resolution) points)
339  last-anchor (list x1 y1)
340  cur-point (list x y)))))
341  (#\t
342  (cmd-repeat (args 2)
343  (let ((x1 (+ cur-x (- cur-x (car last-anchor))))
344  (y1 (+ cur-y (- cur-y (cadr last-anchor))))
345  (x (+ (car args) cur-x))
346  (y (+ (cadr args) cur-y)))
347  (setf points (append (bezier-quadratic cur-x cur-y x y x1 y1 :resolution curve-resolution) points)
348  last-anchor (list x1 y1)
349  cur-point (list x y)))))
350  (#\A
351  (cmd-repeat (args 7)
352  (let ((rx (car args))
353  (ry (cadr args))
354  (x-rot (caddr args))
355  (large-arc (cadddr args))
356  (sweep-flag (cadr (cdddr args)))
357  (x1 (car cur-point))
358  (y1 (cadr cur-point))
359  (x2 (+ (caddr (cdddr args)) (car cur-point)))
360  (y2 (+ (cadddr (cdddr args)) (cadr cur-point))))
361  (setf points (append (elliptical-arc x1 y1 x2 y2 rx ry x-rot large-arc sweep-flag :resolution curve-resolution) points)
362  cur-point (list x2 y2)))))
363  (#\a
364  (cmd-repeat (args 7)
365  (let ((rx (car args))
366  (ry (cadr args))
367  (x-rot (caddr args))
368  (large-arc (cadddr args))
369  (sweep-flag (cadr (cdddr args)))
370  (x1 (car cur-point))
371  (y1 (cadr cur-point))
372  (x2 (+ (caddr (cdddr args)) (car cur-point)))
373  (y2 (+ (cadddr (cdddr args)) (cadr cur-point))))
374  (setf points (append (elliptical-arc x1 y1 x2 y2 rx ry x-rot large-arc sweep-flag :resolution curve-resolution) points)
375  cur-point (list x2 y2)))))
376  (#\Z
377  (push (coerce (reverse (if (points-close-equal-p (car points) first-point)
378  (cdr points)
379  points)) 'vector) parts)
380  (setf points nil))))
381  (when (= (length points) 1)
382  (setf first-point (car points))))
383  (when (not (zerop (length points)))
384  ;; we have unfinished points. add them to the part list
385  (setf disconnected t)
386  (push (coerce (reverse (if (points-close-equal-p (car points) first-point)
387  (cdr points)
388  points)) 'vector) parts))
389  (values (reverse parts) disconnected)))
390 
391 (defun bezier-cubic (x1 y1 x2 y2 ax1 ay1 ax2 ay2 &key (resolution 10))
392  "Sample resolution points off of a cubic bezier curve from (x1,y1) to (x2,y2)
393  using anchor points (ax1,ay1) (ax2,ay2)."
394  (let ((points nil))
395  (flet ((cubic (t-val p0 p1 p2 p3)
396  (+ (* (expt (- 1 t-val) 3) p0)
397  (* 3 (expt (- 1 t-val) 2) t-val p1)
398  (* 3 (- 1 t-val) (expt t-val 2) p2)
399  (* (expt t-val 3) p3))))
400  (dotimes (i resolution)
401  (let ((t-val (* (1+ i) (/ 1 resolution))))
402  (push (list (cubic t-val x1 ax1 ax2 x2)
403  (cubic t-val y1 ay1 ay2 y2))
404  points))))
405  points))
406 
407 (defun bezier-quadratic (x1 y1 x2 y2 ax1 ay1 &key (resolution 10))
408  "Sample resolution points off of a quadratic bezier curve from (x1,y1) to
409  (x2,y2) using anchor points (ax1,ay1) (ax2,ay2)."
410  (let ((points nil))
411  (flet ((quadratic (t-val p0 p1 p2)
412  (+ (* (expt (- 1 t-val) 2) p0)
413  (* 2 (- 1 t-val) t-val p1)
414  (* (expt t-val 2) p2))))
415  (dotimes (i resolution)
416  (let ((t-val (* (1+ i) (/ 1 resolution))))
417  (push (list (quadratic t-val x1 ax1 x2)
418  (quadratic t-val y1 ay1 y2)) points))))
419  points))
420 
421 (defun elliptical-arc (x1 y1 x2 y2 rx ry x-rotation large-arc-flag sweep-flag &key (resolution 10))
422  "Calculate an arc in a path. Yuck."
423  (let ((rot-mat-i (m-rotate x-rotation :reverse t))
424  (rot-mat (m-rotate x-rotation)))
425  ;; calculate a bunch of crap, mainly ellipse center x,y
426  (let* ((xy-i (matv* rot-mat-i (list (/ (- x1 x2) 2)
427  (/ (- y1 y2) 2))))
428  (x-i (car xy-i))
429  (y-i (cadr xy-i))
430  (rx2 (expt rx 2))
431  (ry2 (expt ry 2))
432  (x-i2 (expt x-i 2))
433  (y-i2 (expt y-i 2))
434  (cxy-m (expt (/ (- (* rx2 ry2) (* rx2 y-i2) (* ry2 x-i2))
435  (+ (* rx2 y-i2) (* rx2 x-i2)))
436  .5))
437  (cxy-m (if (eq large-arc-flag sweep-flag)
438  (- cxy-m)
439  cxy-m))
440  (cx-i (* cxy-m (/ (* rx y-i) ry)))
441  (cy-i (* cxy-m (/ (* ry x-i) (- rx))))
442  (cxy (matv* rot-mat (list cx-i cy-i)))
443  (cx (+ (car cxy) (/ (+ x1 x2) 2)))
444  (cy (+ (cadr cxy) (/ (+ y1 y2) 2))))
445  (flet ((angle (v1 v2)
446  (let ((x1 (car v1))
447  (y1 (cadr v1))
448  (x2 (car v2))
449  (y2 (cadr v2)))
450  (let ((sign (if (< 0 (- (* x1 y2) (* y1 x2)))
451  1
452  -1)))
453  (* sign (acos (/ (dot-prod v1 v2)
454  (* (norm v1) (norm v2)))))))))
455  ;; calculate the start/delta angles
456  (let ((theta-1 (angle (list 1 0) (list (/ (- x-i cx-i) rx)
457  (/ (- y-i cy-i) ry))))
458  (theta-delta (angle (list (/ (- x-i cx-i) rx)
459  (/ (- y-i cy-i) ry))
460  (list (/ (- (- x-i) cx-i) rx)
461  (/ (- (- y-i) cy-i) ry)))))
462  (let ((theta-step (/ theta-delta resolution))
463  (points nil))
464  ;; create our points for the ellipse. if this were a true
465  ;; implementation, we'd do radii correction such that x2,y2 always
466  ;; fall ON the ellipse path, but i truly do not care enough to
467  ;; bother. if your SVG generator sucks, take it up with them, or
468  ;; better yet do the proper calculations and issue a pull request.
469  (dotimes (i resolution)
470  (let ((angle (+ theta-1 (* theta-step i))))
471  (let ((xy (matv* rot-mat (list (* rx (cos angle))
472  (* ry (sin angle))))))
473  (push (list (+ (car xy) cx)
474  (+ (cadr xy) cy)) points))))
475  ;; get the last point on there.
476  (push (list x2 y2) points)
477  (reverse points)))))))
478 
479 ;;; SVG
480 (define-condition not-an-object (simple-condition) ())
481 
482 (defun get-points-from-ellipse (x y rx ry &key (curve-resolution 20))
483  "Calculate curve-resolution points along an ellipse. Can be used for circles
484  too (when rx == ry)."
485  (let ((points (make-array curve-resolution)))
486  (dotimes (i curve-resolution)
487  (let ((rad (* i (/ (* 2 PI) curve-resolution))))
488  (setf (aref points i)
489  (list (coerce (+ x (* (cos rad) rx)) 'single-float)
490  (coerce (+ y (* (sin rad) ry)) 'single-float)))))
491  points))
492 
493 (defmacro with-plist-string-reads (plist bindings &body body)
494  "Helper macro to make convert-to-points much more readable. Basically wraps
495  around reading values from a string in a plist and binding the result to a
496  variable:
497 
498  (with-plist-string-reads my-plist ((x :x) (y :y))
499  (+ x y))
500 
501  Expands to:
502 
503  (let ((x (read-from-string (getf my-plist :x)))
504  (y (read-from-string (getf my-plist :y))))
505  (+ x y))
506 
507  Much cleaner."
508  `(let ,(loop for binding in bindings collect
509  (list (car binding) `(read-from-string (getf ,plist ,(cadr binding)))))
510  ,@body))
511 
512 (defun convert-to-points (obj &key (curve-resolution 10))
513  "Take an object loaded from and SVG file (most likely using parse-svg-nodes)
514  and turn it into a set of points describing a polygon. Curves are
515  approximated using :curve-resolution. The higher the resolution, the more
516  accurate the curve will be. This works for paths with bezier curves as well
517  as ellipses and circles."
518  (case (intern (string-upcase (getf obj :type)) :dat/svg)
519  (rect
520  (with-plist-string-reads obj ((x :x) (y :y) (w :width) (h :height))
521  (list :points (list (vector (list x y)
522  (list (+ x w) y)
523  (list (+ x w) (+ y h))
524  (list x (+ y h)))))))
525  (polygon
526  (let* ((pairs (uiop:split-string (getf obj :points)))
527  (points (loop for pair in pairs
528  if (find #\, pair) collect (progn (setf (aref pair (search "," pair)) #\space)
529  (read-from-string (format nil "(~a)" pair))))))
530  (list :points (list (coerce points 'vector)))))
531  (path
532  (multiple-value-bind (parts disconnected)
533  (get-points-from-path (getf obj :d) :curve-resolution curve-resolution)
534  (list :points parts :meta (list :disconnected disconnected))))
535  (ellipse
536  (with-plist-string-reads obj ((x :cx) (y :cy) (rx :rx) (ry :ry))
537  (list :points (list (get-points-from-ellipse x y rx ry :curve-resolution curve-resolution)))))
538  (circle
539  (with-plist-string-reads obj ((x :cx) (y :cy) (r :r))
540  (list :points (list (get-points-from-ellipse x y r r :curve-resolution curve-resolution)))))
541  (t
542  (error 'not-an-object))))
543 
544 (defun get-node-attr (node attr-name)
545  "Given a node, get the attribute stored under attr-name."
546  (cadr (dat/xml::find-attrib attr-name node)))
547 
548 (defun parse-svg-nodes (nodes &key parent-group (next-id 0) save-attributes (group-id-attribute-name "id"))
549  "Given an SVG doc read via dat/xml:parse, return two things:
550 
551  1. A list of plist objects describing ALL the objects found in the SVG file.
552  Each object stores the group it's part of along with its attributes and
553  transformations.
554  2. A list of plist objects describing ALL the groups found, each storing its
555  group id (created if not explicit) and any transformations that group has.
556 
557  The idea is that given this data, we can easily generate polygons for each
558  object and then apply transformations to it starting with its top-level group
559  and working down to the object's transformations itself."
560  (let ((objs nil)
561  (groups nil))
562  (loop for node in (xml-node-children nodes)
563  do (let ((tag (xml-node-name node)))
564  (if (equal tag "g")
565  (let* ((gid (get-node-attr node group-id-attribute-name))
566  (gid (if gid gid (get-node-attr node "id")))
567  (gid (list (if gid gid (incf next-id))))
568  (full-gid (if parent-group
569  (append parent-group gid)
570  gid)))
571  (multiple-value-bind (sub-nodes sub-groups) (parse-svg-nodes node
572  :parent-group full-gid
573  :next-id next-id
574  :save-attributes save-attributes
575  :group-id-attribute-name group-id-attribute-name)
576  (setf objs (append sub-nodes objs))
577  (push (list :group gid :transform (parse-transform (get-node-attr node "transform")) :groups sub-groups) groups)))
578  (let* ((gid parent-group)
579  (obj (list :type tag :group gid))
580  (tagsym (intern (string-upcase tag) :dat/svg))
581  (attrs (append (case tagsym
582  (rect (list "x" "y" "width" "height"))
583  (polygon (list "points"))
584  (path (list "d"))
585  (ellipse (list "cx" "cy" "rx" "ry"))
586  (circle (list "cx" "cy" "r"))
587  (t nil))
588  save-attributes)))
589  (when attrs
590  (push (append obj (loop for attr in (append attrs (list "transform" "fill" "style" "opacity"))
591  for val = (get-node-attr node attr)
592  for parsed = (if (and val (equal attr "transform")) (parse-transform val) val)
593  if parsed append (list (read-from-string (format nil ":~a" attr)) parsed)))
594  objs))))))
595  (values objs groups)))
596 
597 (defun file-contents (path)
598  "Sucks up an entire file from PATH into a freshly-allocated string,
599  returning two values: the string and the number of bytes read."
600  (with-open-file (s path)
601  (let* ((len (file-length s))
602  (data (make-string len)))
603  (values data (read-sequence data s)))))
604 
605 (defun parse-svg-string (svg-str &key (curve-resolution 10) scale save-attributes (group-id-attribute-name "id"))
606  "Parses an SVG string, creating the nodes and groups from the SVG, then
607  converts each object into a set of points using the data in that object and
608  the transformations from the groups the object belongs to (and the object's
609  own transformations).
610 
611  SVG object curve resolutions can be set via :curve-resolution (the higher the
612  value, the more accurate curves are)."
613  (multiple-value-bind (nodes groups)
614  (parse-svg-nodes (xml-parse svg-str :quash-errors nil) :save-attributes save-attributes :group-id-attribute-name group-id-attribute-name)
615  (remove-if
616  'null
617  (mapcar (lambda (node)
618  (handler-case
619  (let* ((points-and-meta (convert-to-points node :curve-resolution curve-resolution))
620  (points-and-holes (getf points-and-meta :points))
621  (points (apply-transformations (car points-and-holes) node groups :scale scale))
622  (holes nil))
623  (dolist (hole (cdr points-and-holes))
624  (push (coerce (apply-transformations hole node groups :scale scale) 'vector) holes))
625  (append node (list :point-data (coerce points 'vector) :holes holes :meta (getf points-and-meta :meta))))
626  (not-an-object ()
627  nil)))
628  nodes))))
629 
630 (defun parse-svg-file (filename &key (curve-resolution 10) scale save-attributes (group-id-attribute-name "id"))
631  "Simple wrapper around parse-svg-string.
632 
633  SVG object curve resolutions can be set via :curve-resolution (the higher the
634  value, the more accurate curves are)."
635  (parse-svg-string (file-contents filename) :curve-resolution curve-resolution :scale scale :save-attributes save-attributes :group-id-attribute-name group-id-attribute-name))