├── .gitignore ├── README.md ├── __init__.py ├── config.py ├── convert ├── lib ├── bezmisc.py ├── cspsubdiv.py ├── cubicsuperpath.py ├── ffgeom.py ├── shapes.py ├── simplepath.py └── simpletransform.py ├── svg2gcode.py └── tests ├── 2circle.svg ├── 3curves1line.svg ├── circle.svg ├── complicated_face.svg ├── ellipse.svg ├── inkscape_text.svg ├── inkscape_text_2.svg ├── inkscape_text_fails.svg ├── inkscape_text_fails_2.svg ├── laser-machine.svg ├── letters.svg ├── line.svg ├── rect.svg ├── star.svg └── test_all.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Python Files 2 | *.pyc 3 | 4 | # Gcode outputs 5 | tests/gcode_output 6 | 7 | # Log Files 8 | tests/log 9 | /venv/ 10 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python SVG to G-Code Converter 2 | A fast svg to gcode compiler forked from [vishpat/svg2gcode](https://github.com/vishpat/svg2gcode). 3 | 4 | This library takes an svg file `location/my_file.svg` and outputs the gcode conversion to a folder in the same directory `location/gcode_output/my_file.gcode`. 5 | 6 | The file `config.py` contains the configurations for the conversion (printer bed size etc). 7 | 8 | ## Installation 9 | Simply clone this repo. 10 | ``` 11 | git clone https://github.com/pjpscriv/py-svg2gcode.git 12 | ``` 13 | 14 | ## Usage 15 | ### As a Python module 16 | To import it into your existing python project: 17 | ```python 18 | import py-svg2gcode 19 | ``` 20 | or 21 | ```python 22 | import generate_gcode from py-scvg2gcode 23 | ``` 24 | ### As a Python Command 25 | ``` 26 | python svg2gcode.py 27 | ``` 28 | 29 | ### With Bash Script (Recommended) 30 | You can also use the `RUNME` script to convert files. 31 | 32 | This method is useful for debugging as it gives you extra information. 33 | ``` 34 | ./RUNME my_svg_file.svg 35 | ``` 36 | 37 | ## Details 38 | The compiler is based on the eggbot project and it basically converts all of the SVG shapes into bezier curves. The bezier curves are then recursively sub divided until desired smoothness is achieved. The sub curves are then approximated as lines which are then converted into g-code. 39 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | ''' Module entry point for any other python program importing 2 | this folder. 3 | Date: 26 Oct 2016 4 | Author: Peter Scriven 5 | ''' 6 | 7 | from svg2gcode import generate_gcode, test 8 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | ''' Configuration file for SVG to GCODE converter 2 | Date: 26 Oct 2016 3 | Author: Peter Scriven 4 | ''' 5 | 6 | 7 | '''G-code emitted at the start of processing the SVG file''' 8 | preamble = "G28\nG1 Z0.0\nM05" 9 | 10 | '''G-code emitted at the end of processing the SVG file''' 11 | postamble = "G28" 12 | 13 | '''G-code emitted before processing a SVG shape''' 14 | shape_preamble = "G4 P0.2" 15 | 16 | '''G-code emitted after processing a SVG shape''' 17 | shape_postamble = "G4 P0.2\nM05" 18 | 19 | # A4 area: 210mm x 297mm 20 | # Printer Cutting Area: ~178mm x ~344mm 21 | # Testing Area: 150mm x 150mm (for now) 22 | '''Print bed width in mm''' 23 | bed_max_x = 150 24 | 25 | '''Print bed height in mm''' 26 | bed_max_y = 150 27 | 28 | ''' Used to control the smoothness/sharpness of the curves. 29 | Smaller the value greater the sharpness. Make sure the 30 | value is greater than 0.1''' 31 | smoothness = 0.2 32 | -------------------------------------------------------------------------------- /convert: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Display all of input+output files or 4 | # just 15 lines of each 5 | verbose=false 6 | 7 | if [[ ! $# -eq 1 ]] 8 | then 9 | echo -e "\e[95mBruh\e[0m you needa have one argument. C'mon now. \nIt should probably to be an \e[93m.svg file\e[0m because then I can do some magic with it." 10 | else 11 | infile=$1 12 | 13 | # 1. Print input file name # Output Styles listed here: 14 | inlen=$(cat $infile | wc -l) 15 | echo -en "\e[4m" # 1. Underline 16 | echo -e "Input: \"$infile\" ($inlen lines)" 17 | 18 | # 2. Show input file 19 | echo -en "\e[24;96m" # 2. Cyan, no Underline 20 | if [ $verbose = false ] ; then 21 | if [[ $inlen -gt 15 ]] 22 | then 23 | cat $infile | head -n 10 24 | echo ... 25 | cat $infile | tail -n 5 26 | else 27 | cat $infile 28 | fi 29 | else 30 | cat $infile 31 | fi 32 | 33 | # 3. Convert to GCode 34 | buglen=$(python -c "import svg2gcode as s2g; s2g.generate_gcode(\"$infile\")" | wc -l) 35 | echo -e "\e[0;4m" # 4. White, Underlined 36 | echo "Debugging: ($buglen lines)" 37 | echo -en "\e[24;33m" # 5. Orange, no Underline 38 | python -c "import svg2gcode as s2g; s2g.generate_gcode(\"$infile\")" 39 | 40 | # 4. Print output file name 41 | 42 | fname=${infile%.svg} 43 | name=${fname##*/} 44 | flocation=${fname%$name} 45 | outfile=${flocation}gcode_output/$name.gcode 46 | outlen=$(cat $outfile | wc -l) 47 | echo -e "\e[0;4m" # 4. White, Underlined 48 | echo -e "Output: \"$outfile\" ($outlen lines)" 49 | 50 | 51 | # 5. Print output file 52 | echo -en "\e[24;32m" # 5. Green, no Underline 53 | if [ $verbose = false ] ; then 54 | if [[ $outlen -gt 15 ]] 55 | then 56 | cat $outfile | head -n 10 57 | echo ... 58 | cat $outfile | tail -n 5 59 | else 60 | cat $outfile 61 | fi 62 | else 63 | cat $outfile 64 | fi 65 | 66 | echo -ne "\e[0m" # Reset to normal 67 | fi 68 | -------------------------------------------------------------------------------- /lib/bezmisc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru 4 | Copyright (C) 2005 Aaron Spike, aaron@ekips.org 5 | 6 | This program 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 2 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | """ 20 | 21 | import math, cmath 22 | 23 | 24 | def rootWrapper(a, b, c, d): 25 | if a: 26 | # Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots 27 | a, b, c = (b / a, c / a, d / a) 28 | m = 2.0 * a**3 - 9.0 * a * b + 27.0 * c 29 | k = a**2 - 3.0 * b 30 | n = m**2 - 4.0 * k**3 31 | w1 = -0.5 + 0.5 * cmath.sqrt(-3.0) 32 | w2 = -0.5 - 0.5 * cmath.sqrt(-3.0) 33 | if n < 0: 34 | m1 = pow(complex((m + cmath.sqrt(n)) / 2), 1.0 / 3) 35 | n1 = pow(complex((m - cmath.sqrt(n)) / 2), 1.0 / 3) 36 | else: 37 | if m + math.sqrt(n) < 0: 38 | m1 = -pow(-(m + math.sqrt(n)) / 2, 1.0 / 3) 39 | else: 40 | m1 = pow((m + math.sqrt(n)) / 2, 1.0 / 3) 41 | if m - math.sqrt(n) < 0: 42 | n1 = -pow(-(m - math.sqrt(n)) / 2, 1.0 / 3) 43 | else: 44 | n1 = pow((m - math.sqrt(n)) / 2, 1.0 / 3) 45 | x1 = -1.0 / 3 * (a + m1 + n1) 46 | x2 = -1.0 / 3 * (a + w1 * m1 + w2 * n1) 47 | x3 = -1.0 / 3 * (a + w2 * m1 + w1 * n1) 48 | return (x1, x2, x3) 49 | elif b: 50 | det = c**2.0 - 4.0 * b * d 51 | if det: 52 | return (-c + cmath.sqrt(det)) / (2.0 * b), (-c - cmath.sqrt(det)) / ( 53 | 2.0 * b 54 | ) 55 | else: 56 | return (-c / (2.0 * b),) 57 | elif c: 58 | return (1.0 * (-d / c),) 59 | return () 60 | 61 | 62 | def bezierparameterize(points): 63 | bx0, by0 = points[0] 64 | bx1, by1 = points[1] 65 | bx2, by2 = points[2] 66 | bx3, by3 = points[3] 67 | 68 | x0 = bx0 69 | y0 = by0 70 | cx = 3 * (bx1 - x0) 71 | bx = 3 * (bx2 - bx1) - cx 72 | ax = bx3 - x0 - cx - bx 73 | cy = 3 * (by1 - y0) 74 | by = 3 * (by2 - by1) - cy 75 | ay = by3 - y0 - cy - by 76 | 77 | return ax, ay, bx, by, cx, cy, x0, y0 78 | 79 | 80 | def linebezierintersect(line, bezier): 81 | lx1, ly1 = line[0] 82 | lx2, ly2 = line[1] 83 | bx0, by0 = bezier[0] 84 | bx1, by1 = bezier[1] 85 | bx2, by2 = bezier[2] 86 | bx3, by3 = bezier[3] 87 | 88 | dd = lx1 89 | cc = lx2 - lx1 90 | bb = ly1 91 | aa = ly2 - ly1 92 | 93 | if aa: 94 | coef1 = cc / aa 95 | coef2 = 1 96 | else: 97 | coef1 = 1 98 | coef2 = aa / cc 99 | 100 | ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize( 101 | [(bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)] 102 | ) 103 | 104 | a = coef1 * ay - coef2 * ax 105 | b = coef1 * by - coef2 * bx 106 | c = coef1 * cy - coef2 * cx 107 | d = coef1 * (y0 - bb) - coef2 * (x0 - dd) 108 | 109 | roots = rootWrapper(a, b, c, d) 110 | retval = [] 111 | for i in roots: 112 | if isinstance(i, complex) and i.imag == 0: 113 | i = i.real 114 | if isinstance(i, (int, float)) and 0 <= i <= 1: 115 | retval.append( 116 | bezierpointatt([(bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)], i) 117 | ) 118 | return retval 119 | 120 | 121 | def bezierpointatt(points, t): 122 | bx0, by0 = points[0] 123 | bx1, by1 = points[1] 124 | bx2, by2 = points[2] 125 | bx3, by3 = points[3] 126 | 127 | ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize( 128 | [(bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)] 129 | ) 130 | 131 | x = ax * (t**3) + bx * (t**2) + cx * t + x0 132 | y = ay * (t**3) + by * (t**2) + cy * t + y0 133 | return x, y 134 | 135 | 136 | def bezierslopeatt(points, t): 137 | bx0, by0 = points[0] 138 | bx1, by1 = points[1] 139 | bx2, by2 = points[2] 140 | bx3, by3 = points[3] 141 | 142 | ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize( 143 | ((bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)) 144 | ) 145 | dx = 3 * ax * (t**2) + 2 * bx * t + cx 146 | dy = 3 * ay * (t**2) + 2 * by * t + cy 147 | return dx, dy 148 | 149 | 150 | def beziertatslope(points, slope): 151 | bx0, by0 = points[0] 152 | bx1, by1 = points[1] 153 | bx2, by2 = points[2] 154 | bx3, by3 = points[3] 155 | 156 | dy, dx = slope 157 | 158 | ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize( 159 | ((bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)) 160 | ) 161 | # quadratic coefficents of slope formula 162 | if dx: 163 | slope = 1.0 * (dy / dx) 164 | a = 3 * ay - 3 * ax * slope 165 | b = 2 * by - 2 * bx * slope 166 | c = cy - cx * slope 167 | elif dy: 168 | slope = 1.0 * (dx / dy) 169 | a = 3 * ax - 3 * ay * slope 170 | b = 2 * bx - 2 * by * slope 171 | c = cx - cy * slope 172 | else: 173 | return [] 174 | 175 | roots = rootWrapper(0, a, b, c) 176 | retval = [] 177 | for i in roots: 178 | if type(i) is complex and i.imag == 0: 179 | i = i.real 180 | if type(i) is not complex and 0 <= i <= 1: 181 | retval.append(i) 182 | return retval 183 | 184 | 185 | def tpoint(x, y, t): 186 | x1, y1 = x 187 | x2, y2 = y 188 | 189 | return x1 + t * (x2 - x1), y1 + t * (y2 - y1) 190 | 191 | 192 | def beziersplitatt(points, t): 193 | bx0, by0 = points[0] 194 | bx1, by1 = points[1] 195 | bx2, by2 = points[2] 196 | bx3, by3 = points[3] 197 | 198 | m1 = tpoint((bx0, by0), (bx1, by1), t) 199 | m2 = tpoint((bx1, by1), (bx2, by2), t) 200 | m3 = tpoint((bx2, by2), (bx3, by3), t) 201 | m4 = tpoint(m1, m2, t) 202 | m5 = tpoint(m2, m3, t) 203 | m = tpoint(m4, m5, t) 204 | 205 | return ((bx0, by0), m1, m4, m), (m, m5, m3, (bx3, by3)) 206 | 207 | 208 | """ 209 | Approximating the arc length of a bezier curve 210 | according to 211 | 212 | if: 213 | L1 = |P0 P1| +|P1 P2| +|P2 P3| 214 | L0 = |P0 P3| 215 | then: 216 | L = 1/2*L0 + 1/2*L1 217 | ERR = L1-L0 218 | ERR approaches 0 as the number of subdivisions (m) increases 219 | 2^-4m 220 | 221 | Reference: 222 | Jens Gravesen 223 | "Adaptive subdivision and the length of Bezier curves" 224 | mat-report no. 1992-10, Mathematical Institute, The Technical 225 | University of Denmark. 226 | """ 227 | 228 | 229 | def pointdistance(p1, p2): 230 | x1, y1 = p1 231 | x2, y2 = p2 232 | 233 | return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2)) 234 | 235 | 236 | def Gravesen_addifclose(b, len, error=0.001): 237 | box = 0 238 | for i in range(1, 4): 239 | box += pointdistance(b[i - 1], b[i]) 240 | chord = pointdistance(b[0], b[3]) 241 | if (box - chord) > error: 242 | first, second = beziersplitatt(b, 0.5) 243 | Gravesen_addifclose(first, len, error) 244 | Gravesen_addifclose(second, len, error) 245 | else: 246 | len[0] += (box / 2.0) + (chord / 2.0) 247 | 248 | 249 | def bezierlengthGravesen(b, error=0.001): 250 | len = [0] 251 | Gravesen_addifclose(b, len, error) 252 | return len[0] 253 | 254 | 255 | # balf = Bezier Arc Length Function 256 | balfax, balfbx, balfcx, balfay, balfby, balfcy = 0, 0, 0, 0, 0, 0 257 | 258 | 259 | def balf(t): 260 | retval = (balfax * (t**2) + balfbx * t + balfcx) ** 2 + ( 261 | balfay * (t**2) + balfby * t + balfcy 262 | ) ** 2 263 | return math.sqrt(retval) 264 | 265 | 266 | def Simpson(f, a, b, n_limit, tolerance): 267 | n = 2 268 | multiplier = (b - a) / 6.0 269 | endsum = f(a) + f(b) 270 | interval = (b - a) / 2.0 271 | asum = 0.0 272 | bsum = f(a + interval) 273 | est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum)) 274 | est0 = 2.0 * est1 275 | # print multiplier, endsum, interval, asum, bsum, est1, est0 276 | while n < n_limit and abs(est1 - est0) > tolerance: 277 | n *= 2 278 | multiplier /= 2.0 279 | interval /= 2.0 280 | asum += bsum 281 | bsum = 0.0 282 | est0 = est1 283 | for i in xrange(1, n, 2): 284 | bsum += f(a + (i * interval)) 285 | est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum)) 286 | # print multiplier, endsum, interval, asum, bsum, est1, est0 287 | return est1 288 | 289 | 290 | def bezierlengthSimpson(points, tolerance=0.001): 291 | bx0, by0 = points[0] 292 | bx1, by1 = points[1] 293 | bx2, by2 = points[2] 294 | bx3, by3 = points[3] 295 | 296 | global balfax, balfbx, balfcx, balfay, balfby, balfcy 297 | ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize( 298 | ((bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)) 299 | ) 300 | balfax, balfbx, balfcx, balfay, balfby, balfcy = ( 301 | 3 * ax, 302 | 2 * bx, 303 | cx, 304 | 3 * ay, 305 | 2 * by, 306 | cy, 307 | ) 308 | return Simpson(balf, 0.0, 1.0, 4096, tolerance) 309 | 310 | 311 | def beziertatlength(points, l=0.5, tolerance=0.001): 312 | bx0, by0 = points[0] 313 | bx1, by1 = points[1] 314 | bx2, by2 = points[2] 315 | bx3, by3 = points[3] 316 | 317 | global balfax, balfbx, balfcx, balfay, balfby, balfcy 318 | ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize( 319 | ((bx0, by0), (bx1, by1), (bx2, by2), (bx3, by3)) 320 | ) 321 | balfax, balfbx, balfcx, balfay, balfby, balfcy = ( 322 | 3 * ax, 323 | 2 * bx, 324 | cx, 325 | 3 * ay, 326 | 2 * by, 327 | cy, 328 | ) 329 | t = 1.0 330 | tdiv = t 331 | curlen = Simpson(balf, 0.0, t, 4096, tolerance) 332 | targetlen = l * curlen 333 | diff = curlen - targetlen 334 | while abs(diff) > tolerance: 335 | tdiv /= 2.0 336 | if diff < 0: 337 | t += tdiv 338 | else: 339 | t -= tdiv 340 | curlen = Simpson(balf, 0.0, t, 4096, tolerance) 341 | diff = curlen - targetlen 342 | return t 343 | 344 | 345 | # default bezier length method 346 | bezierlength = bezierlengthSimpson 347 | 348 | if __name__ == "__main__": 349 | # import timing 350 | # print linebezierintersect(((,),(,)),((,),(,),(,),(,))) 351 | # print linebezierintersect(((0,1),(0,-1)),((-1,0),(-.5,0),(.5,0),(1,0))) 352 | tol = 0.00000001 353 | curves = [ 354 | ((0, 0), (1, 5), (4, 5), (5, 5)), 355 | ((0, 0), (0, 0), (5, 0), (10, 0)), 356 | ((0, 0), (0, 0), (5, 1), (10, 0)), 357 | ((-10, 0), (0, 0), (10, 0), (10, 10)), 358 | ((15, 10), (0, 0), (10, 0), (-5, 10)), 359 | ] 360 | """ 361 | for curve in curves: 362 | timing.start() 363 | g = bezierlengthGravesen(curve,tol) 364 | timing.finish() 365 | gt = timing.micro() 366 | 367 | timing.start() 368 | s = bezierlengthSimpson(curve,tol) 369 | timing.finish() 370 | st = timing.micro() 371 | 372 | print g, gt 373 | print s, st 374 | """ 375 | for curve in curves: 376 | print(beziertatlength(curve, 0.5)) 377 | 378 | 379 | # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99 380 | -------------------------------------------------------------------------------- /lib/cspsubdiv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from bezmisc import * 3 | from ffgeom import * 4 | 5 | 6 | def maxdist(points): 7 | p0x, p0y = points[0] 8 | p1x, p1y = points[1] 9 | p2x, p2y = points[2] 10 | p3x, p3y = points[3] 11 | 12 | p0 = Point(p0x, p0y) 13 | p1 = Point(p1x, p1y) 14 | p2 = Point(p2x, p2y) 15 | p3 = Point(p3x, p3y) 16 | 17 | s1 = Segment(p0, p3) 18 | return max(s1.distanceToPoint(p1), s1.distanceToPoint(p2)) 19 | 20 | 21 | def cspsubdiv(csp, flat): 22 | for sp in csp: 23 | subdiv(sp, flat) 24 | 25 | 26 | def subdiv_recursive(sp, flat, i=1): 27 | p0 = sp[i - 1][1] 28 | p1 = sp[i - 1][2] 29 | p2 = sp[i][0] 30 | p3 = sp[i][1] 31 | 32 | b = (p0, p1, p2, p3) 33 | m = maxdist(b) 34 | if m <= flat: 35 | try: 36 | subdiv(sp, flat, i + 1) 37 | except IndexError: 38 | pass 39 | else: 40 | one, two = beziersplitatt(b, 0.5) 41 | sp[i - 1][2] = one[1] 42 | sp[i][0] = two[2] 43 | p = [one[2], one[3], two[1]] 44 | sp[i:1] = [p] 45 | subdiv(sp, flat, i) 46 | 47 | 48 | def subdiv(sp, flat): 49 | # This is a non-recursive version of the above function 50 | # and avoids a `RecursionError: maximum recursion depth exceeded` error 51 | stack = [(1, len(sp))] 52 | while stack: 53 | i, end = stack.pop() 54 | while i < end: 55 | p0 = sp[i - 1][1] 56 | p1 = sp[i - 1][2] 57 | p2 = sp[i][0] 58 | p3 = sp[i][1] 59 | 60 | b = (p0, p1, p2, p3) 61 | m = maxdist(b) 62 | if m > flat: 63 | one, two = beziersplitatt(b, 0.5) 64 | sp[i - 1][2] = one[1] 65 | sp[i][0] = two[2] 66 | p = [one[2], one[3], two[1]] 67 | sp.insert(i, p) 68 | end += 1 69 | i += 1 70 | stack.extend([(i, end) for i in range(i, end)]) 71 | -------------------------------------------------------------------------------- /lib/cubicsuperpath.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | cubicsuperpath.py 4 | 5 | Copyright (C) 2005 Aaron Spike, aaron@ekips.org 6 | 7 | This program is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation; either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program; if not, write to the Free Software 19 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 | 21 | """ 22 | import simplepath 23 | from math import * 24 | 25 | def matprod(mlist): 26 | prod=mlist[0] 27 | for m in mlist[1:]: 28 | a00=prod[0][0]*m[0][0]+prod[0][1]*m[1][0] 29 | a01=prod[0][0]*m[0][1]+prod[0][1]*m[1][1] 30 | a10=prod[1][0]*m[0][0]+prod[1][1]*m[1][0] 31 | a11=prod[1][0]*m[0][1]+prod[1][1]*m[1][1] 32 | prod=[[a00,a01],[a10,a11]] 33 | return prod 34 | def rotmat(teta): 35 | return [[cos(teta),-sin(teta)],[sin(teta),cos(teta)]] 36 | def applymat(mat, pt): 37 | x=mat[0][0]*pt[0]+mat[0][1]*pt[1] 38 | y=mat[1][0]*pt[0]+mat[1][1]*pt[1] 39 | pt[0]=x 40 | pt[1]=y 41 | def norm(pt): 42 | return sqrt(pt[0]*pt[0]+pt[1]*pt[1]) 43 | 44 | def ArcToPath(p1,params): 45 | A=p1[:] 46 | rx,ry,teta,longflag,sweepflag,x2,y2=params[:] 47 | teta = teta*pi/180.0 48 | B=[x2,y2] 49 | if rx==0 or ry==0: 50 | return([[A,A,A],[B,B,B]]) 51 | mat=matprod((rotmat(teta),[[1/rx,0],[0,1/ry]],rotmat(-teta))) 52 | applymat(mat, A) 53 | applymat(mat, B) 54 | k=[-(B[1]-A[1]),B[0]-A[0]] 55 | d=k[0]*k[0]+k[1]*k[1] 56 | k[0]/=sqrt(d) 57 | k[1]/=sqrt(d) 58 | d=sqrt(max(0,1-d/4)) 59 | if longflag==sweepflag: 60 | d*=-1 61 | O=[(B[0]+A[0])/2+d*k[0],(B[1]+A[1])/2+d*k[1]] 62 | OA=[A[0]-O[0],A[1]-O[1]] 63 | OB=[B[0]-O[0],B[1]-O[1]] 64 | start=acos(OA[0]/norm(OA)) 65 | if OA[1]<0: 66 | start*=-1 67 | end=acos(OB[0]/norm(OB)) 68 | if OB[1]<0: 69 | end*=-1 70 | 71 | if sweepflag and start>end: 72 | end +=2*pi 73 | if (not sweepflag) and start" 30 | 31 | def __str__(self): 32 | return str(self.xml_node) 33 | 34 | # PATH tag 35 | class path(svgshape): 36 | 37 | def __init__(self, xml_node): 38 | super(path, self).__init__(xml_node) 39 | 40 | if not self.xml_node == None: 41 | path_el = self.xml_node 42 | self.d = path_el.get('d') 43 | else: 44 | self.d = None 45 | logging.error("path: Unable to get the attributes for %s", self.xml_node) 46 | 47 | def d_path(self): 48 | return self.d 49 | 50 | # RECT tag 51 | class rect(svgshape): 52 | 53 | def __init__(self, xml_node): 54 | super(rect, self).__init__(xml_node) 55 | 56 | if not self.xml_node == None: 57 | rect_el = self.xml_node 58 | self.x = float(rect_el.get('x')) if rect_el.get('x') else 0 59 | self.y = float(rect_el.get('y')) if rect_el.get('y') else 0 60 | self.rx = float(rect_el.get('rx')) if rect_el.get('rx') else 0 61 | self.ry = float(rect_el.get('ry')) if rect_el.get('ry') else 0 62 | self.width = float(rect_el.get('width')) if rect_el.get('width') else 0 63 | self.height = float(rect_el.get('height')) if rect_el.get('height') else 0 64 | else: 65 | self.x = self.y = self.rx = self.ry = self.width = self.height = 0 66 | logging.error("rect: Unable to get the attributes for %s", self.xml_node) 67 | 68 | def d_path(self): 69 | a = list() 70 | a.append( ['M ', [self.x, self.y]] ) 71 | a.append( [' l ', [self.width, 0]] ) 72 | a.append( [' l ', [0, self.height]] ) 73 | a.append( [' l ', [-self.width, 0]] ) 74 | a.append( [' Z', []] ) 75 | return simplepath.formatPath(a) 76 | 77 | # ELLIPSE tag 78 | class ellipse(svgshape): 79 | 80 | def __init__(self, xml_node): 81 | super(ellipse, self).__init__(xml_node) 82 | 83 | if not self.xml_node == None: 84 | ellipse_el = self.xml_node 85 | self.cx = float(ellipse_el.get('cx')) if ellipse_el.get('cx') else 0 86 | self.cy = float(ellipse_el.get('cy')) if ellipse_el.get('cy') else 0 87 | self.rx = float(ellipse_el.get('rx')) if ellipse_el.get('rx') else 0 88 | self.ry = float(ellipse_el.get('ry')) if ellipse_el.get('ry') else 0 89 | else: 90 | self.cx = self.cy = self.rx = self.ry = 0 91 | logging.error("ellipse: Unable to get the attributes for %s", self.xml_node) 92 | 93 | def d_path(self): 94 | x1 = self.cx - self.rx 95 | x2 = self.cx + self.rx 96 | p = 'M %f,%f ' % ( x1, self.cy ) + \ 97 | 'A %f,%f ' % ( self.rx, self.ry ) + \ 98 | '0 1 0 %f,%f ' % ( x2, self.cy ) + \ 99 | 'A %f,%f ' % ( self.rx, self.ry ) + \ 100 | '0 1 0 %f,%f' % ( x1, self.cy ) 101 | return p 102 | 103 | # CIRCLE tag 104 | class circle(ellipse): 105 | 106 | def __init__(self, xml_node): 107 | super(ellipse, self).__init__(xml_node) 108 | 109 | if not self.xml_node == None: 110 | circle_el = self.xml_node 111 | self.cx = float(circle_el.get('cx')) if circle_el.get('cx') else 0 112 | self.cy = float(circle_el.get('cy')) if circle_el.get('cy') else 0 113 | self.rx = float(circle_el.get('r')) if circle_el.get('r') else 0 114 | self.ry = self.rx 115 | else: 116 | self.cx = self.cy = self.r = 0 117 | logging.error("Circle: Unable to get the attributes for %s", self.xml_node) 118 | 119 | # LINE tag 120 | class line(svgshape): 121 | 122 | def __init__(self, xml_node): 123 | super(line, self).__init__(xml_node) 124 | 125 | if not self.xml_node == None: 126 | line_el = self.xml_node 127 | self.x1 = float(line_el.get('x1')) if line_el.get('x1') else 0 128 | self.y1 = float(line_el.get('y1')) if line_el.get('y1') else 0 129 | self.x2 = float(line_el.get('x2')) if line_el.get('x2') else 0 130 | self.y2 = float(line_el.get('y2')) if line_el.get('y2') else 0 131 | else: 132 | self.x1 = self.y1 = self.x2 = self.y2 = 0 133 | logging.error("line: Unable to get the attributes for %s", self.xml_node) 134 | 135 | def d_path(self): 136 | a = [] 137 | a.append( ['M ', [self.x1, self.y1]] ) 138 | a.append( ['L ', [self.x2, self.y2]] ) 139 | return simplepath.formatPath(a) 140 | 141 | # Poly-point Parent Class 142 | class polycommon(svgshape): 143 | 144 | def __init__(self, xml_node, polytype): 145 | super(polycommon, self).__init__(xml_node) 146 | self.points = list() 147 | 148 | if not self.xml_node == None: 149 | polycommon_el = self.xml_node 150 | points = polycommon_el.get('points') if polycommon_el.get('points') else list() 151 | for pa in points.split(): 152 | self.points.append(pa) 153 | else: 154 | logging.error("polycommon: Unable to get the attributes for %s", self.xml_node) 155 | 156 | # POLYGON tag 157 | class polygon(polycommon): 158 | 159 | def __init__(self, xml_node): 160 | super(polygon, self).__init__(xml_node, 'polygon') 161 | 162 | def d_path(self): 163 | d = "M " + self.points[0] 164 | for i in range( 1, len(self.points) ): 165 | d += " L " + self.points[i] 166 | d += " Z" 167 | return d 168 | 169 | # POLYLINE tag 170 | class polyline(polycommon): 171 | 172 | def __init__(self, xml_node): 173 | super(polyline, self).__init__(xml_node, 'polyline') 174 | 175 | def d_path(self): 176 | d = "M " + self.points[0] 177 | for i in range( 1, len(self.points) ): 178 | d += " L " + self.points[i] 179 | return d 180 | 181 | # 182 | def point_generator(path, mat, flatness): 183 | 184 | if len(simplepath.parsePath(path)) == 0: 185 | return 186 | 187 | simple_path = simplepath.parsePath(path) 188 | startX,startY = float(simple_path[0][1][0]), float(simple_path[0][1][1]) 189 | yield startX, startY 190 | 191 | p = cubicsuperpath.parsePath(path) 192 | 193 | if mat: 194 | simpletransform.applyTransformToPath(mat, p) 195 | 196 | for sp in p: 197 | cspsubdiv.subdiv( sp, flatness) 198 | for csp in sp: 199 | ctrl_pt1 = csp[0] 200 | ctrl_pt2 = csp[1] 201 | end_pt = csp[2] 202 | yield end_pt[0], end_pt[1], 203 | -------------------------------------------------------------------------------- /lib/simplepath.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | simplepath.py 4 | functions for digesting paths into a simple list structure 5 | 6 | Copyright (C) 2005 Aaron Spike, aaron@ekips.org 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program; if not, write to the Free Software 20 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 | 22 | """ 23 | import re, math 24 | 25 | 26 | def lexPath(d): 27 | """ 28 | returns and iterator that breaks path data 29 | identifies command and parameter tokens 30 | """ 31 | offset = 0 32 | length = len(d) 33 | delim = re.compile(r"[ \t\r\n,]+") 34 | command = re.compile(r"[MLHVCSQTAZmlhvcsqtaz]") 35 | parameter = re.compile( 36 | r"(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)" 37 | ) 38 | while 1: 39 | m = delim.match(d, offset) 40 | if m: 41 | offset = m.end() 42 | if offset >= length: 43 | break 44 | m = command.match(d, offset) 45 | if m: 46 | yield [d[offset : m.end()], True] 47 | offset = m.end() 48 | continue 49 | m = parameter.match(d, offset) 50 | if m: 51 | yield [d[offset : m.end()], False] 52 | offset = m.end() 53 | continue 54 | # TODO: create new exception 55 | raise Exception("Invalid path data!") 56 | 57 | 58 | """ 59 | pathdefs = {commandfamily: 60 | [ 61 | implicitnext, 62 | #params, 63 | [casts,cast,cast], 64 | [coord type,x,y,0] 65 | ]} 66 | """ 67 | pathdefs = { 68 | "M": ["L", 2, [float, float], ["x", "y"]], 69 | "L": ["L", 2, [float, float], ["x", "y"]], 70 | "H": ["H", 1, [float], ["x"]], 71 | "V": ["V", 1, [float], ["y"]], 72 | "C": [ 73 | "C", 74 | 6, 75 | [float, float, float, float, float, float], 76 | ["x", "y", "x", "y", "x", "y"], 77 | ], 78 | "S": ["S", 4, [float, float, float, float], ["x", "y", "x", "y"]], 79 | "Q": ["Q", 4, [float, float, float, float], ["x", "y", "x", "y"]], 80 | "T": ["T", 2, [float, float], ["x", "y"]], 81 | "A": [ 82 | "A", 83 | 7, 84 | [float, float, float, int, int, float, float], 85 | [0, 0, 0, 0, 0, "x", "y"], 86 | ], 87 | "Z": ["L", 0, [], []], 88 | } 89 | 90 | 91 | def parsePath(d): 92 | """ 93 | Parse SVG path and return an array of segments. 94 | Removes all shorthand notation. 95 | Converts coordinates to absolute. 96 | """ 97 | retval = [] 98 | lexer = lexPath(d) 99 | 100 | pen = (0.0, 0.0) 101 | subPathStart = pen 102 | lastControl = pen 103 | lastCommand = "" 104 | 105 | while 1: 106 | try: 107 | token, isCommand = next(lexer) 108 | except StopIteration: 109 | break 110 | params = [] 111 | needParam = True 112 | if isCommand: 113 | if not lastCommand and token.upper() != "M": 114 | raise Exception("Invalid path, must begin with moveto.") 115 | else: 116 | command = token 117 | else: 118 | # command was omited 119 | # use last command's implicit next command 120 | needParam = False 121 | if lastCommand: 122 | if lastCommand.isupper(): 123 | command = pathdefs[lastCommand][0] 124 | else: 125 | command = pathdefs[lastCommand.upper()][0].lower() 126 | else: 127 | raise Exception("Invalid path, no initial command.") 128 | numParams = pathdefs[command.upper()][1] 129 | while numParams > 0: 130 | if needParam: 131 | try: 132 | token, isCommand = next(lexer) 133 | if isCommand: 134 | raise Exception("Invalid number of parameters") 135 | except StopIteration: 136 | raise Exception("Unexpected end of path") 137 | cast = pathdefs[command.upper()][2][-numParams] 138 | param = cast(token) 139 | if command.islower(): 140 | if pathdefs[command.upper()][3][-numParams] == "x": 141 | param += pen[0] 142 | elif pathdefs[command.upper()][3][-numParams] == "y": 143 | param += pen[1] 144 | params.append(param) 145 | needParam = True 146 | numParams -= 1 147 | # segment is now absolute so 148 | outputCommand = command.upper() 149 | 150 | # Flesh out shortcut notation 151 | if outputCommand in ("H", "V"): 152 | if outputCommand == "H": 153 | params.append(pen[1]) 154 | if outputCommand == "V": 155 | params.insert(0, pen[0]) 156 | outputCommand = "L" 157 | if outputCommand in ("S", "T"): 158 | params.insert(0, pen[1] + (pen[1] - lastControl[1])) 159 | params.insert(0, pen[0] + (pen[0] - lastControl[0])) 160 | if outputCommand == "S": 161 | outputCommand = "C" 162 | if outputCommand == "T": 163 | outputCommand = "Q" 164 | 165 | # current values become "last" values 166 | if outputCommand == "M": 167 | subPathStart = tuple(params[0:2]) 168 | pen = subPathStart 169 | if outputCommand == "Z": 170 | pen = subPathStart 171 | else: 172 | pen = tuple(params[-2:]) 173 | 174 | if outputCommand in ("Q", "C"): 175 | lastControl = tuple(params[-4:-2]) 176 | else: 177 | lastControl = pen 178 | lastCommand = command 179 | 180 | retval.append([outputCommand, params]) 181 | return retval 182 | 183 | 184 | def formatPath(a): 185 | """Format SVG path data from an array""" 186 | return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a]) 187 | 188 | 189 | def translatePath(p, x, y): 190 | for cmd, params in p: 191 | defs = pathdefs[cmd] 192 | for i in range(defs[1]): 193 | if defs[3][i] == "x": 194 | params[i] += x 195 | elif defs[3][i] == "y": 196 | params[i] += y 197 | 198 | 199 | def scalePath(p, x, y): 200 | for cmd, params in p: 201 | defs = pathdefs[cmd] 202 | for i in range(defs[1]): 203 | if defs[3][i] == "x": 204 | params[i] *= x 205 | elif defs[3][i] == "y": 206 | params[i] *= y 207 | 208 | 209 | def rotatePath(p, a, cx=0, cy=0): 210 | if a == 0: 211 | return p 212 | for cmd, params in p: 213 | defs = pathdefs[cmd] 214 | for i in range(defs[1]): 215 | if defs[3][i] == "x": 216 | x = params[i] - cx 217 | y = params[i + 1] - cy 218 | r = math.sqrt((x**2) + (y**2)) 219 | if r != 0: 220 | theta = math.atan2(y, x) + a 221 | params[i] = (r * math.cos(theta)) + cx 222 | params[i + 1] = (r * math.sin(theta)) + cy 223 | -------------------------------------------------------------------------------- /lib/simpletransform.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr 4 | Copyright (C) 2010 Alvin Penner, penner@vaxxine.com 5 | 6 | This program 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 2 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | barraud@math.univ-lille1.fr 20 | 21 | This code defines several functions to make handling of transform 22 | attribute easier. 23 | """ 24 | import cubicsuperpath, bezmisc 25 | import copy, math, re 26 | 27 | 28 | def parseTransform(transf, mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]): 29 | if transf == "" or transf == None: 30 | return mat 31 | stransf = transf.strip() 32 | result = re.match( 33 | "(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?", stransf 34 | ) 35 | # -- translate -- 36 | if result.group(1) == "translate": 37 | args = result.group(2).replace(",", " ").split() 38 | dx = float(args[0]) 39 | if len(args) == 1: 40 | dy = 0.0 41 | else: 42 | dy = float(args[1]) 43 | matrix = [[1, 0, dx], [0, 1, dy]] 44 | # -- scale -- 45 | if result.group(1) == "scale": 46 | args = result.group(2).replace(",", " ").split() 47 | sx = float(args[0]) 48 | if len(args) == 1: 49 | sy = sx 50 | else: 51 | sy = float(args[1]) 52 | matrix = [[sx, 0, 0], [0, sy, 0]] 53 | # -- rotate -- 54 | if result.group(1) == "rotate": 55 | args = result.group(2).replace(",", " ").split() 56 | a = float(args[0]) * math.pi / 180 57 | if len(args) == 1: 58 | cx, cy = (0.0, 0.0) 59 | else: 60 | cx, cy = map(float, args[1:]) 61 | matrix = [[math.cos(a), -math.sin(a), cx], [math.sin(a), math.cos(a), cy]] 62 | matrix = composeTransform(matrix, [[1, 0, -cx], [0, 1, -cy]]) 63 | # -- skewX -- 64 | if result.group(1) == "skewX": 65 | a = float(result.group(2)) * math.pi / 180 66 | matrix = [[1, math.tan(a), 0], [0, 1, 0]] 67 | # -- skewY -- 68 | if result.group(1) == "skewY": 69 | a = float(result.group(2)) * math.pi / 180 70 | matrix = [[1, 0, 0], [math.tan(a), 1, 0]] 71 | # -- matrix -- 72 | if result.group(1) == "matrix": 73 | a11, a21, a12, a22, v1, v2 = result.group(2).replace(",", " ").split() 74 | matrix = [ 75 | [float(a11), float(a12), float(v1)], 76 | [float(a21), float(a22), float(v2)], 77 | ] 78 | 79 | matrix = composeTransform(mat, matrix) 80 | if result.end() < len(stransf): 81 | return parseTransform(stransf[result.end() :], matrix) 82 | else: 83 | return matrix 84 | 85 | 86 | def formatTransform(mat): 87 | return "matrix(%f,%f,%f,%f,%f,%f)" % ( 88 | mat[0][0], 89 | mat[1][0], 90 | mat[0][1], 91 | mat[1][1], 92 | mat[0][2], 93 | mat[1][2], 94 | ) 95 | 96 | 97 | def composeTransform(M1, M2): 98 | a11 = M1[0][0] * M2[0][0] + M1[0][1] * M2[1][0] 99 | a12 = M1[0][0] * M2[0][1] + M1[0][1] * M2[1][1] 100 | a21 = M1[1][0] * M2[0][0] + M1[1][1] * M2[1][0] 101 | a22 = M1[1][0] * M2[0][1] + M1[1][1] * M2[1][1] 102 | 103 | v1 = M1[0][0] * M2[0][2] + M1[0][1] * M2[1][2] + M1[0][2] 104 | v2 = M1[1][0] * M2[0][2] + M1[1][1] * M2[1][2] + M1[1][2] 105 | return [[a11, a12, v1], [a21, a22, v2]] 106 | 107 | 108 | def composeParents(node, mat): 109 | trans = node.get("transform") 110 | if trans: 111 | mat = composeTransform(parseTransform(trans), mat) 112 | if node.getparent().tag == inkex.addNS("g", "svg"): 113 | mat = composeParents(node.getparent(), mat) 114 | return mat 115 | 116 | 117 | def applyTransformToNode(mat, node): 118 | m = parseTransform(node.get("transform")) 119 | newtransf = formatTransform(composeTransform(mat, m)) 120 | node.set("transform", newtransf) 121 | 122 | 123 | def applyTransformToPoint(mat, pt): 124 | x = mat[0][0] * pt[0] + mat[0][1] * pt[1] + mat[0][2] 125 | y = mat[1][0] * pt[0] + mat[1][1] * pt[1] + mat[1][2] 126 | pt[0] = x 127 | pt[1] = y 128 | 129 | 130 | def applyTransformToPath(mat, path): 131 | for comp in path: 132 | for ctl in comp: 133 | for pt in ctl: 134 | applyTransformToPoint(mat, pt) 135 | 136 | 137 | def fuseTransform(node): 138 | if node.get("d") == None: 139 | # FIXME: how do you raise errors? 140 | raise AssertionError( 141 | 'can not fuse "transform" of elements that have no "d" attribute' 142 | ) 143 | t = node.get("transform") 144 | if t == None: 145 | return 146 | m = parseTransform(t) 147 | d = node.get("d") 148 | p = cubicsuperpath.parsePath(d) 149 | applyTransformToPath(m, p) 150 | node.set("d", cubicsuperpath.formatPath(p)) 151 | del node.attrib["transform"] 152 | 153 | 154 | #################################################################### 155 | ##-- Some functions to compute a rough bbox of a given list of objects. 156 | ##-- this should be shipped out in an separate file... 157 | 158 | 159 | def boxunion(b1, b2): 160 | if b1 is None: 161 | return b2 162 | elif b2 is None: 163 | return b1 164 | else: 165 | return ( 166 | min(b1[0], b2[0]), 167 | max(b1[1], b2[1]), 168 | min(b1[2], b2[2]), 169 | max(b1[3], b2[3]), 170 | ) 171 | 172 | 173 | def roughBBox(path): 174 | xmin, xMax, ymin, yMax = ( 175 | path[0][0][0][0], 176 | path[0][0][0][0], 177 | path[0][0][0][1], 178 | path[0][0][0][1], 179 | ) 180 | for pathcomp in path: 181 | for ctl in pathcomp: 182 | for pt in ctl: 183 | xmin = min(xmin, pt[0]) 184 | xMax = max(xMax, pt[0]) 185 | ymin = min(ymin, pt[1]) 186 | yMax = max(yMax, pt[1]) 187 | return xmin, xMax, ymin, yMax 188 | 189 | 190 | def refinedBBox(path): 191 | xmin, xMax, ymin, yMax = ( 192 | path[0][0][1][0], 193 | path[0][0][1][0], 194 | path[0][0][1][1], 195 | path[0][0][1][1], 196 | ) 197 | for pathcomp in path: 198 | for i in range(1, len(pathcomp)): 199 | cmin, cmax = cubicExtrema( 200 | pathcomp[i - 1][1][0], 201 | pathcomp[i - 1][2][0], 202 | pathcomp[i][0][0], 203 | pathcomp[i][1][0], 204 | ) 205 | xmin = min(xmin, cmin) 206 | xMax = max(xMax, cmax) 207 | cmin, cmax = cubicExtrema( 208 | pathcomp[i - 1][1][1], 209 | pathcomp[i - 1][2][1], 210 | pathcomp[i][0][1], 211 | pathcomp[i][1][1], 212 | ) 213 | ymin = min(ymin, cmin) 214 | yMax = max(yMax, cmax) 215 | return xmin, xMax, ymin, yMax 216 | 217 | 218 | def cubicExtrema(y0, y1, y2, y3): 219 | cmin = min(y0, y3) 220 | cmax = max(y0, y3) 221 | d1 = y1 - y0 222 | d2 = y2 - y1 223 | d3 = y3 - y2 224 | if d1 - 2 * d2 + d3: 225 | if d2 * d2 > d1 * d3: 226 | t = (d1 - d2 + math.sqrt(d2 * d2 - d1 * d3)) / (d1 - 2 * d2 + d3) 227 | if (t > 0) and (t < 1): 228 | y = ( 229 | y0 * (1 - t) * (1 - t) * (1 - t) 230 | + 3 * y1 * t * (1 - t) * (1 - t) 231 | + 3 * y2 * t * t * (1 - t) 232 | + y3 * t * t * t 233 | ) 234 | cmin = min(cmin, y) 235 | cmax = max(cmax, y) 236 | t = (d1 - d2 - math.sqrt(d2 * d2 - d1 * d3)) / (d1 - 2 * d2 + d3) 237 | if (t > 0) and (t < 1): 238 | y = ( 239 | y0 * (1 - t) * (1 - t) * (1 - t) 240 | + 3 * y1 * t * (1 - t) * (1 - t) 241 | + 3 * y2 * t * t * (1 - t) 242 | + y3 * t * t * t 243 | ) 244 | cmin = min(cmin, y) 245 | cmax = max(cmax, y) 246 | elif d3 - d1: 247 | t = -d1 / (d3 - d1) 248 | if (t > 0) and (t < 1): 249 | y = ( 250 | y0 * (1 - t) * (1 - t) * (1 - t) 251 | + 3 * y1 * t * (1 - t) * (1 - t) 252 | + 3 * y2 * t * t * (1 - t) 253 | + y3 * t * t * t 254 | ) 255 | cmin = min(cmin, y) 256 | cmax = max(cmax, y) 257 | return cmin, cmax 258 | 259 | 260 | def computeBBox(aList, mat=[[1, 0, 0], [0, 1, 0]]): 261 | bbox = None 262 | for node in aList: 263 | m = parseTransform(node.get("transform")) 264 | m = composeTransform(mat, m) 265 | # TODO: text not supported! 266 | d = None 267 | if node.get("d"): 268 | d = node.get("d") 269 | elif node.get("points"): 270 | d = "M" + node.get("points") 271 | elif node.tag in [ 272 | inkex.addNS("rect", "svg"), 273 | "rect", 274 | inkex.addNS("image", "svg"), 275 | "image", 276 | ]: 277 | d = ( 278 | "M" 279 | + node.get("x", "0") 280 | + "," 281 | + node.get("y", "0") 282 | + "h" 283 | + node.get("width") 284 | + "v" 285 | + node.get("height") 286 | + "h-" 287 | + node.get("width") 288 | ) 289 | elif node.tag in [inkex.addNS("line", "svg"), "line"]: 290 | d = ( 291 | "M" 292 | + node.get("x1") 293 | + "," 294 | + node.get("y1") 295 | + " " 296 | + node.get("x2") 297 | + "," 298 | + node.get("y2") 299 | ) 300 | elif node.tag in [ 301 | inkex.addNS("circle", "svg"), 302 | "circle", 303 | inkex.addNS("ellipse", "svg"), 304 | "ellipse", 305 | ]: 306 | rx = node.get("r") 307 | if rx is not None: 308 | ry = rx 309 | else: 310 | rx = node.get("rx") 311 | ry = node.get("ry") 312 | cx = float(node.get("cx", "0")) 313 | cy = float(node.get("cy", "0")) 314 | x1 = cx - float(rx) 315 | x2 = cx + float(rx) 316 | d = ( 317 | "M %f %f " % (x1, cy) 318 | + "A" 319 | + rx 320 | + "," 321 | + ry 322 | + " 0 1 0 %f,%f" % (x2, cy) 323 | + "A" 324 | + rx 325 | + "," 326 | + ry 327 | + " 0 1 0 %f,%f" % (x1, cy) 328 | ) 329 | 330 | if d is not None: 331 | p = cubicsuperpath.parsePath(d) 332 | applyTransformToPath(m, p) 333 | bbox = boxunion(refinedBBox(p), bbox) 334 | 335 | elif node.tag == inkex.addNS("use", "svg") or node.tag == "use": 336 | refid = node.get(inkex.addNS("href", "xlink")) 337 | path = '//*[@id="%s"]' % refid[1:] 338 | refnode = node.xpath(path) 339 | bbox = boxunion(computeBBox(refnode, m), bbox) 340 | 341 | bbox = boxunion(computeBBox(node, m), bbox) 342 | return bbox 343 | 344 | 345 | # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 346 | -------------------------------------------------------------------------------- /svg2gcode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # External Imports 4 | import os 5 | import sys 6 | import xml.etree.ElementTree as ET 7 | 8 | # Local Imports 9 | sys.path.insert(0, "./lib") # (Import from lib folder) 10 | import shapes as shapes_pkg 11 | from shapes import point_generator 12 | from config import * 13 | 14 | DEBUGGING = True 15 | SVG = set(["rect", "circle", "ellipse", "line", "polyline", "polygon", "path"]) 16 | 17 | 18 | def generate_gcode(filename): 19 | """The main method that converts svg files into gcode files. 20 | Still incomplete. See tests/start.svg""" 21 | 22 | # Check File Validity 23 | if not os.path.isfile(filename): 24 | raise ValueError('File "' + filename + '" not found.') 25 | 26 | if not filename.endswith(".svg"): 27 | raise ValueError('File "' + filename + '" is not an SVG file.') 28 | 29 | # Define the Output 30 | # ASSUMING LINUX / OSX FOLDER NAMING STYLE 31 | log = "" 32 | log += debug_log("Input File: " + filename) 33 | 34 | file = filename.split("/")[-1] 35 | dirlist = filename.split("/")[:-1] 36 | dir_string = "" 37 | for folder in dirlist: 38 | dir_string += folder + "/" 39 | 40 | # Make Output File 41 | outdir = dir_string + "gcode_output/" 42 | if not os.path.exists(outdir): 43 | os.makedirs(outdir) 44 | outfile = outdir + file.split(".svg")[0] + ".gcode" 45 | log += debug_log("Output File: " + outfile) 46 | 47 | # Make Debug File 48 | debugdir = dir_string + "log/" 49 | if not os.path.exists(debugdir): 50 | os.makedirs(debugdir) 51 | debug_file = debugdir + file.split(".svg")[0] + ".log" 52 | log += debug_log("Log File: " + debug_file + "\n") 53 | 54 | # Get the SVG Input File 55 | file = open(filename, "r") 56 | tree = ET.parse(file) 57 | root = tree.getroot() 58 | file.close() 59 | 60 | # Get the Height and Width from the parent svg tag 61 | width = root.get("width") 62 | height = root.get("height") 63 | if width == None or height == None: 64 | viewbox = root.get("viewBox") 65 | if viewbox: 66 | _, _, width, height = viewbox.split() 67 | 68 | if width == None or height == None: 69 | # raise ValueError("Unable to get width or height for the svg") 70 | print("Unable to get width and height for the svg") 71 | sys.exit(1) 72 | 73 | # If it's a string and pt or px is in it, remove it 74 | if type(width) == str: 75 | width = width.replace("pt", "") 76 | width = width.replace("px", "") 77 | 78 | if type(height) == str: 79 | height = height.replace("pt", "") 80 | height = height.replace("px", "") 81 | 82 | # Scale the file appropriately 83 | # (Will never distort image - always scales evenly) 84 | # ASSUMES: Y ASIX IS LONG AXIS 85 | # X AXIS IS SHORT AXIS 86 | # i.e. laser cutter is in "portrait" 87 | scale_x = bed_max_x / float(width) 88 | scale_y = bed_max_y / float(height) 89 | scale = min(scale_x, scale_y) 90 | if scale > 1: 91 | scale = 1 92 | 93 | log += debug_log("wdth: " + str(width)) 94 | log += debug_log("hght: " + str(height)) 95 | log += debug_log("scale: " + str(scale)) 96 | log += debug_log("x%: " + str(scale_x)) 97 | log += debug_log("y%: " + str(scale_y)) 98 | 99 | # CREATE OUTPUT VARIABLE 100 | gcode = "" 101 | 102 | # Write Initial G-Codes 103 | gcode += preamble + "\n" 104 | 105 | # Iterate through svg elements 106 | for elem in root.iter(): 107 | log += debug_log("--Found Elem: " + str(elem)) 108 | new_shape = True 109 | try: 110 | tag_suffix = elem.tag.split("}")[-1] 111 | except: 112 | print("Error reading tag value:", tag_suffix) 113 | continue 114 | 115 | # Checks element is valid SVG shape 116 | if tag_suffix in SVG: 117 | log += debug_log(" --Name: " + str(tag_suffix)) 118 | 119 | # Get corresponding class object from 'shapes.py' 120 | shape_class = getattr(shapes_pkg, tag_suffix) 121 | shape_obj = shape_class(elem) 122 | 123 | log += debug_log("\tClass : " + str(shape_class)) 124 | log += debug_log("\tObject: " + str(shape_obj)) 125 | log += debug_log("\tAttrs : " + str(elem.items())) 126 | log += debug_log("\tTransform: " + str(elem.get("transform"))) 127 | 128 | ############ HERE'S THE MEAT!!! ############# 129 | # Gets the Object path info in one of 2 ways: 130 | # 1. Reads the 's 'd' attribute. 131 | # 2. Reads the SVG and generates the path itself. 132 | d = shape_obj.d_path() 133 | log += debug_log("\td: " + str(d)) 134 | 135 | # The *Transformation Matrix* # 136 | # Specifies something about how curves are approximated 137 | # Non-essential - a default is used if the method below 138 | # returns None. 139 | m = shape_obj.transformation_matrix() 140 | log += debug_log("\tm: " + str(m)) 141 | 142 | if d: 143 | log += debug_log("\td is GOOD!") 144 | 145 | gcode += shape_preamble + "\n" 146 | points = point_generator(d, m, smoothness) 147 | 148 | log += debug_log("\tPoints: " + str(points)) 149 | 150 | for x, y in points: 151 | # log += debug_log("\t pt: "+str((x,y))) 152 | 153 | x = scale * x 154 | y = bed_max_y - scale * y 155 | 156 | log += debug_log("\t pt: " + str((x, y))) 157 | 158 | if x >= 0 and x <= bed_max_x and y >= 0 and y <= bed_max_y: 159 | if new_shape: 160 | gcode += "G0 X%0.1f Y%0.1f\n" % (x, y) 161 | gcode += "M03\n" 162 | new_shape = False 163 | else: 164 | gcode += "G0 X%0.1f Y%0.1f\n" % (x, y) 165 | log += debug_log("\t --Point printed") 166 | else: 167 | log += debug_log( 168 | "\t --POINT NOT PRINTED (" 169 | + str(bed_max_x) 170 | + "," 171 | + str(bed_max_y) 172 | + ")" 173 | ) 174 | gcode += shape_postamble + "\n" 175 | else: 176 | log += debug_log("\tNO PATH INSTRUCTIONS FOUND!!") 177 | else: 178 | log += debug_log(" --No Name: " + tag_suffix) 179 | 180 | gcode += postamble + "\n" 181 | 182 | # Write the Result 183 | ofile = open(outfile, "w+") 184 | ofile.write(gcode) 185 | ofile.close() 186 | 187 | # Write Debugging 188 | if DEBUGGING: 189 | dfile = open(debug_file, "w+") 190 | dfile.write(log) 191 | dfile.close() 192 | 193 | 194 | def debug_log(message): 195 | """Simple debugging function. If you don't understand 196 | something then chuck this frickin everywhere.""" 197 | if DEBUGGING: 198 | print(message) 199 | return message + "\n" 200 | 201 | 202 | def test(filename): 203 | """Simple test function to call to check that this 204 | module has been loaded properly""" 205 | circle_gcode = "G28\nG1 Z5.0\nG4 P200\nG1 X10.0 Y100.0\nG1 X10.0 Y101.8\nG1 X10.6 Y107.0\nG1 X11.8 Y112.1\nG1 X13.7 Y117.0\nG1 X16.2 Y121.5\nG1 X19.3 Y125.7\nG1 X22.9 Y129.5\nG1 X27.0 Y132.8\nG1 X31.5 Y135.5\nG1 X36.4 Y137.7\nG1 X41.4 Y139.1\nG1 X46.5 Y139.9\nG1 X51.7 Y140.0\nG1 X56.9 Y139.4\nG1 X62.0 Y138.2\nG1 X66.9 Y136.3\nG1 X71.5 Y133.7\nG1 X75.8 Y130.6\nG1 X79.6 Y127.0\nG1 X82.8 Y122.9\nG1 X85.5 Y118.5\nG1 X87.6 Y113.8\nG1 X89.1 Y108.8\nG1 X89.9 Y103.6\nG1 X90.0 Y98.2\nG1 X89.4 Y93.0\nG1 X88.2 Y87.9\nG1 X86.3 Y83.0\nG1 X83.8 Y78.5\nG1 X80.7 Y74.3\nG1 X77.1 Y70.5\nG1 X73.0 Y67.2\nG1 X68.5 Y64.5\nG1 X63.6 Y62.3\nG1 X58.6 Y60.9\nG1 X53.5 Y60.1\nG1 X48.3 Y60.0\nG1 X43.1 Y60.6\nG1 X38.0 Y61.8\nG1 X33.1 Y63.7\nG1 X28.5 Y66.3\nG1 X24.2 Y69.4\nG1 X20.4 Y73.0\nG1 X17.2 Y77.1\nG1 X14.5 Y81.5\nG1 X12.4 Y86.2\nG1 X10.9 Y91.2\nG1 X10.1 Y96.4\nG1 X10.0 Y100.0\nG4 P200\nG4 P200\nG1 X110.0 Y100.0\nG1 X110.0 Y101.8\nG1 X110.6 Y107.0\nG1 X111.8 Y112.1\nG1 X113.7 Y117.0\nG1 X116.2 Y121.5\nG1 X119.3 Y125.7\nG1 X122.9 Y129.5\nG1 X127.0 Y132.8\nG1 X131.5 Y135.5\nG1 X136.4 Y137.7\nG1 X141.4 Y139.1\nG1 X146.5 Y139.9\nG1 X151.7 Y140.0\nG1 X156.9 Y139.4\nG1 X162.0 Y138.2\nG1 X166.9 Y136.3\nG1 X171.5 Y133.7\nG1 X175.8 Y130.6\nG1 X179.6 Y127.0\nG1 X182.8 Y122.9\nG1 X185.5 Y118.5\nG1 X187.6 Y113.8\nG1 X189.1 Y108.8\nG1 X189.9 Y103.6\nG1 X190.0 Y98.2\nG1 X189.4 Y93.0\nG1 X188.2 Y87.9\nG1 X186.3 Y83.0\nG1 X183.8 Y78.5\nG1 X180.7 Y74.3\nG1 X177.1 Y70.5\nG1 X173.0 Y67.2\nG1 X168.5 Y64.5\nG1 X163.6 Y62.3\nG1 X158.6 Y60.9\nG1 X153.5 Y60.1\nG1 X148.3 Y60.0\nG1 X143.1 Y60.6\nG1 X138.0 Y61.8\nG1 X133.1 Y63.7\nG1 X128.5 Y66.3\nG1 X124.2 Y69.4\nG1 X120.4 Y73.0\nG1 X117.2 Y77.1\nG1 X114.5 Y81.5\nG1 X112.4 Y86.2\nG1 X110.9 Y91.2\nG1 X110.1 Y96.4\nG1 X110.0 Y100.0\nG4 P200\nG28\n" 206 | print(circle_gcode[:90], "...") 207 | return circle_gcode 208 | 209 | 210 | if __name__ == "__main__": 211 | """If this file is called by itself in the command line 212 | then this will execute.""" 213 | # file = raw_input("Please supply a filename: ") 214 | # Python3: 215 | # file = input("Please supply a filename: ") 216 | # Take in an argument: 217 | 218 | if len(sys.argv) > 1: 219 | file = sys.argv[1] 220 | else: 221 | file = input("Please supply a filename: ") 222 | 223 | generate_gcode(file) 224 | -------------------------------------------------------------------------------- /tests/2circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/3curves1line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sorry, your browser does not support inline SVG. 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sorry, your browser does not support inline SVG. 4 | -------------------------------------------------------------------------------- /tests/inkscape_text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/inkscape_text_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 37 | 41 | 45 | 49 | 53 | 57 | 61 | 65 | 69 | 73 | 77 | 81 | 85 | 89 | 93 | 97 | 101 | 105 | 109 | 113 | 117 | 121 | 125 | 129 | 133 | 137 | 141 | 145 | 149 | 153 | 157 | 161 | 165 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /tests/inkscape_text_fails.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | They said to do a good assjob withChance 3 86 | 87 | 88 | -------------------------------------------------------------------------------- /tests/inkscape_text_fails_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | They said to do a good assjob withChance 3 86 | 87 | 88 | -------------------------------------------------------------------------------- /tests/laser-machine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/letters.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 37 | 41 | 45 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/test_all.sh: -------------------------------------------------------------------------------- 1 | for f in $(ls tests) 2 | do 3 | read -p "Press Enter to test \""$f"\" ..." 4 | ./RUNME tests/$f 5 | done 6 | --------------------------------------------------------------------------------