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 3 ;; based on https://github.com/orthecreedence/cl-svg-polygon 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))) 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)))))) 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 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. 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)) 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) 228 (read-from-string a))) 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) 238 (setf cur-point args) 239 (push cur-point points))) 242 (setf cur-point (list (+ cur-x (car args)) 243 (+ cur-y (cadr args)))) 244 (push cur-point points))) 247 (setf cur-point args) 248 (push cur-point points))) 251 (setf cur-point (list (+ cur-x (car args)) 252 (+ cur-y (cadr args)))) 253 (push cur-point points))) 256 (setf (car cur-point) (car args)) 257 (push cur-point points))) 260 (setf (car cur-point) (+ cur-x (car args))) 261 (push cur-point points))) 264 (setf (cadr cur-point) (car args)) 265 (push cur-point points))) 268 (setf (cadr cur-point) (+ cur-y (car args))) 269 (push cur-point points))) 272 (let ((x1 (car 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))))) 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))))) 294 (let ((x1 (+ cur-x (- cur-x (car last-anchor)))) 295 (y1 (+ cur-y (- cur-y (cadr last-anchor)))) 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))))) 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))))) 316 (let ((x1 (car 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))))) 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))))) 334 (let ((x1 (+ cur-x (- cur-x (car last-anchor)))) 335 (y1 (+ cur-y (- cur-y (cadr last-anchor)))) 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))))) 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))))) 352 (let ((rx (car args)) 355 (large-arc (cadddr args)) 356 (sweep-flag (cadr (cdddr args))) 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))))) 365 (let ((rx (car args)) 368 (large-arc (cadddr args)) 369 (sweep-flag (cadr (cdddr args))) 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))))) 377 (push (coerce (reverse (if (points-close-equal-p (car points) first-point) 379 points)) 'vector) parts) 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) 388 points)) 'vector) parts)) 389 (values (reverse parts) disconnected))) 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)." 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)) 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)." 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)))) 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) 434 (cxy-m (expt (/ (- (* rx2 ry2) (* rx2 y-i2) (* ry2 x-i2)) 435 (+ (* rx2 y-i2) (* rx2 x-i2))) 437 (cxy-m (if (eq large-arc-flag sweep-flag) 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) 450 (let ((sign (if (< 0 (- (* x1 y2) (* y1 x2))) 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) 460 (list (/ (- (- x-i) cx-i) rx) 461 (/ (- (- y-i) cy-i) ry))))) 462 (let ((theta-step (/ theta-delta resolution)) 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))))))) 480 (define-condition not-an-object (simple-condition) ()) 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))))) 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 498 (with-plist-string-reads my-plist ((x :x) (y :y)) 503 (let ((x (read-from-string (getf my-plist :x))) 504 (y (read-from-string (getf my-plist :y)))) 508 `(let ,(loop for binding in bindings collect 509 (list (car binding) `(read-from-string (getf ,plist ,(cadr binding))))) 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) 520 (with-plist-string-reads obj ((x :x) (y :y) (w :width) (h :height)) 521 (list :points (list (vector (list x y) 523 (list (+ x w) (+ y h)) 524 (list x (+ y h))))))) 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))))) 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)))) 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))))) 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))))) 542 (error 'not-an-object)))) 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))) 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: 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 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. 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." 562 (loop for node in (xml-node-children nodes) 563 do (let ((tag (xml-node-name node))) 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) 571 (multiple-value-bind (sub-nodes sub-groups) (parse-svg-nodes node 572 :parent-group full-gid 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")) 585 (ellipse (list "cx" "cy" "rx" "ry")) 586 (circle (list "cx" "cy" "r")) 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))) 595 (values objs groups))) 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))))) 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). 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) 617 (mapcar (lambda (node) 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)) 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)))) 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. 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))