Coverage report: /home/ellis/comp/core/lib/dat/xml/svg.lisp
Kind | Covered | All | % |
expression | 0 | 1611 | 0.0 |
branch | 0 | 38 | 0.0 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; dat/xml/svg.lisp --- SVG data encoders
3
;; based on https://github.com/orthecreedence/cl-svg-polygon
9
(defun id-matrix (dims)
10
"Return a square identity matrix with the specified dimensions."
11
(let ((array (make-array (* dims dims) :initial-element 0.0 :element-type 'single-float)))
13
(setf (aref array (* d (1+ dims))) 1.0))
17
"Multiply 3x3 matrices m1 by m2."
18
(let ((new (make-array 9 :initial-element 0.0 :element-type 'single-float)))
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)))))
28
"Multiple a matrix by a vector, return the resulting vector."
29
(let ((new (make-list 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))))))
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)
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))
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))
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))
75
"Calculate a vector norm."
76
(expt (loop for x in v sum (expt x 2)) .5))
79
"Normalize a 2D vector"
82
(let ((norm (norm v)))
83
(list (/ x norm) (/ y norm)))))
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))))
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))))
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)))
113
(push transform transformations))
115
(traverse-groups (cdr path) groups)))))))
116
(traverse-groups object-group groups))
117
(when object-transform
118
(push object-transform transformations))
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))
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)
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
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))))))
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))
153
(dolist (transform transformations)
154
(setf matrix (mat* (get-matrix-from-transformation transform) matrix)))
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)
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."))
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))))
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))))
178
(setf (aref new-str i) (if (eq c char)
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
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."
192
(args (car args-and-count))
193
(count (cadr args-and-count)))
194
`(dotimes (,i (floor (/ (length ,args) ,count)))
196
(setf cur-x (car cur-point)
197
cur-y (cadr cur-point))
199
(setf ,args (cdr ,args))))))
201
(defun get-points-from-path (str-data &key (curve-resolution 10))
202
"Given a string describing an SVG path, do our best to retrieve points along
203
that path. Bezier curves are approximated as accurately as needed (defined by
206
If the path generates an arc between x1,y1 and x2,y2, we just ignore the whole
207
arc thing and set x2,y2 as the next point in the path.
209
If Z/z ends the path in the middle, we silently return the current set of
210
points without continuing the path. The idea here is we are generating
211
polygons so breaks or cutouts are not acceptable."
212
(let ((commands (split "(?=[a-zA-Z])" str-data))
213
(scanner-empty-p (cl-ppcre:create-scanner (concatenate 'string "[" *whitespaces* "]") :multi-line-mode t))
220
(dolist (cmd-str commands)
221
;; this (let) splits the command from "M-113-20" to
222
;; ("M" "-113" "-20")
223
(let* ((cmd-parts (cl-ppcre:split "( |,|(?<=[A-Za-z])|(?=\-))" cmd-str))
224
(cmd (aref (car cmd-parts) 0))
225
;(forget (format t "cmd: ~s~%" cmd-parts))
226
(args (remove-if #'null (mapcar (lambda (a)
227
(if (cl-ppcre:scan scanner-empty-p a)
229
(read-from-string a)))
231
(cur-x (car cur-point))
232
(cur-y (cadr cur-point)))
233
;; process the commands (http://www.w3.org/TR/SVG/paths.html)
234
(case (if (eq cmd #\z)
235
(aref (string-upcase cmd) 0)
239
(setf cur-point args)
240
(push cur-point points)))
243
(setf cur-point (list (+ cur-x (car args))
244
(+ cur-y (cadr args))))
245
(push cur-point points)))
248
(setf cur-point args)
249
(push cur-point points)))
252
(setf cur-point (list (+ cur-x (car args))
253
(+ cur-y (cadr args))))
254
(push cur-point points)))
257
(setf (car cur-point) (car args))
258
(push cur-point points)))
261
(setf (car cur-point) (+ cur-x (car args)))
262
(push cur-point points)))
265
(setf (cadr cur-point) (car args))
266
(push cur-point points)))
269
(setf (cadr cur-point) (+ cur-y (car args)))
270
(push cur-point points)))
273
(let ((x1 (car args))
279
(setf points (append (bezier-cubic cur-x cur-y x y x1 y1 x2 y2 :resolution curve-resolution) points)
280
last-anchor (list x2 y2)
281
cur-point (list x y)))))
284
(let ((x1 (+ (car args) cur-x))
285
(y1 (+ (cadr args) cur-y))
286
(x2 (+ (nth 2 args) cur-x))
287
(y2 (+ (nth 3 args) cur-y))
288
(x (+ (nth 4 args) cur-x))
289
(y (+ (nth 5 args) cur-y)))
290
(setf points (append (bezier-cubic cur-x cur-y x y x1 y1 x2 y2 :resolution curve-resolution) points)
291
last-anchor (list x2 y2)
292
cur-point (list x y)))))
295
(let ((x1 (+ cur-x (- cur-x (car last-anchor))))
296
(y1 (+ cur-y (- cur-y (cadr last-anchor))))
301
(setf points (append (bezier-cubic cur-x cur-y x y x1 y1 x2 y2 :resolution curve-resolution) points)
302
last-anchor (list x2 y2)
303
cur-point (list x y)))))
306
(let ((x1 (+ cur-x (- cur-x (car last-anchor))))
307
(y1 (+ cur-y (- cur-y (cadr last-anchor))))
308
(x2 (+ (car args) cur-x))
309
(y2 (+ (cadr args) cur-y))
310
(x (+ (nth 2 args) cur-x))
311
(y (+ (nth 3 args) cur-y)))
312
(setf points (append (bezier-cubic cur-x cur-y x y x1 y1 x2 y2 :resolution curve-resolution) points)
313
last-anchor (list x2 y2)
314
cur-point (list x y)))))
317
(let ((x1 (car args))
321
(setf points (append (bezier-quadratic cur-x cur-y x y x1 y1 :resolution curve-resolution) points)
322
last-anchor (list x1 y1)
323
cur-point (list x y)))))
326
(let ((x1 (+ (car args) cur-x))
327
(y1 (+ (cadr args) cur-y))
328
(x (+ (nth 2 args) cur-x))
329
(y (+ (nth 3 args) cur-y)))
330
(setf points (append (bezier-quadratic cur-x cur-y x y x1 y1 :resolution curve-resolution) points)
331
last-anchor (list x1 y1)
332
cur-point (list x y)))))
335
(let ((x1 (+ cur-x (- cur-x (car last-anchor))))
336
(y1 (+ cur-y (- cur-y (cadr last-anchor))))
339
(setf points (append (bezier-quadratic cur-x cur-y x y x1 y1 :resolution curve-resolution) points)
340
last-anchor (list x1 y1)
341
cur-point (list x y)))))
344
(let ((x1 (+ cur-x (- cur-x (car last-anchor))))
345
(y1 (+ cur-y (- cur-y (cadr last-anchor))))
346
(x (+ (car args) cur-x))
347
(y (+ (cadr args) cur-y)))
348
(setf points (append (bezier-quadratic cur-x cur-y x y x1 y1 :resolution curve-resolution) points)
349
last-anchor (list x1 y1)
350
cur-point (list x y)))))
353
(let ((rx (car args))
356
(large-arc (cadddr args))
357
(sweep-flag (cadr (cdddr args)))
359
(y1 (cadr cur-point))
360
(x2 (+ (caddr (cdddr args)) (car cur-point)))
361
(y2 (+ (cadddr (cdddr args)) (cadr cur-point))))
362
(setf points (append (elliptical-arc x1 y1 x2 y2 rx ry x-rot large-arc sweep-flag :resolution curve-resolution) points)
363
cur-point (list x2 y2)))))
366
(let ((rx (car args))
369
(large-arc (cadddr args))
370
(sweep-flag (cadr (cdddr args)))
372
(y1 (cadr cur-point))
373
(x2 (+ (caddr (cdddr args)) (car cur-point)))
374
(y2 (+ (cadddr (cdddr args)) (cadr cur-point))))
375
(setf points (append (elliptical-arc x1 y1 x2 y2 rx ry x-rot large-arc sweep-flag :resolution curve-resolution) points)
376
cur-point (list x2 y2)))))
378
(push (coerce (reverse (if (points-close-equal-p (car points) first-point)
380
points)) 'vector) parts)
382
(when (= (length points) 1)
383
(setf first-point (car points))))
384
(when (not (zerop (length points)))
385
;; we have unfinished points. add them to the part list
386
(setf disconnected t)
387
(push (coerce (reverse (if (points-close-equal-p (car points) first-point)
389
points)) 'vector) parts))
390
(values (reverse parts) disconnected)))
392
(defun bezier-cubic (x1 y1 x2 y2 ax1 ay1 ax2 ay2 &key (resolution 10))
393
"Sample resolution points off of a cubic bezier curve from (x1,y1) to (x2,y2)
394
using anchor points (ax1,ay1) (ax2,ay2)."
396
(flet ((cubic (t-val p0 p1 p2 p3)
397
(+ (* (expt (- 1 t-val) 3) p0)
398
(* 3 (expt (- 1 t-val) 2) t-val p1)
399
(* 3 (- 1 t-val) (expt t-val 2) p2)
400
(* (expt t-val 3) p3))))
401
(dotimes (i resolution)
402
(let ((t-val (* (1+ i) (/ 1 resolution))))
403
(push (list (cubic t-val x1 ax1 ax2 x2)
404
(cubic t-val y1 ay1 ay2 y2))
408
(defun bezier-quadratic (x1 y1 x2 y2 ax1 ay1 &key (resolution 10))
409
"Sample resolution points off of a quadratic bezier curve from (x1,y1) to
410
(x2,y2) using anchor points (ax1,ay1) (ax2,ay2)."
412
(flet ((quadratic (t-val p0 p1 p2)
413
(+ (* (expt (- 1 t-val) 2) p0)
414
(* 2 (- 1 t-val) t-val p1)
415
(* (expt t-val 2) p2))))
416
(dotimes (i resolution)
417
(let ((t-val (* (1+ i) (/ 1 resolution))))
418
(push (list (quadratic t-val x1 ax1 x2)
419
(quadratic t-val y1 ay1 y2)) points))))
422
(defun elliptical-arc (x1 y1 x2 y2 rx ry x-rotation large-arc-flag sweep-flag &key (resolution 10))
423
"Calculate an arc in a path. Yuck."
424
(let ((rot-mat-i (m-rotate x-rotation :reverse t))
425
(rot-mat (m-rotate x-rotation)))
426
;; calculate a bunch of crap, mainly ellipse center x,y
427
(let* ((xy-i (matv* rot-mat-i (list (/ (- x1 x2) 2)
435
(cxy-m (expt (/ (- (* rx2 ry2) (* rx2 y-i2) (* ry2 x-i2))
436
(+ (* rx2 y-i2) (* rx2 x-i2)))
438
(cxy-m (if (eq large-arc-flag sweep-flag)
441
(cx-i (* cxy-m (/ (* rx y-i) ry)))
442
(cy-i (* cxy-m (/ (* ry x-i) (- rx))))
443
(cxy (matv* rot-mat (list cx-i cy-i)))
444
(cx (+ (car cxy) (/ (+ x1 x2) 2)))
445
(cy (+ (cadr cxy) (/ (+ y1 y2) 2))))
446
(flet ((angle (v1 v2)
451
(let ((sign (if (< 0 (- (* x1 y2) (* y1 x2)))
454
(* sign (acos (/ (dot-prod v1 v2)
455
(* (norm v1) (norm v2)))))))))
456
;; calculate the start/delta angles
457
(let ((theta-1 (angle (list 1 0) (list (/ (- x-i cx-i) rx)
458
(/ (- y-i cy-i) ry))))
459
(theta-delta (angle (list (/ (- x-i cx-i) rx)
461
(list (/ (- (- x-i) cx-i) rx)
462
(/ (- (- y-i) cy-i) ry)))))
463
(let ((theta-step (/ theta-delta resolution))
465
;; create our points for the ellipse. if this were a true
466
;; implementation, we'd do radii correction such that x2,y2 always
467
;; fall ON the ellipse path, but i truly do not care enough to
468
;; bother. if your SVG generator sucks, take it up with them, or
469
;; better yet do the proper calculations and issue a pull request.
470
(dotimes (i resolution)
471
(let ((angle (+ theta-1 (* theta-step i))))
472
(let ((xy (matv* rot-mat (list (* rx (cos angle))
473
(* ry (sin angle))))))
474
(push (list (+ (car xy) cx)
475
(+ (cadr xy) cy)) points))))
476
;; get the last point on there.
477
(push (list x2 y2) points)
478
(reverse points)))))))
481
(define-condition not-an-object (simple-condition) ())
483
(defun get-points-from-ellipse (x y rx ry &key (curve-resolution 20))
484
"Calculate curve-resolution points along an ellipse. Can be used for circles
485
too (when rx == ry)."
486
(let ((points (make-array curve-resolution)))
487
(dotimes (i curve-resolution)
488
(let ((rad (* i (/ (* 2 PI) curve-resolution))))
489
(setf (aref points i)
490
(list (coerce (+ x (* (cos rad) rx)) 'single-float)
491
(coerce (+ y (* (sin rad) ry)) 'single-float)))))
494
(defmacro with-plist-string-reads (plist bindings &body body)
495
"Helper macro to make convert-to-points much more readable. Basically wraps
496
around reading values from a string in a plist and binding the result to a
499
(with-plist-string-reads my-plist ((x :x) (y :y))
504
(let ((x (read-from-string (getf my-plist :x)))
505
(y (read-from-string (getf my-plist :y))))
509
`(let ,(loop for binding in bindings collect
510
(list (car binding) `(read-from-string (getf ,plist ,(cadr binding)))))
513
(defun convert-to-points (obj &key (curve-resolution 10))
514
"Take an object loaded from and SVG file (most likely using parse-svg-nodes)
515
and turn it into a set of points describing a polygon. Curves are
516
approximated using :curve-resolution. The higher the resolution, the more
517
accurate the curve will be. This works for paths with bezier curves as well
518
as ellipses and circles."
519
(case (intern (string-upcase (getf obj :type)) :dat/svg)
521
(with-plist-string-reads obj ((x :x) (y :y) (w :width) (h :height))
522
(list :points (list (vector (list x y)
524
(list (+ x w) (+ y h))
525
(list x (+ y h)))))))
527
(let* ((pairs (uiop:split-string (getf obj :points)))
528
(points (loop for pair in pairs
529
if (find #\, pair) collect (progn (setf (aref pair (search "," pair)) #\space)
530
(read-from-string (format nil "(~a)" pair))))))
531
(list :points (list (coerce points 'vector)))))
533
(multiple-value-bind (parts disconnected)
534
(get-points-from-path (getf obj :d) :curve-resolution curve-resolution)
535
(list :points parts :meta (list :disconnected disconnected))))
537
(with-plist-string-reads obj ((x :cx) (y :cy) (rx :rx) (ry :ry))
538
(list :points (list (get-points-from-ellipse x y rx ry :curve-resolution curve-resolution)))))
540
(with-plist-string-reads obj ((x :cx) (y :cy) (r :r))
541
(list :points (list (get-points-from-ellipse x y r r :curve-resolution curve-resolution)))))
543
(error 'not-an-object))))
545
(defun get-node-attr (node attr-name)
546
"Given a node, get the attribute stored under attr-name."
547
(cadr (dat/xml::find-attrib attr-name node)))
549
(defun parse-svg-nodes (nodes &key parent-group (next-id 0) save-attributes (group-id-attribute-name "id"))
550
"Given an SVG doc read via dat/xml:parse, return two things:
552
1. A list of plist objects describing ALL the objects found in the SVG file.
553
Each object stores the group it's part of along with its attributes and
555
2. A list of plist objects describing ALL the groups found, each storing its
556
group id (created if not explicit) and any transformations that group has.
558
The idea is that given this data, we can easily generate polygons for each
559
object and then apply transformations to it starting with its top-level group
560
and working down to the object's transformations itself."
563
(loop for node in (xml-node-children nodes)
564
do (let ((tag (xml-node-name node)))
566
(let* ((gid (get-node-attr node group-id-attribute-name))
567
(gid (if gid gid (get-node-attr node "id")))
568
(gid (list (if gid gid (incf next-id))))
569
(full-gid (if parent-group
570
(append parent-group gid)
572
(multiple-value-bind (sub-nodes sub-groups) (parse-svg-nodes node
573
:parent-group full-gid
575
:save-attributes save-attributes
576
:group-id-attribute-name group-id-attribute-name)
577
(setf objs (append sub-nodes objs))
578
(push (list :group gid :transform (parse-transform (get-node-attr node "transform")) :groups sub-groups) groups)))
579
(let* ((gid parent-group)
580
(obj (list :type tag :group gid))
581
(tagsym (intern (string-upcase tag) :dat/svg))
582
(attrs (append (case tagsym
583
(rect (list "x" "y" "width" "height"))
584
(polygon (list "points"))
586
(ellipse (list "cx" "cy" "rx" "ry"))
587
(circle (list "cx" "cy" "r"))
591
(push (append obj (loop for attr in (append attrs (list "transform" "fill" "style" "opacity"))
592
for val = (get-node-attr node attr)
593
for parsed = (if (and val (equal attr "transform")) (parse-transform val) val)
594
if parsed append (list (read-from-string (format nil ":~a" attr)) parsed)))
596
(values objs groups)))
598
(defun file-contents (path)
599
"Sucks up an entire file from PATH into a freshly-allocated string,
600
returning two values: the string and the number of bytes read."
601
(with-open-file (s path)
602
(let* ((len (file-length s))
603
(data (make-string len)))
604
(values data (read-sequence data s)))))
606
(defun parse-svg-string (svg-str &key (curve-resolution 10) scale save-attributes (group-id-attribute-name "id"))
607
"Parses an SVG string, creating the nodes and groups from the SVG, then
608
converts each object into a set of points using the data in that object and
609
the transformations from the groups the object belongs to (and the object's
610
own transformations).
612
SVG object curve resolutions can be set via :curve-resolution (the higher the
613
value, the more accurate curves are)."
614
(multiple-value-bind (nodes groups)
615
(parse-svg-nodes (xml-parse svg-str :quash-errors nil) :save-attributes save-attributes :group-id-attribute-name group-id-attribute-name)
618
(mapcar (lambda (node)
620
(let* ((points-and-meta (convert-to-points node :curve-resolution curve-resolution))
621
(points-and-holes (getf points-and-meta :points))
622
(points (apply-transformations (car points-and-holes) node groups :scale scale))
624
(dolist (hole (cdr points-and-holes))
625
(push (coerce (apply-transformations hole node groups :scale scale) 'vector) holes))
626
(append node (list :point-data (coerce points 'vector) :holes holes :meta (getf points-and-meta :meta))))
631
(defun parse-svg-file (filename &key (curve-resolution 10) scale save-attributes (group-id-attribute-name "id"))
632
"Simple wrapper around parse-svg-string.
634
SVG object curve resolutions can be set via :curve-resolution (the higher the
635
value, the more accurate curves are)."
636
(parse-svg-string (file-contents filename) :curve-resolution curve-resolution :scale scale :save-attributes save-attributes :group-id-attribute-name group-id-attribute-name))