├── .gitignore ├── svg ├── __init__.py ├── svg │ ├── __init__.py │ ├── geometry.py │ └── svg.py ├── README.md └── LICENSE ├── README.md ├── examples ├── dt-logo.svg └── dt-logo.mod ├── LICENSE └── svg2mod.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*.sw? 3 | *.pyc 4 | -------------------------------------------------------------------------------- /svg/__init__.py: -------------------------------------------------------------------------------- 1 | from .svg import * 2 | -------------------------------------------------------------------------------- /svg/svg/__init__.py: -------------------------------------------------------------------------------- 1 | #__all__ = ['geometry', 'svg'] 2 | 3 | from .svg import * 4 | 5 | def parse(filename): 6 | f = svg.Svg(filename) 7 | return f 8 | 9 | -------------------------------------------------------------------------------- /svg/README.md: -------------------------------------------------------------------------------- 1 | SVG parser library 2 | ================== 3 | 4 | This is a SVG parser library written in Python. 5 | ([see here](https://github.com/cjlano/svg])) 6 | 7 | Capabilities: 8 | - Parse SVG XML 9 | - apply any transformation (svg transform) 10 | - Explode SVG Path into basic elements (Line, Bezier, ...) 11 | - Interpolate SVG Path as a series of segments 12 | - Able to simplify segments given a precision using Ramer-Douglas-Peucker algorithm 13 | 14 | Not (yet) supported: 15 | - SVG Path Arc ('A') 16 | - Non-linear transformation drawing (SkewX, ...) 17 | 18 | License: GPLv2+ 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svg2mod 2 | This is a small program to convert Inkscape SVG drawings to KiCad footprint module files. It uses [cjlano's python SVG parser and drawing module](https://github.com/cjlano/svg) to interpret drawings and approximate curves using straight line segments. Module files can be output in KiCad's legacy or s-expression (i.e., pretty) formats. Horizontally mirrored modules are automatically generated for use on the back of a 2-layer PCB. 3 | 4 | ## Usage 5 | ``` 6 | usage: svg2mod.py [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] 7 | [-f FACTOR] [-p PRECISION] [--front-only] [--format FORMAT] 8 | [--units UNITS] 9 | 10 | Convert Inkscape SVG drawings to KiCad footprint modules. 11 | 12 | optional arguments: 13 | -h, --help show this help message and exit 14 | -i FILENAME, --input-file FILENAME 15 | name of the SVG file 16 | -o FILENAME, --output-file FILENAME 17 | name of the module file 18 | --name NAME, --module-name NAME 19 | base name of the module 20 | --value VALUE, --module-value VALUE 21 | value of the module 22 | -f FACTOR, --factor FACTOR 23 | scale paths by this factor 24 | -p PRECISION, --precision PRECISION 25 | smoothness for approximating curves with line segments 26 | (float) 27 | --front-only omit output of back module (legacy output format) 28 | --format FORMAT output module file format (legacy|pretty) 29 | --units UNITS output units, if output format is legacy (decimil|mm) 30 | ``` 31 | 32 | ## SVG Files 33 | 34 | svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. 35 | * Drawings should be to scale (1 mm in Inscape will be 1 mm in KiCad). Use the --factor option to resize the resulting module(s) up or down from there. 36 | * Paths are supported. 37 | * A path may have an outline and a fill. (Colors will be ignored.) 38 | * A path may have holes, defined by interior segments within the path (see included examples). Sometimes this will render propery in KiCad, but sometimes not. 39 | * Paths with filled areas within holes may not work at all. 40 | * Groups may be used. However, styles applied to groups (e.g., stroke-width) are not applied to contained drawing elements. In these cases, it may be necessary to ungroup (and perhaps regroup) the elements. 41 | * Layers must be used to indicate the mapping of drawing elements to KiCad layers. 42 | * Layers must be named according to the rules below. 43 | * Drawing elements will be mapped to front layers by default. Mirrored images of these elements can be automatically generated and mapped to back layers in a separate module (see --front-only option). 44 | * Other types of elements such as rect, arc, and circle are not supported. 45 | * Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these elements into paths that will work. 46 | 47 | ### Layers 48 | Layers must be named (case-insensitive) according to the following rules: 49 | 50 | | Inkscape layer name | KiCad layer(s) | KiCad legacy | KiCad pretty | 51 | |:-------------------:|:----------------:|:------------:|:------------:| 52 | | Cu | F.Cu, B.Cu | Yes | Yes | 53 | | Adhes | F.Adhes, B.Adhes | Yes | Yes | 54 | | Paste | F.Paste, B.Paste | Yes | Yes | 55 | | SilkS | F.SilkS, B.SilkS | Yes | Yes | 56 | | Mask | F.Mask, B.Mask | Yes | Yes | 57 | | Dwgs.User | Dwgs.User | Yes | -- | 58 | | Cmts.User | Cmts.User | Yes | -- | 59 | | Eco1.User | Eco1.User | Yes | -- | 60 | | Eco2.User | Eco2.User | Yes | -- | 61 | | Edge.Cuts | Edge.Cuts | Yes | Yes | 62 | | Fab | F.Fab, B.Fab | -- | Yes | 63 | | CrtYd | F.CrtYd, B.CrtYd | -- | Yes | 64 | 65 | Note: If you have a layer "Cu", all of its sub-layers will be treated as "Cu" regardless of their names. 66 | -------------------------------------------------------------------------------- /examples/dt-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 70 | 74 | 75 | 80 | 84 | 88 | 92 | 96 | 100 | 104 | 105 | 106 | 112 | 116 | 117 | 123 | 128 | 134 | 140 | 143 | 149 | 155 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | 118 | -------------------------------------------------------------------------------- /svg/svg/geometry.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > 2 | 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License along 14 | # with this program; if not, write to the Free Software Foundation, Inc., 15 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16 | 17 | ''' 18 | This module contains all the geometric classes and functions not directly 19 | related to SVG parsing. It can be reused outside the scope of SVG. 20 | ''' 21 | 22 | import math 23 | import numbers 24 | import operator 25 | 26 | class Point: 27 | def __init__(self, x=None, y=None): 28 | '''A Point is defined either by a tuple/list of length 2 or 29 | by 2 coordinates 30 | >>> Point(1,2) 31 | (1.000,2.000) 32 | >>> Point((1,2)) 33 | (1.000,2.000) 34 | >>> Point([1,2]) 35 | (1.000,2.000) 36 | >>> Point('1', '2') 37 | (1.000,2.000) 38 | >>> Point(('1', None)) 39 | (1.000,0.000) 40 | ''' 41 | if (isinstance(x, tuple) or isinstance(x, list)) and len(x) == 2: 42 | x,y = x 43 | 44 | # Handle empty parameter(s) which should be interpreted as 0 45 | if x is None: x = 0 46 | if y is None: y = 0 47 | 48 | try: 49 | self.x = float(x) 50 | self.y = float(y) 51 | except: 52 | raise TypeError("A Point is defined by 2 numbers or a tuple") 53 | 54 | def __add__(self, other): 55 | '''Add 2 points by adding coordinates. 56 | Try to convert other to Point if necessary 57 | >>> Point(1,2) + Point(3,2) 58 | (4.000,4.000) 59 | >>> Point(1,2) + (3,2) 60 | (4.000,4.000)''' 61 | if not isinstance(other, Point): 62 | try: other = Point(other) 63 | except: return NotImplemented 64 | return Point(self.x + other.x, self.y + other.y) 65 | 66 | def __sub__(self, other): 67 | '''Substract two Points. 68 | >>> Point(1,2) - Point(3,2) 69 | (-2.000,0.000) 70 | ''' 71 | if not isinstance(other, Point): 72 | try: other = Point(other) 73 | except: return NotImplemented 74 | return Point(self.x - other.x, self.y - other.y) 75 | 76 | def __mul__(self, other): 77 | '''Multiply a Point with a constant. 78 | >>> 2 * Point(1,2) 79 | (2.000,4.000) 80 | >>> Point(1,2) * Point(1,2) #doctest:+IGNORE_EXCEPTION_DETAIL 81 | Traceback (most recent call last): 82 | ... 83 | TypeError: 84 | ''' 85 | if not isinstance(other, numbers.Real): 86 | return NotImplemented 87 | return Point(self.x * other, self.y * other) 88 | def __rmul__(self, other): 89 | return self.__mul__(other) 90 | 91 | def __eq__(self, other): 92 | '''Test equality 93 | >>> Point(1,2) == (1,2) 94 | True 95 | >>> Point(1,2) == Point(2,1) 96 | False 97 | ''' 98 | if not isinstance(other, Point): 99 | try: other = Point(other) 100 | except: return NotImplemented 101 | return (self.x == other.x) and (self.y == other.y) 102 | 103 | def __repr__(self): 104 | return '(' + format(self.x,'.3f') + ',' + format( self.y,'.3f') + ')' 105 | 106 | def __str__(self): 107 | return self.__repr__(); 108 | 109 | def coord(self): 110 | '''Return the point tuple (x,y)''' 111 | return (self.x, self.y) 112 | 113 | def length(self): 114 | '''Vector length, Pythagoras theorem''' 115 | return math.sqrt(self.x ** 2 + self.y ** 2) 116 | 117 | def rot(self, angle): 118 | '''Rotate vector [Origin,self] ''' 119 | if not isinstance(angle, Angle): 120 | try: angle = Angle(angle) 121 | except: return NotImplemented 122 | x = self.x * angle.cos - self.y * angle.sin 123 | y = self.x * angle.sin + self.y * angle.cos 124 | return Point(x,y) 125 | 126 | 127 | class Angle: 128 | '''Define a trigonometric angle [of a vector] ''' 129 | def __init__(self, arg): 130 | if isinstance(arg, numbers.Real): 131 | # We precompute sin and cos for rotations 132 | self.angle = arg 133 | self.cos = math.cos(self.angle) 134 | self.sin = math.sin(self.angle) 135 | elif isinstance(arg, Point): 136 | # Point angle is the trigonometric angle of the vector [origin, Point] 137 | pt = arg 138 | try: 139 | self.cos = pt.x/pt.length() 140 | self.sin = pt.y/pt.length() 141 | except ZeroDivisionError: 142 | self.cos = 1 143 | self.sin = 0 144 | 145 | self.angle = math.acos(self.cos) 146 | if self.sin < 0: 147 | self.angle = -self.angle 148 | else: 149 | raise TypeError("Angle is defined by a number or a Point") 150 | 151 | def __neg__(self): 152 | return Angle(Point(self.cos, -self.sin)) 153 | 154 | class Segment: 155 | '''A segment is an object defined by 2 points''' 156 | def __init__(self, start, end): 157 | self.start = start 158 | self.end = end 159 | 160 | def __str__(self): 161 | return 'Segment from ' + str(self.start) + ' to ' + str(self.end) 162 | 163 | def segments(self, precision=0): 164 | ''' Segments is simply the segment start -> end''' 165 | return [self.start, self.end] 166 | 167 | def length(self): 168 | '''Segment length, Pythagoras theorem''' 169 | s = self.end - self.start 170 | return math.sqrt(s.x ** 2 + s.y ** 2) 171 | 172 | def pdistance(self, p): 173 | '''Perpendicular distance between this Segment and a given Point p''' 174 | if not isinstance(p, Point): 175 | return NotImplemented 176 | 177 | if self.start == self.end: 178 | # Distance from a Point to another Point is length of a segment 179 | return Segment(self.start, p).length() 180 | 181 | s = self.end - self.start 182 | if s.x == 0: 183 | # Vertical Segment => pdistance is the difference of abscissa 184 | return abs(self.start.x - p.x) 185 | else: 186 | # That's 2-D perpendicular distance formulae (ref: Wikipedia) 187 | slope = s.y/s.x 188 | # intercept: Crossing with ordinate y-axis 189 | intercept = self.start.y - (slope * self.start.x) 190 | return abs(slope * p.x - p.y + intercept) / math.sqrt(slope ** 2 + 1) 191 | 192 | 193 | def bbox(self): 194 | xmin = min(self.start.x, self.end.x) 195 | xmax = max(self.start.x, self.end.x) 196 | ymin = min(self.start.y, self.end.y) 197 | ymax = max(self.start.y, self.end.y) 198 | 199 | return (Point(xmin,ymin),Point(xmax,ymax)) 200 | 201 | def transform(self, matrix): 202 | self.start = matrix * self.start 203 | self.end = matrix * self.end 204 | 205 | def scale(self, ratio): 206 | self.start *= ratio 207 | self.end *= ratio 208 | def translate(self, offset): 209 | self.start += offset 210 | self.end += offset 211 | def rotate(self, angle): 212 | self.start = self.start.rot(angle) 213 | self.end = self.end.rot(angle) 214 | 215 | class Bezier: 216 | '''Bezier curve class 217 | A Bezier curve is defined by its control points 218 | Its dimension is equal to the number of control points 219 | Note that SVG only support dimension 3 and 4 Bezier curve, respectively 220 | Quadratic and Cubic Bezier curve''' 221 | def __init__(self, pts): 222 | self.pts = list(pts) 223 | self.dimension = len(pts) 224 | 225 | def __str__(self): 226 | return 'Bezier' + str(self.dimension) + \ 227 | ' : ' + ", ".join([str(x) for x in self.pts]) 228 | 229 | def control_point(self, n): 230 | if n >= self.dimension: 231 | raise LookupError('Index is larger than Bezier curve dimension') 232 | else: 233 | return self.pts[n] 234 | 235 | def rlength(self): 236 | '''Rough Bezier length: length of control point segments''' 237 | pts = list(self.pts) 238 | l = 0.0 239 | p1 = pts.pop() 240 | while pts: 241 | p2 = pts.pop() 242 | l += Segment(p1, p2).length() 243 | p1 = p2 244 | return l 245 | 246 | def bbox(self): 247 | return self.rbbox() 248 | 249 | def rbbox(self): 250 | '''Rough bounding box: return the bounding box (P1,P2) of the Bezier 251 | _control_ points''' 252 | xmin = min([p.x for p in self.pts]) 253 | xmax = max([p.x for p in self.pts]) 254 | ymin = min([p.y for p in self.pts]) 255 | ymax = max([p.y for p in self.pts]) 256 | 257 | return (Point(xmin,ymin), Point(xmax,ymax)) 258 | 259 | def segments(self, precision=0): 260 | '''Return a polyline approximation ("segments") of the Bezier curve 261 | precision is the minimum significative length of a segment''' 262 | segments = [] 263 | # n is the number of Bezier points to draw according to precision 264 | if precision != 0: 265 | n = int(self.rlength() / precision) + 1 266 | else: 267 | n = 1000 268 | #if n < 10: n = 10 269 | if n > 1000 : n = 1000 270 | 271 | for t in range(0, n+1): 272 | segments.append(self._bezierN(float(t)/n)) 273 | return segments 274 | 275 | def _bezier1(self, p0, p1, t): 276 | '''Bezier curve, one dimension 277 | Compute the Point corresponding to a linear Bezier curve between 278 | p0 and p1 at "time" t ''' 279 | pt = p0 + t * (p1 - p0) 280 | return pt 281 | 282 | def _bezierN(self, t): 283 | '''Bezier curve, Nth dimension 284 | Compute the point of the Nth dimension Bezier curve at "time" t''' 285 | # We reduce the N Bezier control points by computing the linear Bezier 286 | # point of each control point segment, creating N-1 control points 287 | # until we reach one single point 288 | res = list(self.pts) 289 | # We store the resulting Bezier points in res[], recursively 290 | for n in range(self.dimension, 1, -1): 291 | # For each control point of nth dimension, 292 | # compute linear Bezier point a t 293 | for i in range(0,n-1): 294 | res[i] = self._bezier1(res[i], res[i+1], t) 295 | return res[0] 296 | 297 | def transform(self, matrix): 298 | self.pts = [matrix * x for x in self.pts] 299 | 300 | def scale(self, ratio): 301 | self.pts = [x * ratio for x in self.pts] 302 | def translate(self, offset): 303 | self.pts = [x + offset for x in self.pts] 304 | def rotate(self, angle): 305 | self.pts = [x.rot(angle) for x in self.pts] 306 | 307 | class MoveTo: 308 | def __init__(self, dest): 309 | self.dest = dest 310 | 311 | def bbox(self): 312 | return (self.dest, self.dest) 313 | 314 | def transform(self, matrix): 315 | self.dest = matrix * self.dest 316 | 317 | def scale(self, ratio): 318 | self.dest *= ratio 319 | def translate(self, offset): 320 | self.dest += offset 321 | def rotate(self, angle): 322 | self.dest = self.dest.rot(angle) 323 | 324 | 325 | def simplify_segment(segment, epsilon): 326 | '''Ramer-Douglas-Peucker algorithm''' 327 | if len(segment) < 3 or epsilon <= 0: 328 | return segment[:] 329 | 330 | l = Segment(segment[0], segment[-1]) # Longest segment 331 | 332 | # Find the furthest point from the segment 333 | index, maxDist = max([(i, l.pdistance(p)) for i,p in enumerate(segment)], 334 | key=operator.itemgetter(1)) 335 | 336 | if maxDist > epsilon: 337 | # Recursively call with segment splited in 2 on its furthest point 338 | r1 = simplify_segment(segment[:index+1], epsilon) 339 | r2 = simplify_segment(segment[index:], epsilon) 340 | # Remove redundant 'middle' Point 341 | return r1[:-1] + r2 342 | else: 343 | return [segment[0], segment[-1]] 344 | -------------------------------------------------------------------------------- /examples/dt-logo.mod: -------------------------------------------------------------------------------- 1 | PCBNEW-LibModule-V1 Wed 23 Sep 2015 12:56:11 PM 2 | Units mm 3 | $INDEX 4 | DT-Logo-Front 5 | DT-Logo-Back 6 | $EndINDEX 7 | # 8 | # dt-logo.svg 9 | # 10 | $MODULE DT-Logo-Front 11 | Po 0 0 0 15 00000000 00000000 ~~ 12 | Li DT-Logo-Front 13 | T0 0 -4.48650007222 1.524 1.524 0 0.3048 N I 21 "DT-Logo-Front" 14 | T1 0 4.48650007222 1.524 1.524 0 0.3048 N I 21 "G***" 15 | DP 0 0 0 0 45 0.00254 23 16 | Dl -1.11500002778 -1.43850007222 17 | Dl 1.11500002778 -1.43850007222 18 | Dl 1.21826499778 -1.43021840222 19 | Dl 1.31612738778 -1.40623191222 20 | Dl 1.40729941778 -1.36782838222 21 | Dl 1.49049330778 -1.31629559222 22 | Dl 1.56442127778 -1.25292132222 23 | Dl 1.62779554778 -1.17899335222 24 | Dl 1.67932833778 -1.09579946222 25 | Dl 1.71773186778 -1.00462743222 26 | Dl 1.74171835778 -0.906765042222 27 | Dl 1.75000002778 -0.803500072222 28 | Dl 1.75000002778 0.803499987556 29 | Dl 1.74171835778 0.906764959926 30 | Dl 1.71773186778 1.00462735636 31 | Dl 1.67932833778 1.09579939584 32 | Dl 1.62779554778 1.17899329736 33 | Dl 1.56442127778 1.25292127989 34 | Dl 1.49049330778 1.31629556242 35 | Dl 1.40729941778 1.36782836393 36 | Dl 1.31612738778 1.40623190342 37 | Dl 1.21826499778 1.43021839985 38 | Dl 1.11500002778 1.43850007222 39 | Dl -1.11500002778 1.43850007222 40 | Dl -1.21826499778 1.43021839985 41 | Dl -1.31612738778 1.40623190342 42 | Dl -1.40729941778 1.36782836393 43 | Dl -1.49049330778 1.31629556242 44 | Dl -1.56442127778 1.25292127989 45 | Dl -1.62779554778 1.17899329736 46 | Dl -1.67932833778 1.09579939584 47 | Dl -1.71773186778 1.00462735636 48 | Dl -1.74171835778 0.906764959926 49 | Dl -1.75000002778 0.803499987556 50 | Dl -1.75000002778 -0.803500072222 51 | Dl -1.74171835778 -0.906765042222 52 | Dl -1.71773186778 -1.00462743222 53 | Dl -1.67932833778 -1.09579946222 54 | Dl -1.62779554778 -1.17899335222 55 | Dl -1.56442127778 -1.25292132222 56 | Dl -1.49049330778 -1.31629559222 57 | Dl -1.40729941778 -1.36782838222 58 | Dl -1.31612738778 -1.40623191222 59 | Dl -1.21826499778 -1.43021840222 60 | Dl -1.11500002778 -1.43850007222 61 | DP 0 0 0 0 45 0.00254 15 62 | Dl -0.799826470222 -0.551812572889 63 | Dl -0.799826470222 0.551656560444 64 | Dl -0.565762111778 0.551656560444 65 | Dl -0.561958169057 0.551588736518 66 | Dl -0.557617048171 0.551401158364 67 | Dl -0.552923190034 0.551117666424 68 | Dl -0.548061035561 0.550762101138 69 | Dl -0.543215025667 0.550358302944 70 | Dl -0.538569601266 0.549930112284 71 | Dl -0.534309203273 0.549501369598 72 | Dl -0.530618272603 0.549095915324 73 | Dl -0.52768125017 0.548737589904 74 | Dl -0.525682576889 0.548450233778 75 | Dl -0.492995048016 0.540568430728 76 | Dl -0.462283746331 0.52830719253 77 | Dl -0.433893631435 0.512020431606 78 | Dl -0.40816966293 0.49206206038 79 | Dl -0.385456800417 0.468785991278 80 | Dl -0.366100003497 0.442546136722 81 | Dl -0.350444231772 0.413696409136 82 | Dl -0.338834444843 0.382590720946 83 | Dl -0.331615602311 0.349582984574 84 | Dl -0.329132663778 0.315027112444 85 | Dl -0.329132663778 -0.315022850889 86 | Dl -0.331662301481 -0.349341511769 87 | Dl -0.339000486336 -0.381745179104 88 | Dl -0.350771125962 -0.412034607335 89 | Dl -0.366598127977 -0.440010550903 90 | Dl -0.3861054 -0.46547376425 91 | Dl -0.40891684965 -0.488225001817 92 | Dl -0.434656384545 -0.508065018045 93 | Dl -0.462947912304 -0.524794567376 94 | Dl -0.493415340546 -0.538214404251 95 | Dl -0.525682576889 -0.548125283111 96 | Dl -0.528861655226 -0.548802540244 97 | Dl -0.532532627931 -0.549415840178 98 | Dl -0.536580107545 -0.549963455711 99 | Dl -0.54088870661 -0.550443659644 100 | Dl -0.545343037667 -0.550854724778 101 | Dl -0.549827713257 -0.551194923911 102 | Dl -0.554227345922 -0.551462529844 103 | Dl -0.558426548203 -0.551655815378 104 | Dl -0.562309932641 -0.551773053311 105 | Dl -0.565762111778 -0.551812516444 106 | Dl -0.799826470222 -0.551812572889 107 | DP 0 0 0 0 75 0.00254 15 108 | Dl -1.11853875578 -1.0274762681 109 | Dl -1.13990576822 -1.02618187506 110 | Dl -1.16015910005 -1.02243221638 111 | Dl -1.17903130383 -1.01642757278 112 | Dl -1.19625493213 -1.00836822494 113 | Dl -1.2115625375 -0.998454453586 114 | Dl -1.22468667251 -0.9868865394 115 | Dl -1.23535988973 -0.973864763088 116 | Dl -1.24331474171 -0.959589405351 117 | Dl -1.24828378102 -0.944260746888 118 | Dl -1.24999956022 -0.9280790684 119 | Dl -1.24999956022 0.927762714222 120 | Dl -1.24828378102 0.943953377266 121 | Dl -1.24331474171 0.959306407746 122 | Dl -1.23535988973 0.973617677884 123 | Dl -1.22468667251 0.986683059904 124 | Dl -1.2115625375 0.998298426028 125 | Dl -1.19625493213 1.00825964848 126 | Dl -1.17903130383 1.01636259948 127 | Dl -1.16015910005 1.02240315125 128 | Dl -1.13990576822 1.02617717602 129 | Dl -1.11853875578 1.027480546 130 | Dl -0.0133061004444 1.027480546 131 | Dl 0.008060891422 1.02617717602 132 | Dl 0.0283142113138 1.02240315125 133 | Dl 0.0471864102696 1.01636259948 134 | Dl 0.064410039328 1.00825964848 135 | Dl 0.0797176495278 0.998298426028 136 | Dl 0.0928417919076 0.986683059904 137 | Dl 0.103515017506 0.973617677884 138 | Dl 0.111469877362 0.959306407746 139 | Dl 0.116438922514 0.943953377266 140 | Dl 0.118154704 0.927762714222 141 | Dl 0.118154704 -0.9280790684 142 | Dl 0.116438922514 -0.944260746888 143 | Dl 0.111469877362 -0.959589405351 144 | Dl 0.103515017506 -0.973864763088 145 | Dl 0.0928417919076 -0.9868865394 146 | Dl 0.0797176495278 -0.998454453586 147 | Dl 0.064410039328 -1.00836822494 148 | Dl 0.0471864102696 -1.01642757278 149 | Dl 0.0283142113138 -1.02243221638 150 | Dl 0.008060891422 -1.02618187506 151 | Dl -0.0133061004444 -1.0274762681 152 | Dl -1.11853875578 -1.0274762681 153 | Dl -0.997979580533 -0.743713298222 154 | Dl -0.614498785111 -0.743713298222 155 | Dl -0.517025410222 -0.743713298222 156 | Dl -0.511574570222 -0.743713298222 157 | Dl -0.450075184458 -0.738785629162 158 | Dl -0.391801897737 -0.724512564011 159 | Dl -0.337519621914 -0.701659015808 160 | Dl -0.287993268846 -0.670989897596 161 | Dl -0.243987750389 -0.633270122417 162 | Dl -0.206267978398 -0.58926460331 163 | Dl -0.17559886473 -0.539738253318 164 | Dl -0.152745321241 -0.485455985483 165 | Dl -0.138472259786 -0.427182712844 166 | Dl -0.133544592222 -0.365683348444 167 | Dl -0.133544592222 0.365366977333 168 | Dl -0.138472259786 0.42687531338 169 | Dl -0.152745321241 0.485172952267 170 | Dl -0.17559886473 0.539491132513 171 | Dl -0.206267978398 0.58906109264 172 | Dl -0.243987750389 0.633114071167 173 | Dl -0.287993268846 0.670881306613 174 | Dl -0.337519621914 0.7015940375 175 | Dl -0.391801897737 0.724483502347 176 | Dl -0.450075184458 0.738780939673 177 | Dl -0.511574570222 0.743717588 178 | Dl -0.517025410222 0.743717588 179 | Dl -0.614498785111 0.743717588 180 | Dl -0.997979580533 0.743717588 181 | Dl -0.997979580533 -0.743713298222 182 | Dl -1.11853875578 -1.0274762681 183 | DP 0 0 0 0 70 0.00254 15 184 | Dl 0.391977936444 -0.747777337733 185 | Dl 0.382259953651 -0.746732845899 186 | Dl 0.373038393029 -0.74370875169 187 | Dl 0.364437270418 -0.738869127051 188 | Dl 0.356580601657 -0.732378043925 189 | Dl 0.349592402583 -0.724399574256 190 | Dl 0.343596689036 -0.715097789986 191 | Dl 0.338717476855 -0.70463676306 192 | Dl 0.335078781877 -0.693180565421 193 | Dl 0.332804619942 -0.680893269013 194 | Dl 0.332019006889 -0.667938945778 195 | Dl 0.332019006889 -0.624653084222 196 | Dl 0.332804618418 -0.611698760513 197 | Dl 0.335078776459 -0.599411462818 198 | Dl 0.338717466187 -0.587955263282 199 | Dl 0.34359667278 -0.577494234053 200 | Dl 0.349592381417 -0.568192447278 201 | Dl 0.356580577273 -0.560213975102 202 | Dl 0.364437245526 -0.553722889673 203 | Dl 0.373038371355 -0.548883263138 204 | Dl 0.382259939935 -0.545859167642 205 | Dl 0.391977936444 -0.544814675333 206 | Dl 0.693055256 -0.544814675333 207 | Dl 0.693055256 0.671999333333 208 | Dl 0.694042585976 0.684300999329 209 | Dl 0.69690207426 0.69596052674 210 | Dl 0.701479968386 0.706824163099 211 | Dl 0.70762251589 0.716738155943 212 | Dl 0.715175964306 0.725548752806 213 | Dl 0.723986561168 0.733102201221 214 | Dl 0.733900554012 0.739244748725 215 | Dl 0.744764190372 0.743822642852 216 | Dl 0.756423717782 0.746682131135 217 | Dl 0.768725383778 0.747669461111 218 | Dl 0.822912868889 0.747669461111 219 | Dl 0.835214534885 0.746682131135 220 | Dl 0.846874062295 0.743822642852 221 | Dl 0.857737698655 0.739244748725 222 | Dl 0.867651691499 0.733102201221 223 | Dl 0.876462288361 0.725548752806 224 | Dl 0.884015736777 0.716738155943 225 | Dl 0.890158284281 0.706824163099 226 | Dl 0.894736178407 0.69596052674 227 | Dl 0.897595666691 0.684300999329 228 | Dl 0.898582996667 0.671999333333 229 | Dl 0.898582996667 -0.544814675333 230 | Dl 1.19004122333 -0.544814675333 231 | Dl 1.19975921378 -0.545859167642 232 | Dl 1.20898078052 -0.548883263138 233 | Dl 1.2175819079 -0.553722889673 234 | Dl 1.22543858025 -0.560213975102 235 | Dl 1.23242678189 -0.568192447278 236 | Dl 1.23842249716 -0.577494234053 237 | Dl 1.24330171038 -0.587955263282 238 | Dl 1.2469404059 -0.599411462818 239 | Dl 1.24921456803 -0.611698760513 240 | Dl 1.25000018111 -0.624653084222 241 | Dl 1.25000018111 -0.667938945778 242 | Dl 1.24921456955 -0.680893269013 243 | Dl 1.24694041132 -0.693180565421 244 | Dl 1.24330172105 -0.70463676306 245 | Dl 1.23842251341 -0.715097789986 246 | Dl 1.23242680306 -0.724399574256 247 | Dl 1.22543860463 -0.732378043925 248 | Dl 1.21758193279 -0.738869127051 249 | Dl 1.2089808022 -0.74370875169 250 | Dl 1.19975922749 -0.746732845899 251 | Dl 1.19004122333 -0.747777337733 252 | Dl 0.391977936444 -0.747777337733 253 | Dl 0.391977936444 -0.747777337733 254 | $EndMODULE DT-Logo-Front 255 | $MODULE DT-Logo-Back 256 | Po 0 0 0 15 00000000 00000000 ~~ 257 | Li DT-Logo-Back 258 | T0 0 -4.48650007222 1.524 1.524 0 0.3048 N I 21 "DT-Logo-Back" 259 | T1 0 4.48650007222 1.524 1.524 0 0.3048 N I 21 "G***" 260 | DP 0 0 0 0 45 0.00254 22 261 | Dl 1.11500002778 -1.43850007222 262 | Dl -1.11500002778 -1.43850007222 263 | Dl -1.21826499778 -1.43021840222 264 | Dl -1.31612738778 -1.40623191222 265 | Dl -1.40729941778 -1.36782838222 266 | Dl -1.49049330778 -1.31629559222 267 | Dl -1.56442127778 -1.25292132222 268 | Dl -1.62779554778 -1.17899335222 269 | Dl -1.67932833778 -1.09579946222 270 | Dl -1.71773186778 -1.00462743222 271 | Dl -1.74171835778 -0.906765042222 272 | Dl -1.75000002778 -0.803500072222 273 | Dl -1.75000002778 0.803499987556 274 | Dl -1.74171835778 0.906764959926 275 | Dl -1.71773186778 1.00462735636 276 | Dl -1.67932833778 1.09579939584 277 | Dl -1.62779554778 1.17899329736 278 | Dl -1.56442127778 1.25292127989 279 | Dl -1.49049330778 1.31629556242 280 | Dl -1.40729941778 1.36782836393 281 | Dl -1.31612738778 1.40623190342 282 | Dl -1.21826499778 1.43021839985 283 | Dl -1.11500002778 1.43850007222 284 | Dl 1.11500002778 1.43850007222 285 | Dl 1.21826499778 1.43021839985 286 | Dl 1.31612738778 1.40623190342 287 | Dl 1.40729941778 1.36782836393 288 | Dl 1.49049330778 1.31629556242 289 | Dl 1.56442127778 1.25292127989 290 | Dl 1.62779554778 1.17899329736 291 | Dl 1.67932833778 1.09579939584 292 | Dl 1.71773186778 1.00462735636 293 | Dl 1.74171835778 0.906764959926 294 | Dl 1.75000002778 0.803499987556 295 | Dl 1.75000002778 -0.803500072222 296 | Dl 1.74171835778 -0.906765042222 297 | Dl 1.71773186778 -1.00462743222 298 | Dl 1.67932833778 -1.09579946222 299 | Dl 1.62779554778 -1.17899335222 300 | Dl 1.56442127778 -1.25292132222 301 | Dl 1.49049330778 -1.31629559222 302 | Dl 1.40729941778 -1.36782838222 303 | Dl 1.31612738778 -1.40623191222 304 | Dl 1.21826499778 -1.43021840222 305 | Dl 1.11500002778 -1.43850007222 306 | DP 0 0 0 0 45 0.00254 0 307 | Dl 0.799826470222 -0.551812572889 308 | Dl 0.799826470222 0.551656560444 309 | Dl 0.565762111778 0.551656560444 310 | Dl 0.561958169057 0.551588736518 311 | Dl 0.557617048171 0.551401158364 312 | Dl 0.552923190034 0.551117666424 313 | Dl 0.548061035561 0.550762101138 314 | Dl 0.543215025667 0.550358302944 315 | Dl 0.538569601266 0.549930112284 316 | Dl 0.534309203273 0.549501369598 317 | Dl 0.530618272603 0.549095915324 318 | Dl 0.52768125017 0.548737589904 319 | Dl 0.525682576889 0.548450233778 320 | Dl 0.492995048016 0.540568430728 321 | Dl 0.462283746331 0.52830719253 322 | Dl 0.433893631435 0.512020431606 323 | Dl 0.40816966293 0.49206206038 324 | Dl 0.385456800417 0.468785991278 325 | Dl 0.366100003497 0.442546136722 326 | Dl 0.350444231772 0.413696409136 327 | Dl 0.338834444843 0.382590720946 328 | Dl 0.331615602311 0.349582984574 329 | Dl 0.329132663778 0.315027112444 330 | Dl 0.329132663778 -0.315022850889 331 | Dl 0.331662301481 -0.349341511769 332 | Dl 0.339000486336 -0.381745179104 333 | Dl 0.350771125962 -0.412034607335 334 | Dl 0.366598127977 -0.440010550903 335 | Dl 0.3861054 -0.46547376425 336 | Dl 0.40891684965 -0.488225001817 337 | Dl 0.434656384545 -0.508065018045 338 | Dl 0.462947912304 -0.524794567376 339 | Dl 0.493415340546 -0.538214404251 340 | Dl 0.525682576889 -0.548125283111 341 | Dl 0.528861655226 -0.548802540244 342 | Dl 0.532532627931 -0.549415840178 343 | Dl 0.536580107545 -0.549963455711 344 | Dl 0.54088870661 -0.550443659644 345 | Dl 0.545343037667 -0.550854724778 346 | Dl 0.549827713257 -0.551194923911 347 | Dl 0.554227345922 -0.551462529844 348 | Dl 0.558426548203 -0.551655815378 349 | Dl 0.562309932641 -0.551773053311 350 | Dl 0.565762111778 -0.551812516444 351 | Dl 0.799826470222 -0.551812572889 352 | DP 0 0 0 0 75 0.00254 0 353 | Dl 1.11853875578 -1.0274762681 354 | Dl 1.13990576822 -1.02618187506 355 | Dl 1.16015910005 -1.02243221638 356 | Dl 1.17903130383 -1.01642757278 357 | Dl 1.19625493213 -1.00836822494 358 | Dl 1.2115625375 -0.998454453586 359 | Dl 1.22468667251 -0.9868865394 360 | Dl 1.23535988973 -0.973864763088 361 | Dl 1.24331474171 -0.959589405351 362 | Dl 1.24828378102 -0.944260746888 363 | Dl 1.24999956022 -0.9280790684 364 | Dl 1.24999956022 0.927762714222 365 | Dl 1.24828378102 0.943953377266 366 | Dl 1.24331474171 0.959306407746 367 | Dl 1.23535988973 0.973617677884 368 | Dl 1.22468667251 0.986683059904 369 | Dl 1.2115625375 0.998298426028 370 | Dl 1.19625493213 1.00825964848 371 | Dl 1.17903130383 1.01636259948 372 | Dl 1.16015910005 1.02240315125 373 | Dl 1.13990576822 1.02617717602 374 | Dl 1.11853875578 1.027480546 375 | Dl 0.0133061004444 1.027480546 376 | Dl -0.008060891422 1.02617717602 377 | Dl -0.0283142113138 1.02240315125 378 | Dl -0.0471864102696 1.01636259948 379 | Dl -0.064410039328 1.00825964848 380 | Dl -0.0797176495278 0.998298426028 381 | Dl -0.0928417919076 0.986683059904 382 | Dl -0.103515017506 0.973617677884 383 | Dl -0.111469877362 0.959306407746 384 | Dl -0.116438922514 0.943953377266 385 | Dl -0.118154704 0.927762714222 386 | Dl -0.118154704 -0.9280790684 387 | Dl -0.116438922514 -0.944260746888 388 | Dl -0.111469877362 -0.959589405351 389 | Dl -0.103515017506 -0.973864763088 390 | Dl -0.0928417919076 -0.9868865394 391 | Dl -0.0797176495278 -0.998454453586 392 | Dl -0.064410039328 -1.00836822494 393 | Dl -0.0471864102696 -1.01642757278 394 | Dl -0.0283142113138 -1.02243221638 395 | Dl -0.008060891422 -1.02618187506 396 | Dl 0.0133061004444 -1.0274762681 397 | Dl 1.11853875578 -1.0274762681 398 | Dl 0.997979580533 -0.743713298222 399 | Dl 0.614498785111 -0.743713298222 400 | Dl 0.517025410222 -0.743713298222 401 | Dl 0.511574570222 -0.743713298222 402 | Dl 0.450075184458 -0.738785629162 403 | Dl 0.391801897737 -0.724512564011 404 | Dl 0.337519621914 -0.701659015808 405 | Dl 0.287993268846 -0.670989897596 406 | Dl 0.243987750389 -0.633270122417 407 | Dl 0.206267978398 -0.58926460331 408 | Dl 0.17559886473 -0.539738253318 409 | Dl 0.152745321241 -0.485455985483 410 | Dl 0.138472259786 -0.427182712844 411 | Dl 0.133544592222 -0.365683348444 412 | Dl 0.133544592222 0.365366977333 413 | Dl 0.138472259786 0.42687531338 414 | Dl 0.152745321241 0.485172952267 415 | Dl 0.17559886473 0.539491132513 416 | Dl 0.206267978398 0.58906109264 417 | Dl 0.243987750389 0.633114071167 418 | Dl 0.287993268846 0.670881306613 419 | Dl 0.337519621914 0.7015940375 420 | Dl 0.391801897737 0.724483502347 421 | Dl 0.450075184458 0.738780939673 422 | Dl 0.511574570222 0.743717588 423 | Dl 0.517025410222 0.743717588 424 | Dl 0.614498785111 0.743717588 425 | Dl 0.997979580533 0.743717588 426 | Dl 0.997979580533 -0.743713298222 427 | Dl 1.11853875578 -1.0274762681 428 | DP 0 0 0 0 70 0.00254 0 429 | Dl -0.391977936444 -0.747777337733 430 | Dl -0.382259953651 -0.746732845899 431 | Dl -0.373038393029 -0.74370875169 432 | Dl -0.364437270418 -0.738869127051 433 | Dl -0.356580601657 -0.732378043925 434 | Dl -0.349592402583 -0.724399574256 435 | Dl -0.343596689036 -0.715097789986 436 | Dl -0.338717476855 -0.70463676306 437 | Dl -0.335078781877 -0.693180565421 438 | Dl -0.332804619942 -0.680893269013 439 | Dl -0.332019006889 -0.667938945778 440 | Dl -0.332019006889 -0.624653084222 441 | Dl -0.332804618418 -0.611698760513 442 | Dl -0.335078776459 -0.599411462818 443 | Dl -0.338717466187 -0.587955263282 444 | Dl -0.34359667278 -0.577494234053 445 | Dl -0.349592381417 -0.568192447278 446 | Dl -0.356580577273 -0.560213975102 447 | Dl -0.364437245526 -0.553722889673 448 | Dl -0.373038371355 -0.548883263138 449 | Dl -0.382259939935 -0.545859167642 450 | Dl -0.391977936444 -0.544814675333 451 | Dl -0.693055256 -0.544814675333 452 | Dl -0.693055256 0.671999333333 453 | Dl -0.694042585976 0.684300999329 454 | Dl -0.69690207426 0.69596052674 455 | Dl -0.701479968386 0.706824163099 456 | Dl -0.70762251589 0.716738155943 457 | Dl -0.715175964306 0.725548752806 458 | Dl -0.723986561168 0.733102201221 459 | Dl -0.733900554012 0.739244748725 460 | Dl -0.744764190372 0.743822642852 461 | Dl -0.756423717782 0.746682131135 462 | Dl -0.768725383778 0.747669461111 463 | Dl -0.822912868889 0.747669461111 464 | Dl -0.835214534885 0.746682131135 465 | Dl -0.846874062295 0.743822642852 466 | Dl -0.857737698655 0.739244748725 467 | Dl -0.867651691499 0.733102201221 468 | Dl -0.876462288361 0.725548752806 469 | Dl -0.884015736777 0.716738155943 470 | Dl -0.890158284281 0.706824163099 471 | Dl -0.894736178407 0.69596052674 472 | Dl -0.897595666691 0.684300999329 473 | Dl -0.898582996667 0.671999333333 474 | Dl -0.898582996667 -0.544814675333 475 | Dl -1.19004122333 -0.544814675333 476 | Dl -1.19975921378 -0.545859167642 477 | Dl -1.20898078052 -0.548883263138 478 | Dl -1.2175819079 -0.553722889673 479 | Dl -1.22543858025 -0.560213975102 480 | Dl -1.23242678189 -0.568192447278 481 | Dl -1.23842249716 -0.577494234053 482 | Dl -1.24330171038 -0.587955263282 483 | Dl -1.2469404059 -0.599411462818 484 | Dl -1.24921456803 -0.611698760513 485 | Dl -1.25000018111 -0.624653084222 486 | Dl -1.25000018111 -0.667938945778 487 | Dl -1.24921456955 -0.680893269013 488 | Dl -1.24694041132 -0.693180565421 489 | Dl -1.24330172105 -0.70463676306 490 | Dl -1.23842251341 -0.715097789986 491 | Dl -1.23242680306 -0.724399574256 492 | Dl -1.22543860463 -0.732378043925 493 | Dl -1.21758193279 -0.738869127051 494 | Dl -1.2089808022 -0.74370875169 495 | Dl -1.19975922749 -0.746732845899 496 | Dl -1.19004122333 -0.747777337733 497 | Dl -0.391977936444 -0.747777337733 498 | Dl -0.391977936444 -0.747777337733 499 | $EndMODULE DT-Logo-Back 500 | $EndLIBRARY -------------------------------------------------------------------------------- /svg/LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /svg/svg/svg.py: -------------------------------------------------------------------------------- 1 | # SVG parser in Python 2 | 3 | # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > 4 | 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with this program; if not, write to the Free Software Foundation, Inc., 17 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | 19 | from __future__ import absolute_import 20 | import traceback 21 | import sys 22 | import os 23 | import copy 24 | import re 25 | import xml.etree.ElementTree as etree 26 | import itertools 27 | import operator 28 | import json 29 | from .geometry import * 30 | 31 | svg_ns = '{http://www.w3.org/2000/svg}' 32 | 33 | # Regex commonly used 34 | number_re = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?' 35 | unit_re = r'em|ex|px|in|cm|mm|pt|pc|%' 36 | 37 | # Unit converter 38 | unit_convert = { 39 | None: 1, # Default unit (same as pixel) 40 | 'px': 1, # px: pixel. Default SVG unit 41 | 'em': 10, # 1 em = 10 px FIXME 42 | 'ex': 5, # 1 ex = 5 px FIXME 43 | 'in': 96, # 1 in = 96 px 44 | 'cm': 96 / 2.54, # 1 cm = 1/2.54 in 45 | 'mm': 96 / 25.4, # 1 mm = 1/25.4 in 46 | 'pt': 96 / 72.0, # 1 pt = 1/72 in 47 | 'pc': 96 / 6.0, # 1 pc = 1/6 in 48 | '%' : 1 / 100.0 # 1 percent 49 | } 50 | 51 | class Transformable: 52 | '''Abstract class for objects that can be geometrically drawn & transformed''' 53 | def __init__(self, elt=None): 54 | # a 'Transformable' is represented as a list of Transformable items 55 | self.items = [] 56 | self.id = hex(id(self)) 57 | # Unit transformation matrix on init 58 | self.matrix = Matrix() 59 | self.viewport = Point(800, 600) # default viewport is 800x600 60 | if elt is not None: 61 | self.id = elt.get('id', self.id) 62 | # Parse transform attibute to update self.matrix 63 | self.getTransformations(elt) 64 | 65 | def bbox(self): 66 | '''Bounding box''' 67 | bboxes = [x.bbox() for x in self.items] 68 | if len( bboxes ) < 1: 69 | return (Point(0, 0), Point(0, 0)) 70 | xmin = min([b[0].x for b in bboxes]) 71 | xmax = max([b[1].x for b in bboxes]) 72 | ymin = min([b[0].y for b in bboxes]) 73 | ymax = max([b[1].y for b in bboxes]) 74 | 75 | return (Point(xmin,ymin), Point(xmax,ymax)) 76 | 77 | # Parse transform field 78 | def getTransformations(self, elt): 79 | t = elt.get('transform') 80 | if t is None: return 81 | 82 | svg_transforms = [ 83 | 'matrix', 'translate', 'scale', 'rotate', 'skewX', 'skewY'] 84 | 85 | # match any SVG transformation with its parameter (until final parenthese) 86 | # [^)]* == anything but a closing parenthese 87 | # '|'.join == OR-list of SVG transformations 88 | transforms = re.findall( 89 | '|'.join([x + '[^)]*\)' for x in svg_transforms]), t) 90 | 91 | for t in transforms: 92 | op, arg = t.split('(') 93 | op = op.strip() 94 | # Keep only numbers 95 | arg = [float(x) for x in re.findall(number_re, arg)] 96 | print('transform: ' + op + ' '+ str(arg)) 97 | 98 | if op == 'matrix': 99 | self.matrix *= Matrix(arg) 100 | 101 | if op == 'translate': 102 | tx = arg[0] 103 | if len(arg) == 1: ty = 0 104 | else: ty = arg[1] 105 | self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) 106 | 107 | if op == 'scale': 108 | sx = arg[0] 109 | if len(arg) == 1: sy = sx 110 | else: sy = arg[1] 111 | self.matrix *= Matrix([sx, 0, 0, sy, 0, 0]) 112 | 113 | if op == 'rotate': 114 | cosa = math.cos(math.radians(arg[0])) 115 | sina = math.sin(math.radians(arg[0])) 116 | if len(arg) != 1: 117 | tx, ty = arg[1:3] 118 | self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) 119 | self.matrix *= Matrix([cosa, sina, -sina, cosa, 0, 0]) 120 | if len(arg) != 1: 121 | self.matrix *= Matrix([1, 0, 0, 1, -tx, -ty]) 122 | 123 | if op == 'skewX': 124 | tana = math.tan(math.radians(arg[0])) 125 | self.matrix *= Matrix([1, 0, tana, 1, 0, 0]) 126 | 127 | if op == 'skewY': 128 | tana = math.tan(math.radians(arg[0])) 129 | self.matrix *= Matrix([1, tana, 0, 1, 0, 0]) 130 | 131 | def transform(self, matrix=None): 132 | if matrix is None: 133 | matrix = self.matrix 134 | else: 135 | matrix *= self.matrix 136 | #print( "do transform: {}: {}".format( self.__class__.__name__, matrix ) ) 137 | #print( "do transform: {}: {}".format( self, matrix ) ) 138 | #traceback.print_stack() 139 | for x in self.items: 140 | x.transform(matrix) 141 | 142 | def length(self, v, mode='xy'): 143 | # Handle empty (non-existing) length element 144 | if v is None: 145 | return 0 146 | 147 | # Get length value 148 | m = re.search(number_re, v) 149 | if m: value = m.group(0) 150 | else: raise TypeError(v + 'is not a valid length') 151 | 152 | # Get length unit 153 | m = re.search(unit_re, v) 154 | if m: unit = m.group(0) 155 | else: unit = None 156 | 157 | if unit == '%': 158 | if mode == 'x': 159 | return float(value) * unit_convert[unit] * self.viewport.x 160 | if mode == 'y': 161 | return float(value) * unit_convert[unit] * self.viewport.y 162 | if mode == 'xy': 163 | return float(value) * unit_convert[unit] * self.viewport.x # FIXME 164 | 165 | return float(value) * unit_convert[unit] 166 | 167 | def xlength(self, x): 168 | return self.length(x, 'x') 169 | def ylength(self, y): 170 | return self.length(y, 'y') 171 | 172 | def flatten(self): 173 | '''Flatten the SVG objects nested list into a flat (1-D) list, 174 | removing Groups''' 175 | # http://rightfootin.blogspot.fr/2006/09/more-on-python-flatten.html 176 | # Assigning a slice a[i:i+1] with a list actually replaces the a[i] 177 | # element with the content of the assigned list 178 | i = 0 179 | flat = copy.deepcopy(self.items) 180 | while i < len(flat): 181 | while isinstance(flat[i], Group): 182 | flat[i:i+1] = flat[i].items 183 | i += 1 184 | return flat 185 | 186 | def scale(self, ratio): 187 | for x in self.items: 188 | x.scale(ratio) 189 | return self 190 | 191 | def translate(self, offset): 192 | for x in self.items: 193 | x.translate(offset) 194 | return self 195 | 196 | def rotate(self, angle): 197 | for x in self.items: 198 | x.rotate(angle) 199 | return self 200 | 201 | class Svg(Transformable): 202 | '''SVG class: use parse to parse a file''' 203 | # class Svg handles the tag 204 | # tag = 'svg' 205 | 206 | def __init__(self, filename=None): 207 | Transformable.__init__(self) 208 | if filename: 209 | self.parse(filename) 210 | 211 | def parse(self, filename): 212 | self.filename = filename 213 | tree = etree.parse(filename) 214 | self.root = tree.getroot() 215 | if self.root.tag != svg_ns + 'svg': 216 | raise TypeError('file %s does not seem to be a valid SVG file', filename) 217 | 218 | # Create a top Group to group all other items (useful for viewBox elt) 219 | top_group = Group() 220 | self.items.append(top_group) 221 | 222 | # SVG dimension 223 | width = self.xlength(self.root.get('width')) 224 | height = self.ylength(self.root.get('height')) 225 | # update viewport 226 | top_group.viewport = Point(width, height) 227 | 228 | # viewBox 229 | if self.root.get('viewBox') is not None: 230 | viewBox = re.findall(number_re, self.root.get('viewBox')) 231 | sx = width / float(viewBox[2]) 232 | sy = height / float(viewBox[3]) 233 | tx = -float(viewBox[0]) 234 | ty = -float(viewBox[1]) 235 | top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) 236 | 237 | # Parse XML elements hierarchically with groups 238 | top_group.append(self.root) 239 | 240 | self.transform() 241 | 242 | def title(self): 243 | t = self.root.find(svg_ns + 'title') 244 | if t is not None: 245 | return t 246 | else: 247 | return os.path.splitext(os.path.basename(self.filename))[0] 248 | 249 | def json(self): 250 | return self.items 251 | 252 | 253 | class Group(Transformable): 254 | '''Handle svg elements''' 255 | # class Group handles the tag 256 | tag = 'g' 257 | 258 | def __init__(self, elt=None): 259 | Transformable.__init__(self, elt) 260 | 261 | self.name = "" 262 | if elt is not None: 263 | for id, value in elt.attrib.iteritems(): 264 | 265 | id = self.parse_name( id ) 266 | if id[ "name" ] == "label": 267 | self.name = value 268 | 269 | @staticmethod 270 | def parse_name( tag ): 271 | m = re.match( r'({(.+)})?(.+)', tag ) 272 | return { 273 | 'namespace' : m.group( 2 ), 274 | 'name' : m.group( 3 ), 275 | } 276 | 277 | def append(self, element): 278 | for elt in element: 279 | elt_class = svgClass.get(elt.tag, None) 280 | if elt_class is None: 281 | print('No handler for element %s' % elt.tag) 282 | continue 283 | # instanciate elt associated class (e.g. : item = Path(elt) 284 | item = elt_class(elt) 285 | # Apply group matrix to the newly created object 286 | # Actually, this is effectively done in Svg.__init__() through call to 287 | # self.transform(), so doing it here will result in the transformations 288 | # being applied twice. 289 | #item.matrix = self.matrix * item.matrix 290 | item.viewport = self.viewport 291 | 292 | self.items.append(item) 293 | # Recursively append if elt is a (group) 294 | if elt.tag == svg_ns + 'g': 295 | item.append(elt) 296 | 297 | def __repr__(self): 298 | return ': ' + repr(self.items) 299 | 300 | def json(self): 301 | return {'Group ' + self.id + " ({})".format( self.name ) : self.items} 302 | 303 | class Matrix: 304 | ''' SVG transformation matrix and its operations 305 | a SVG matrix is represented as a list of 6 values [a, b, c, d, e, f] 306 | (named vect hereafter) which represent the 3x3 matrix 307 | ((a, c, e) 308 | (b, d, f) 309 | (0, 0, 1)) 310 | see http://www.w3.org/TR/SVG/coords.html#EstablishingANewUserSpace ''' 311 | 312 | def __init__(self, vect=[1, 0, 0, 1, 0, 0]): 313 | # Unit transformation vect by default 314 | if len(vect) != 6: 315 | raise ValueError("Bad vect size %d" % len(vect)) 316 | self.vect = list(vect) 317 | 318 | def __mul__(self, other): 319 | '''Matrix multiplication''' 320 | if isinstance(other, Matrix): 321 | a = self.vect[0] * other.vect[0] + self.vect[2] * other.vect[1] 322 | b = self.vect[1] * other.vect[0] + self.vect[3] * other.vect[1] 323 | c = self.vect[0] * other.vect[2] + self.vect[2] * other.vect[3] 324 | d = self.vect[1] * other.vect[2] + self.vect[3] * other.vect[3] 325 | e = self.vect[0] * other.vect[4] + self.vect[2] * other.vect[5] \ 326 | + self.vect[4] 327 | f = self.vect[1] * other.vect[4] + self.vect[3] * other.vect[5] \ 328 | + self.vect[5] 329 | return Matrix([a, b, c, d, e, f]) 330 | 331 | elif isinstance(other, Point): 332 | x = other.x * self.vect[0] + other.y * self.vect[2] + self.vect[4] 333 | y = other.x * self.vect[1] + other.y * self.vect[3] + self.vect[5] 334 | return Point(x,y) 335 | 336 | else: 337 | return NotImplemented 338 | 339 | def __str__(self): 340 | return str(self.vect) 341 | 342 | def xlength(self, x): 343 | return x * self.vect[0] 344 | def ylength(self, y): 345 | return y * self.vect[3] 346 | 347 | 348 | COMMANDS = 'MmZzLlHhVvCcSsQqTtAa' 349 | 350 | class Path(Transformable): 351 | '''SVG ''' 352 | # class Path handles the tag 353 | tag = 'path' 354 | 355 | def __init__(self, elt=None): 356 | Transformable.__init__(self, elt) 357 | if elt is not None: 358 | self.style = elt.get('style') 359 | self.parse(elt.get('d')) 360 | 361 | def parse(self, pathstr): 362 | """Parse path string and build elements list""" 363 | 364 | pathlst = re.findall(number_re + r"|\ *[%s]\ *" % COMMANDS, pathstr) 365 | 366 | pathlst.reverse() 367 | 368 | command = None 369 | current_pt = Point(0,0) 370 | start_pt = None 371 | 372 | while pathlst: 373 | if pathlst[-1].strip() in COMMANDS: 374 | last_command = command 375 | command = pathlst.pop().strip() 376 | absolute = (command == command.upper()) 377 | command = command.upper() 378 | else: 379 | if command is None: 380 | raise ValueError("No command found at %d" % len(pathlst)) 381 | 382 | if command == 'M': 383 | # MoveTo 384 | x = pathlst.pop() 385 | y = pathlst.pop() 386 | pt = Point(x, y) 387 | if absolute: 388 | current_pt = pt 389 | else: 390 | current_pt += pt 391 | start_pt = current_pt 392 | 393 | self.items.append(MoveTo(current_pt)) 394 | 395 | # MoveTo with multiple coordinates means LineTo 396 | command = 'L' 397 | 398 | elif command == 'Z': 399 | # Close Path 400 | l = Segment(current_pt, start_pt) 401 | self.items.append(l) 402 | 403 | 404 | elif command in 'LHV': 405 | # LineTo, Horizontal & Vertical line 406 | # extra coord for H,V 407 | if absolute: 408 | x,y = current_pt.coord() 409 | else: 410 | x,y = (0,0) 411 | 412 | if command in 'LH': 413 | x = pathlst.pop() 414 | if command in 'LV': 415 | y = pathlst.pop() 416 | 417 | pt = Point(x, y) 418 | if not absolute: 419 | pt += current_pt 420 | 421 | self.items.append(Segment(current_pt, pt)) 422 | current_pt = pt 423 | 424 | elif command in 'CQ': 425 | dimension = {'Q':3, 'C':4} 426 | bezier_pts = [] 427 | bezier_pts.append(current_pt) 428 | for i in range(1,dimension[command]): 429 | x = pathlst.pop() 430 | y = pathlst.pop() 431 | pt = Point(x, y) 432 | if not absolute: 433 | pt += current_pt 434 | bezier_pts.append(pt) 435 | 436 | self.items.append(Bezier(bezier_pts)) 437 | current_pt = pt 438 | 439 | elif command in 'TS': 440 | # number of points to read 441 | nbpts = {'T':1, 'S':2} 442 | # the control point, from previous Bezier to mirror 443 | ctrlpt = {'T':1, 'S':2} 444 | # last command control 445 | last = {'T': 'QT', 'S':'CS'} 446 | 447 | bezier_pts = [] 448 | bezier_pts.append(current_pt) 449 | 450 | if last_command in last[command]: 451 | pt0 = self.items[-1].control_point(ctrlpt[command]) 452 | else: 453 | pt0 = current_pt 454 | pt1 = current_pt 455 | # Symetrical of pt1 against pt0 456 | bezier_pts.append(pt1 + pt1 - pt0) 457 | 458 | for i in range(0,nbpts[command]): 459 | x = pathlst.pop() 460 | y = pathlst.pop() 461 | pt = Point(x, y) 462 | if not absolute: 463 | pt += current_pt 464 | bezier_pts.append(pt) 465 | 466 | self.items.append(Bezier(bezier_pts)) 467 | current_pt = pt 468 | 469 | elif command == 'A': 470 | rx = pathlst.pop() 471 | ry = pathlst.pop() 472 | xrot = pathlst.pop() 473 | # Arc flags are not necesarily sepatated numbers 474 | flags = pathlst.pop().strip() 475 | large_arc_flag = flags[0] 476 | if large_arc_flag not in '01': 477 | print('Arc parsing failure') 478 | break 479 | 480 | if len(flags) > 1: flags = flags[1:].strip() 481 | else: flags = pathlst.pop().strip() 482 | sweep_flag = flags[0] 483 | if sweep_flag not in '01': 484 | print('Arc parsing failure') 485 | break 486 | 487 | if len(flags) > 1: x = flags[1:] 488 | else: x = pathlst.pop() 489 | y = pathlst.pop() 490 | # TODO 491 | print('ARC: ' + 492 | ', '.join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y])) 493 | # self.items.append( 494 | # Arc(rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) 495 | 496 | else: 497 | pathlst.pop() 498 | 499 | def __str__(self): 500 | return '\n'.join(str(x) for x in self.items) 501 | 502 | def __repr__(self): 503 | return '' 504 | 505 | def segments(self, precision=0): 506 | '''Return a list of segments, each segment is ended by a MoveTo. 507 | A segment is a list of Points''' 508 | ret = [] 509 | # group items separated by MoveTo 510 | for moveTo, group in itertools.groupby(self.items, 511 | lambda x: isinstance(x, MoveTo)): 512 | # Use only non MoveTo item 513 | if not moveTo: 514 | # Generate segments for each relevant item 515 | seg = [x.segments(precision) for x in group] 516 | # Merge all segments into one 517 | ret.append(list(itertools.chain.from_iterable(seg))) 518 | 519 | return ret 520 | 521 | def simplify(self, precision): 522 | '''Simplify segment with precision: 523 | Remove any point which are ~aligned''' 524 | ret = [] 525 | for seg in self.segments(precision): 526 | ret.append(simplify_segment(seg, precision)) 527 | 528 | return ret 529 | 530 | class Ellipse(Transformable): 531 | '''SVG ''' 532 | # class Ellipse handles the tag 533 | tag = 'ellipse' 534 | 535 | def __init__(self, elt=None): 536 | Transformable.__init__(self, elt) 537 | if elt is not None: 538 | self.center = Point(self.xlength(elt.get('cx')), 539 | self.ylength(elt.get('cy'))) 540 | self.rx = self.length(elt.get('rx')) 541 | self.ry = self.length(elt.get('ry')) 542 | self.style = elt.get('style') 543 | 544 | def __repr__(self): 545 | return '' 546 | 547 | def bbox(self): 548 | '''Bounding box''' 549 | pmin = self.center - Point(self.rx, self.ry) 550 | pmax = self.center + Point(self.rx, self.ry) 551 | return (pmin, pmax) 552 | 553 | def transform(self, matrix): 554 | self.center = self.matrix * self.center 555 | self.rx = self.matrix.xlength(self.rx) 556 | self.ry = self.matrix.ylength(self.ry) 557 | 558 | def scale(self, ratio): 559 | self.center *= ratio 560 | self.rx *= ratio 561 | self.ry *= ratio 562 | def translate(self, offset): 563 | self.center += offset 564 | def rotate(self, angle): 565 | self.center = self.center.rot(angle) 566 | 567 | def P(self, t): 568 | '''Return a Point on the Ellipse for t in [0..1]''' 569 | x = self.center.x + self.rx * math.cos(2 * math.pi * t) 570 | y = self.center.y + self.ry * math.sin(2 * math.pi * t) 571 | return Point(x,y) 572 | 573 | def segments(self, precision=0): 574 | if max(self.rx, self.ry) < precision: 575 | return [[self.center]] 576 | 577 | p = [(0,self.P(0)), (1, self.P(1))] 578 | d = 2 * max(self.rx, self.ry) 579 | 580 | while d > precision: 581 | for (t1,p1),(t2,p2) in zip(p[:-1],p[1:]): 582 | t = t1 + (t2 - t1)/2. 583 | d = Segment(p1, p2).pdistance(self.P(t)) 584 | p.append((t, self.P(t))) 585 | p.sort(key=operator.itemgetter(0)) 586 | 587 | ret = [x for t,x in p] 588 | return [ret] 589 | 590 | def simplify(self, precision): 591 | return self 592 | 593 | # A circle is a special type of ellipse where rx = ry = radius 594 | class Circle(Ellipse): 595 | '''SVG ''' 596 | # class Circle handles the tag 597 | tag = 'circle' 598 | 599 | def __init__(self, elt=None): 600 | if elt is not None: 601 | elt.set('rx', elt.get('r')) 602 | elt.set('ry', elt.get('r')) 603 | Ellipse.__init__(self, elt) 604 | 605 | def __repr__(self): 606 | return '' 607 | 608 | class Rect(Transformable): 609 | '''SVG ''' 610 | # class Rect handles the tag 611 | tag = 'rect' 612 | 613 | def __init__(self, elt=None): 614 | Transformable.__init__(self, elt) 615 | if elt is not None: 616 | self.P1 = Point(self.xlength(elt.get('x')), 617 | self.ylength(elt.get('y'))) 618 | 619 | self.P2 = Point(self.P1.x + self.xlength(elt.get('width')), 620 | self.P1.y + self.ylength(elt.get('height'))) 621 | 622 | def __repr__(self): 623 | return '' 624 | 625 | def bbox(self): 626 | '''Bounding box''' 627 | xmin = min([p.x for p in (self.P1, self.P2)]) 628 | xmax = max([p.x for p in (self.P1, self.P2)]) 629 | ymin = min([p.y for p in (self.P1, self.P2)]) 630 | ymax = max([p.y for p in (self.P1, self.P2)]) 631 | 632 | return (Point(xmin,ymin), Point(xmax,ymax)) 633 | 634 | def transform(self, matrix): 635 | self.P1 = self.matrix * self.P1 636 | self.P2 = self.matrix * self.P2 637 | 638 | def segments(self, precision=0): 639 | # A rectangle is built with a segment going thru 4 points 640 | ret = [] 641 | Pa = Point(self.P1.x, self.P2.y) 642 | Pb = Point(self.P2.x, self.P1.y) 643 | 644 | ret.append([self.P1, Pa, self.P2, Pb, self.P1]) 645 | return ret 646 | 647 | def simplify(self, precision): 648 | return self.segments(precision) 649 | 650 | class Line(Transformable): 651 | '''SVG ''' 652 | # class Line handles the tag 653 | tag = 'line' 654 | 655 | def __init__(self, elt=None): 656 | Transformable.__init__(self, elt) 657 | if elt is not None: 658 | self.P1 = Point(self.xlength(elt.get('x1')), 659 | self.ylength(elt.get('y1'))) 660 | self.P2 = Point(self.xlength(elt.get('x2')), 661 | self.ylength(elt.get('y2'))) 662 | self.segment = Segment(self.P1, self.P2) 663 | 664 | def __repr__(self): 665 | return '' 666 | 667 | def bbox(self): 668 | '''Bounding box''' 669 | xmin = min([p.x for p in (self.P1, self.P2)]) 670 | xmax = max([p.x for p in (self.P1, self.P2)]) 671 | ymin = min([p.y for p in (self.P1, self.P2)]) 672 | ymax = max([p.y for p in (self.P1, self.P2)]) 673 | 674 | return (Point(xmin,ymin), Point(xmax,ymax)) 675 | 676 | def transform(self, matrix): 677 | self.P1 = self.matrix * self.P1 678 | self.P2 = self.matrix * self.P2 679 | self.segment = Segment(self.P1, self.P2) 680 | 681 | def segments(self, precision=0): 682 | return [self.segment.segments()] 683 | 684 | def simplify(self, precision): 685 | return self.segments(precision) 686 | 687 | # overwrite JSONEncoder for svg classes which have defined a .json() method 688 | class JSONEncoder(json.JSONEncoder): 689 | def default(self, obj): 690 | if not isinstance(obj, tuple(svgClass.values() + [Svg])): 691 | return json.JSONEncoder.default(self, obj) 692 | 693 | if not hasattr(obj, 'json'): 694 | return repr(obj) 695 | 696 | return obj.json() 697 | 698 | ## Code executed on module load ## 699 | 700 | # SVG tag handler classes are initialized here 701 | # (classes must be defined before) 702 | import inspect 703 | svgClass = {} 704 | # Register all classes with attribute 'tag' in svgClass dict 705 | for name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): 706 | tag = getattr(cls, 'tag', None) 707 | if tag: 708 | svgClass[svg_ns + tag] = cls 709 | 710 | -------------------------------------------------------------------------------- /svg2mod.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import absolute_import 4 | 5 | import argparse 6 | import datetime 7 | import os 8 | from pprint import pformat, pprint 9 | import re 10 | import svg 11 | import sys 12 | 13 | 14 | #---------------------------------------------------------------------------- 15 | 16 | def main(): 17 | 18 | args, parser = get_arguments() 19 | 20 | pretty = args.format == 'pretty' 21 | use_mm = args.units == 'mm' 22 | 23 | if pretty: 24 | 25 | if not use_mm: 26 | print( "Error: decimil units only allowed with legacy output type" ) 27 | sys.exit( -1 ) 28 | 29 | #if args.include_reverse: 30 | #print( 31 | #"Warning: reverse footprint not supported or required for" + 32 | #" pretty output format" 33 | #) 34 | 35 | # Import the SVG: 36 | imported = Svg2ModImport( 37 | args.input_file_name, 38 | args.module_name, 39 | args.module_value 40 | ) 41 | 42 | # Pick an output file name if none was provided: 43 | if args.output_file_name is None: 44 | 45 | args.output_file_name = os.path.splitext( 46 | os.path.basename( args.input_file_name ) 47 | )[ 0 ] 48 | 49 | # Append the correct file name extension if needed: 50 | if pretty: 51 | extension = ".kicad_mod" 52 | else: 53 | extension = ".mod" 54 | if args.output_file_name[ - len( extension ) : ] != extension: 55 | args.output_file_name += extension 56 | 57 | # Create an exporter: 58 | if pretty: 59 | exported = Svg2ModExportPretty( 60 | imported, 61 | args.output_file_name, 62 | args.scale_factor, 63 | args.precision, 64 | ) 65 | 66 | else: 67 | 68 | # If the module file exists, try to read it: 69 | exported = None 70 | if os.path.isfile( args.output_file_name ): 71 | 72 | try: 73 | exported = Svg2ModExportLegacyUpdater( 74 | imported, 75 | args.output_file_name, 76 | args.scale_factor, 77 | args.precision, 78 | include_reverse = not args.front_only, 79 | ) 80 | 81 | except Exception as e: 82 | raise e 83 | #print( e.message ) 84 | #exported = None 85 | 86 | # Write the module file: 87 | if exported is None: 88 | exported = Svg2ModExportLegacy( 89 | imported, 90 | args.output_file_name, 91 | args.scale_factor, 92 | args.precision, 93 | use_mm = use_mm, 94 | include_reverse = not args.front_only, 95 | ) 96 | 97 | # Export the footprint: 98 | exported.write() 99 | 100 | 101 | #---------------------------------------------------------------------------- 102 | 103 | class LineSegment( object ): 104 | 105 | #------------------------------------------------------------------------ 106 | 107 | @staticmethod 108 | def _on_segment( p, q, r ): 109 | """ Given three colinear points p, q, and r, check if 110 | point q lies on line segment pr. """ 111 | 112 | if ( 113 | q.x <= max( p.x, r.x ) and 114 | q.x >= min( p.x, r.x ) and 115 | q.y <= max( p.y, r.y ) and 116 | q.y >= min( p.y, r.y ) 117 | ): 118 | return True 119 | 120 | return False 121 | 122 | 123 | #------------------------------------------------------------------------ 124 | 125 | @staticmethod 126 | def _orientation( p, q, r ): 127 | """ Find orientation of ordered triplet (p, q, r). 128 | Returns following values 129 | 0 --> p, q and r are colinear 130 | 1 --> Clockwise 131 | 2 --> Counterclockwise 132 | """ 133 | 134 | val = ( 135 | ( q.y - p.y ) * ( r.x - q.x ) - 136 | ( q.x - p.x ) * ( r.y - q.y ) 137 | ) 138 | 139 | if val == 0: return 0 140 | if val > 0: return 1 141 | return 2 142 | 143 | 144 | #------------------------------------------------------------------------ 145 | 146 | def __init__( self, p = None, q = None ): 147 | 148 | self.p = p 149 | self.q = q 150 | 151 | 152 | #------------------------------------------------------------------------ 153 | 154 | def connects( self, segment ): 155 | 156 | if self.q.x == segment.p.x and self.q.y == segment.p.y: return True 157 | if self.q.x == segment.q.x and self.q.y == segment.q.y: return True 158 | if self.p.x == segment.p.x and self.p.y == segment.p.y: return True 159 | if self.p.x == segment.q.x and self.p.y == segment.q.y: return True 160 | return False 161 | 162 | 163 | #------------------------------------------------------------------------ 164 | 165 | def intersects( self, segment ): 166 | """ Return true if line segments 'p1q1' and 'p2q2' intersect. 167 | Adapted from: 168 | http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/ 169 | """ 170 | 171 | # Find the four orientations needed for general and special cases: 172 | o1 = self._orientation( self.p, self.q, segment.p ) 173 | o2 = self._orientation( self.p, self.q, segment.q ) 174 | o3 = self._orientation( segment.p, segment.q, self.p ) 175 | o4 = self._orientation( segment.p, segment.q, self.q ) 176 | 177 | return ( 178 | 179 | # General case: 180 | ( o1 != o2 and o3 != o4 ) 181 | 182 | or 183 | 184 | # p1, q1 and p2 are colinear and p2 lies on segment p1q1: 185 | ( o1 == 0 and self._on_segment( self.p, segment.p, self.q ) ) 186 | 187 | or 188 | 189 | # p1, q1 and p2 are colinear and q2 lies on segment p1q1: 190 | ( o2 == 0 and self._on_segment( self.p, segment.q, self.q ) ) 191 | 192 | or 193 | 194 | # p2, q2 and p1 are colinear and p1 lies on segment p2q2: 195 | ( o3 == 0 and self._on_segment( segment.p, self.p, segment.q ) ) 196 | 197 | or 198 | 199 | # p2, q2 and q1 are colinear and q1 lies on segment p2q2: 200 | ( o4 == 0 and self._on_segment( segment.p, self.q, segment.q ) ) 201 | ) 202 | 203 | 204 | #------------------------------------------------------------------------ 205 | 206 | def q_next( self, q ): 207 | 208 | self.p = self.q 209 | self.q = q 210 | 211 | 212 | #------------------------------------------------------------------------ 213 | 214 | #---------------------------------------------------------------------------- 215 | 216 | class PolygonSegment( object ): 217 | 218 | #------------------------------------------------------------------------ 219 | 220 | def __init__( self, points ): 221 | 222 | self.points = points 223 | 224 | if len( points ) < 3: 225 | print( 226 | "Warning:" 227 | " Path segment has only {} points (not a polygon?)".format( 228 | len( points ) 229 | ) 230 | ) 231 | 232 | 233 | #------------------------------------------------------------------------ 234 | 235 | # KiCad will not "pick up the pen" when moving between a polygon outline 236 | # and holes within it, so we search for a pair of points connecting the 237 | # outline (self) to the hole such that the connecting segment will not 238 | # cross the visible inner space within any hole. 239 | def _find_insertion_point( self, hole, holes ): 240 | 241 | #print( " Finding insertion point. {} holes".format( len( holes ) ) ) 242 | 243 | # Try the next point on the container: 244 | for cp in range( len( self.points ) ): 245 | container_point = self.points[ cp ] 246 | 247 | #print( " Trying container point {}".format( cp ) ) 248 | 249 | # Try the next point on the hole: 250 | for hp in range( len( hole.points ) - 1 ): 251 | hole_point = hole.points[ hp ] 252 | 253 | #print( " Trying hole point {}".format( cp ) ) 254 | 255 | bridge = LineSegment( container_point, hole_point ) 256 | 257 | # Check for intersection with each other hole: 258 | for other_hole in holes: 259 | 260 | #print( " Trying other hole. Check = {}".format( hole == other_hole ) ) 261 | 262 | # If the other hole intersects, don't bother checking 263 | # remaining holes: 264 | if other_hole.intersects( 265 | bridge, 266 | check_connects = ( 267 | other_hole == hole or other_hole == self 268 | ) 269 | ): break 270 | 271 | #print( " Hole does not intersect." ) 272 | 273 | else: 274 | print( " Found insertion point: {}, {}".format( cp, hp ) ) 275 | 276 | # No other holes intersected, so this insertion point 277 | # is acceptable: 278 | return ( cp, hole.points_starting_on_index( hp ) ) 279 | 280 | print( 281 | "Could not insert segment without overlapping other segments" 282 | ) 283 | 284 | 285 | #------------------------------------------------------------------------ 286 | 287 | # Return the list of ordered points starting on the given index, ensuring 288 | # that the first and last points are the same. 289 | def points_starting_on_index( self, index ): 290 | 291 | points = self.points 292 | 293 | if index > 0: 294 | 295 | # Strip off end point, which is a duplicate of the start point: 296 | points = points[ : -1 ] 297 | 298 | points = points[ index : ] + points[ : index ] 299 | 300 | points.append( 301 | svg.Point( points[ 0 ].x, points[ 0 ].y ) 302 | ) 303 | 304 | return points 305 | 306 | 307 | #------------------------------------------------------------------------ 308 | 309 | # Return a list of points with the given polygon segments (paths) inlined. 310 | def inline( self, segments ): 311 | 312 | if len( segments ) < 1: 313 | return self.points 314 | 315 | print( " Inlining {} segments...".format( len( segments ) ) ) 316 | 317 | all_segments = segments[ : ] + [ self ] 318 | insertions = [] 319 | 320 | # Find the insertion point for each hole: 321 | for hole in segments: 322 | 323 | insertion = self._find_insertion_point( 324 | hole, all_segments 325 | ) 326 | if insertion is not None: 327 | insertions.append( insertion ) 328 | 329 | insertions.sort( key = lambda i: i[ 0 ] ) 330 | 331 | inlined = [ self.points[ 0 ] ] 332 | ip = 1 333 | points = self.points 334 | 335 | for insertion in insertions: 336 | 337 | while ip <= insertion[ 0 ]: 338 | inlined.append( points[ ip ] ) 339 | ip += 1 340 | 341 | if ( 342 | inlined[ -1 ].x == insertion[ 1 ][ 0 ].x and 343 | inlined[ -1 ].y == insertion[ 1 ][ 0 ].y 344 | ): 345 | inlined += insertion[ 1 ][ 1 : -1 ] 346 | else: 347 | inlined += insertion[ 1 ] 348 | 349 | inlined.append( svg.Point( 350 | points[ ip - 1 ].x, 351 | points[ ip - 1 ].y, 352 | ) ) 353 | 354 | while ip < len( points ): 355 | inlined.append( points[ ip ] ) 356 | ip += 1 357 | 358 | return inlined 359 | 360 | 361 | #------------------------------------------------------------------------ 362 | 363 | def intersects( self, line_segment, check_connects ): 364 | 365 | hole_segment = LineSegment() 366 | 367 | # Check each segment of other hole for intersection: 368 | for point in self.points: 369 | 370 | hole_segment.q_next( point ) 371 | 372 | if hole_segment.p is not None: 373 | 374 | if ( 375 | check_connects and 376 | line_segment.connects( hole_segment ) 377 | ): continue 378 | 379 | if line_segment.intersects( hole_segment ): 380 | 381 | #print( "Intersection detected." ) 382 | 383 | return True 384 | 385 | return False 386 | 387 | 388 | #------------------------------------------------------------------------ 389 | 390 | # Apply all transformations and rounding, then remove duplicate 391 | # consecutive points along the path. 392 | def process( self, transformer, flip ): 393 | 394 | points = [] 395 | for point in self.points: 396 | 397 | point = transformer.transform_point( point, flip ) 398 | 399 | if ( 400 | len( points ) < 1 or 401 | point.x != points[ -1 ].x or 402 | point.y != points[ -1 ].y 403 | ): 404 | points.append( point ) 405 | 406 | if ( 407 | points[ 0 ].x != points[ -1 ].x or 408 | points[ 0 ].y != points[ -1 ].y 409 | ): 410 | #print( "Warning: Closing polygon. start=({}, {}) end=({}, {})".format( 411 | #points[ 0 ].x, points[ 0 ].y, 412 | #points[ -1 ].x, points[ -1 ].y, 413 | #) ) 414 | 415 | points.append( svg.Point( 416 | points[ 0 ].x, 417 | points[ 0 ].y, 418 | ) ) 419 | 420 | #else: 421 | #print( "Polygon closed: start=({}, {}) end=({}, {})".format( 422 | #points[ 0 ].x, points[ 0 ].y, 423 | #points[ -1 ].x, points[ -1 ].y, 424 | #) ) 425 | 426 | self.points = points 427 | 428 | 429 | #------------------------------------------------------------------------ 430 | 431 | #---------------------------------------------------------------------------- 432 | 433 | class Svg2ModImport( object ): 434 | 435 | #------------------------------------------------------------------------ 436 | 437 | def __init__( self, file_name, module_name, module_value ): 438 | 439 | self.file_name = file_name 440 | self.module_name = module_name 441 | self.module_value = module_value 442 | 443 | print( "Parsing SVG..." ) 444 | self.svg = svg.parse( file_name ) 445 | 446 | 447 | #------------------------------------------------------------------------ 448 | 449 | #---------------------------------------------------------------------------- 450 | 451 | class Svg2ModExport( object ): 452 | 453 | #------------------------------------------------------------------------ 454 | 455 | @staticmethod 456 | def _convert_decimil_to_mm( decimil ): 457 | return float( decimil ) * 0.00254 458 | 459 | 460 | #------------------------------------------------------------------------ 461 | 462 | @staticmethod 463 | def _convert_mm_to_decimil( mm ): 464 | return int( round( mm * 393.700787 ) ) 465 | 466 | 467 | #------------------------------------------------------------------------ 468 | 469 | @classmethod 470 | def _get_fill_stroke( cls, item ): 471 | 472 | fill = True 473 | stroke = True 474 | stroke_width = 0.0 475 | 476 | if item.style is not None and item.style != "": 477 | 478 | for property in item.style.split( ";" ): 479 | 480 | nv = property.split( ":" ); 481 | name = nv[ 0 ].strip() 482 | value = nv[ 1 ].strip() 483 | 484 | if name == "fill" and value == "none": 485 | fill = False 486 | 487 | elif name == "stroke" and value == "none": 488 | stroke = False 489 | 490 | elif name == "stroke-width": 491 | value = value.replace( "px", "" ) 492 | stroke_width = float( value ) * 25.4 / 90.0 493 | 494 | if not stroke: 495 | stroke_width = 0.0 496 | elif stroke_width is None: 497 | # Give a default stroke width? 498 | stroke_width = cls._convert_decimil_to_mm( 1 ) 499 | 500 | return fill, stroke, stroke_width 501 | 502 | 503 | #------------------------------------------------------------------------ 504 | 505 | def __init__( 506 | self, 507 | svg2mod_import, 508 | file_name, 509 | scale_factor = 1.0, 510 | precision = 20.0, 511 | use_mm = True, 512 | ): 513 | if use_mm: 514 | # 25.4 mm/in; Inkscape uses 90 DPI: 515 | scale_factor *= 25.4 / 90.0 516 | use_mm = True 517 | else: 518 | # PCBNew uses "decimil" (10K DPI); Inkscape uses 90 DPI: 519 | scale_factor *= 10000.0 / 90.0 520 | 521 | self.imported = svg2mod_import 522 | self.file_name = file_name 523 | self.scale_factor = scale_factor 524 | self.precision = precision 525 | self.use_mm = use_mm 526 | 527 | 528 | #------------------------------------------------------------------------ 529 | 530 | def _calculate_translation( self ): 531 | 532 | min_point, max_point = self.imported.svg.bbox() 533 | 534 | # Center the drawing: 535 | adjust_x = min_point.x + ( max_point.x - min_point.x ) / 2.0 536 | adjust_y = min_point.y + ( max_point.y - min_point.y ) / 2.0 537 | 538 | self.translation = svg.Point( 539 | 0.0 - adjust_x, 540 | 0.0 - adjust_y, 541 | ) 542 | 543 | 544 | #------------------------------------------------------------------------ 545 | 546 | # Find and keep only the layers of interest. 547 | def _prune( self, items = None ): 548 | 549 | if items is None: 550 | 551 | self.layers = {} 552 | for name in self.layer_map.iterkeys(): 553 | self.layers[ name ] = None 554 | 555 | items = self.imported.svg.items 556 | self.imported.svg.items = [] 557 | 558 | for item in items: 559 | 560 | if not isinstance( item, svg.Group ): 561 | continue 562 | 563 | for name in self.layers.iterkeys(): 564 | #if re.search( name, item.name, re.I ): 565 | if name == item.name: 566 | print( "Found SVG layer: {}".format( item.name ) ) 567 | self.imported.svg.items.append( item ) 568 | self.layers[ name ] = item 569 | break 570 | else: 571 | self._prune( item.items ) 572 | 573 | 574 | #------------------------------------------------------------------------ 575 | 576 | def _write_items( self, items, layer, flip = False ): 577 | 578 | for item in items: 579 | 580 | if isinstance( item, svg.Group ): 581 | self._write_items( item.items, layer, flip ) 582 | continue 583 | 584 | elif isinstance( item, svg.Path ): 585 | 586 | segments = [ 587 | PolygonSegment( segment ) 588 | for segment in item.segments( 589 | precision = self.precision 590 | ) 591 | ] 592 | 593 | for segment in segments: 594 | segment.process( self, flip ) 595 | 596 | if len( segments ) > 1: 597 | points = segments[ 0 ].inline( segments[ 1 : ] ) 598 | 599 | elif len( segments ) > 0: 600 | points = segments[ 0 ].points 601 | 602 | fill, stroke, stroke_width = self._get_fill_stroke( item ) 603 | 604 | if not self.use_mm: 605 | stroke_width = self._convert_mm_to_decimil( 606 | stroke_width 607 | ) 608 | 609 | print( " Writing polygon with {} points".format( 610 | len( points ) ) 611 | ) 612 | 613 | self._write_polygon( 614 | points, layer, fill, stroke, stroke_width 615 | ) 616 | 617 | else: 618 | print( "Unsupported SVG element: {}".format( 619 | item.__class__.__name__ 620 | ) ) 621 | 622 | 623 | #------------------------------------------------------------------------ 624 | 625 | def _write_module( self, front ): 626 | 627 | module_name = self._get_module_name( front ) 628 | 629 | min_point, max_point = self.imported.svg.bbox() 630 | min_point = self.transform_point( min_point, flip = False ) 631 | max_point = self.transform_point( max_point, flip = False ) 632 | 633 | label_offset = 1200 634 | label_size = 600 635 | label_pen = 120 636 | 637 | if self.use_mm: 638 | label_size = self._convert_decimil_to_mm( label_size ) 639 | label_pen = self._convert_decimil_to_mm( label_pen ) 640 | reference_y = min_point.y - self._convert_decimil_to_mm( label_offset ) 641 | value_y = max_point.y + self._convert_decimil_to_mm( label_offset ) 642 | else: 643 | reference_y = min_point.y - label_offset 644 | value_y = max_point.y + label_offset 645 | 646 | self._write_module_header( 647 | label_size, label_pen, 648 | reference_y, value_y, 649 | front, 650 | ) 651 | 652 | for name, group in self.layers.iteritems(): 653 | 654 | if group is None: continue 655 | 656 | layer = self._get_layer_name( name, front ) 657 | 658 | #print( " Writing layer: {}".format( name ) ) 659 | self._write_items( group.items, layer, not front ) 660 | 661 | self._write_module_footer( front ) 662 | 663 | 664 | #------------------------------------------------------------------------ 665 | 666 | def _write_polygon_filled( self, points, layer, stroke_width = 0.0 ): 667 | 668 | self._write_polygon_header( points, layer ) 669 | 670 | for point in points: 671 | self._write_polygon_point( point ) 672 | 673 | self._write_polygon_footer( layer, stroke_width ) 674 | 675 | 676 | #------------------------------------------------------------------------ 677 | 678 | def _write_polygon_outline( self, points, layer, stroke_width ): 679 | 680 | prior_point = None 681 | for point in points: 682 | 683 | if prior_point is not None: 684 | 685 | self._write_polygon_segment( 686 | prior_point, point, layer, stroke_width 687 | ) 688 | 689 | prior_point = point 690 | 691 | 692 | #------------------------------------------------------------------------ 693 | 694 | def transform_point( self, point, flip = False ): 695 | 696 | transformed_point = svg.Point( 697 | ( point.x + self.translation.x ) * self.scale_factor, 698 | ( point.y + self.translation.y ) * self.scale_factor, 699 | ) 700 | 701 | if flip: 702 | transformed_point.x *= -1 703 | 704 | if self.use_mm: 705 | transformed_point.x = round( transformed_point.x, 12 ) 706 | transformed_point.y = round( transformed_point.y, 12 ) 707 | else: 708 | transformed_point.x = int( round( transformed_point.x ) ) 709 | transformed_point.y = int( round( transformed_point.y ) ) 710 | 711 | return transformed_point 712 | 713 | 714 | #------------------------------------------------------------------------ 715 | 716 | def write( self ): 717 | 718 | self._prune() 719 | 720 | # Must come after pruning: 721 | translation = self._calculate_translation() 722 | 723 | print( "Writing module file: {}".format( self.file_name ) ) 724 | self.output_file = open( self.file_name, 'w' ) 725 | 726 | self._write_library_intro() 727 | 728 | self._write_modules() 729 | 730 | self.output_file.close() 731 | self.output_file = None 732 | 733 | 734 | #------------------------------------------------------------------------ 735 | 736 | #---------------------------------------------------------------------------- 737 | 738 | class Svg2ModExportLegacy( Svg2ModExport ): 739 | 740 | layer_map = { 741 | #'inkscape-name' : [ kicad-front, kicad-back ], 742 | 'Cu' : [ 15, 0 ], 743 | 'Adhes' : [ 17, 16 ], 744 | 'Paste' : [ 19, 18 ], 745 | 'SilkS' : [ 21, 20 ], 746 | 'Mask' : [ 23, 22 ], 747 | 'Dwgs.User' : [ 24, 24 ], 748 | 'Cmts.User' : [ 25, 25 ], 749 | 'Eco1.User' : [ 26, 26 ], 750 | 'Eco2.User' : [ 27, 27 ], 751 | 'Edge.Cuts' : [ 28, 28 ], 752 | } 753 | 754 | 755 | #------------------------------------------------------------------------ 756 | 757 | def __init__( 758 | self, 759 | svg2mod_import, 760 | file_name, 761 | scale_factor = 1.0, 762 | precision = 20.0, 763 | use_mm = True, 764 | include_reverse = True, 765 | ): 766 | super( Svg2ModExportLegacy, self ).__init__( 767 | svg2mod_import, 768 | file_name, 769 | scale_factor, 770 | precision, 771 | use_mm, 772 | ) 773 | 774 | self.include_reverse = include_reverse 775 | 776 | 777 | #------------------------------------------------------------------------ 778 | 779 | def _get_layer_name( self, name, front ): 780 | 781 | layer_info = self.layer_map[ name ] 782 | layer = layer_info[ 0 ] 783 | if not front and layer_info[ 1 ] is not None: 784 | layer = layer_info[ 1 ] 785 | 786 | return layer 787 | 788 | 789 | #------------------------------------------------------------------------ 790 | 791 | def _get_module_name( self, front = None ): 792 | 793 | if self.include_reverse and not front: 794 | return self.imported.module_name + "-rev" 795 | 796 | return self.imported.module_name 797 | 798 | 799 | #------------------------------------------------------------------------ 800 | 801 | def _write_library_intro( self ): 802 | 803 | modules_list = self._get_module_name( front = True ) 804 | if self.include_reverse: 805 | modules_list += ( 806 | "\n" + 807 | self._get_module_name( front = False ) 808 | ) 809 | 810 | units = "" 811 | if self.use_mm: 812 | units = "\nUnits mm" 813 | 814 | self.output_file.write( """PCBNEW-LibModule-V1 {0}{1} 815 | $INDEX 816 | {2} 817 | $EndINDEX 818 | # 819 | # {3} 820 | # 821 | """.format( 822 | datetime.datetime.now().strftime( "%a %d %b %Y %I:%M:%S %p %Z" ), 823 | units, 824 | modules_list, 825 | self.imported.file_name, 826 | ) 827 | ) 828 | 829 | 830 | #------------------------------------------------------------------------ 831 | 832 | def _write_module_header( 833 | self, 834 | label_size, 835 | label_pen, 836 | reference_y, 837 | value_y, 838 | front, 839 | ): 840 | 841 | self.output_file.write( """$MODULE {0} 842 | Po 0 0 0 {6} 00000000 00000000 ~~ 843 | Li {0} 844 | T0 0 {1} {2} {2} 0 {3} N I 21 "{0}" 845 | T1 0 {5} {2} {2} 0 {3} N I 21 "{4}" 846 | """.format( 847 | self._get_module_name( front ), 848 | reference_y, 849 | label_size, 850 | label_pen, 851 | self.imported.module_value, 852 | value_y, 853 | 15, # Seems necessary 854 | ) 855 | ) 856 | 857 | 858 | #------------------------------------------------------------------------ 859 | 860 | def _write_module_footer( self, front ): 861 | 862 | self.output_file.write( 863 | "$EndMODULE {0}\n".format( self._get_module_name( front ) ) 864 | ) 865 | 866 | 867 | #------------------------------------------------------------------------ 868 | 869 | def _write_modules( self ): 870 | 871 | self._write_module( front = True ) 872 | 873 | if self.include_reverse: 874 | self._write_module( front = False ) 875 | 876 | self.output_file.write( "$EndLIBRARY" ) 877 | 878 | 879 | #------------------------------------------------------------------------ 880 | 881 | def _write_polygon( self, points, layer, fill, stroke, stroke_width ): 882 | 883 | if fill: 884 | self._write_polygon_filled( 885 | points, layer 886 | ) 887 | 888 | if stroke: 889 | 890 | self._write_polygon_outline( 891 | points, layer, stroke_width 892 | ) 893 | 894 | 895 | #------------------------------------------------------------------------ 896 | 897 | def _write_polygon_footer( self, layer, stroke_width ): 898 | 899 | pass 900 | 901 | 902 | #------------------------------------------------------------------------ 903 | 904 | def _write_polygon_header( self, points, layer ): 905 | 906 | pen = 1 907 | if self.use_mm: 908 | pen = self._convert_decimil_to_mm( pen ) 909 | 910 | self.output_file.write( "DP 0 0 0 0 {} {} {}\n".format( 911 | len( points ), 912 | pen, 913 | layer 914 | ) ) 915 | 916 | 917 | #------------------------------------------------------------------------ 918 | 919 | def _write_polygon_point( self, point ): 920 | 921 | self.output_file.write( 922 | "Dl {} {}\n".format( point.x, point.y ) 923 | ) 924 | 925 | 926 | #------------------------------------------------------------------------ 927 | 928 | def _write_polygon_segment( self, p, q, layer, stroke_width ): 929 | 930 | self.output_file.write( "DS {} {} {} {} {} {}\n".format( 931 | p.x, p.y, 932 | q.x, q.y, 933 | stroke_width, 934 | layer 935 | ) ) 936 | 937 | 938 | #------------------------------------------------------------------------ 939 | 940 | #---------------------------------------------------------------------------- 941 | 942 | class Svg2ModExportLegacyUpdater( Svg2ModExportLegacy ): 943 | 944 | #------------------------------------------------------------------------ 945 | 946 | def __init__( 947 | self, 948 | svg2mod_import, 949 | file_name, 950 | scale_factor = 1.0, 951 | precision = 20.0, 952 | include_reverse = True, 953 | ): 954 | self.file_name = file_name 955 | use_mm = self._parse_output_file() 956 | 957 | super( Svg2ModExportLegacyUpdater, self ).__init__( 958 | svg2mod_import, 959 | file_name, 960 | scale_factor, 961 | precision, 962 | use_mm, 963 | include_reverse, 964 | ) 965 | 966 | 967 | #------------------------------------------------------------------------ 968 | 969 | def _parse_output_file( self ): 970 | 971 | print( "Parsing module file: {}".format( self.file_name ) ) 972 | module_file = open( self.file_name, 'r' ) 973 | lines = module_file.readlines() 974 | module_file.close() 975 | 976 | self.loaded_modules = {} 977 | self.post_index = [] 978 | self.pre_index = [] 979 | use_mm = False 980 | 981 | index = 0 982 | 983 | # Find the start of the index: 984 | while index < len( lines ): 985 | 986 | line = lines[ index ] 987 | index += 1 988 | self.pre_index.append( line ) 989 | if line[ : 6 ] == "$INDEX": 990 | break 991 | 992 | m = re.match( "Units[\s]+mm[\s]*", line ) 993 | if m is not None: 994 | print( " Use mm detected" ) 995 | use_mm = True 996 | 997 | # Read the index: 998 | while index < len( lines ): 999 | 1000 | line = lines[ index ] 1001 | if line[ : 9 ] == "$EndINDEX": 1002 | break 1003 | index += 1 1004 | self.loaded_modules[ line.strip() ] = [] 1005 | 1006 | # Read up until the first module: 1007 | while index < len( lines ): 1008 | 1009 | line = lines[ index ] 1010 | if line[ : 7 ] == "$MODULE": 1011 | break 1012 | index += 1 1013 | self.post_index.append( line ) 1014 | 1015 | # Read modules: 1016 | while index < len( lines ): 1017 | 1018 | line = lines[ index ] 1019 | if line[ : 7 ] == "$MODULE": 1020 | module_name, module_lines, index = self._read_module( lines, index ) 1021 | if module_name is not None: 1022 | self.loaded_modules[ module_name ] = module_lines 1023 | 1024 | elif line[ : 11 ] == "$EndLIBRARY": 1025 | break 1026 | 1027 | else: 1028 | raise Exception( 1029 | "Expected $EndLIBRARY: [{}]".format( line ) 1030 | ) 1031 | 1032 | #print( "Pre-index:" ) 1033 | #pprint( self.pre_index ) 1034 | 1035 | #print( "Post-index:" ) 1036 | #pprint( self.post_index ) 1037 | 1038 | #print( "Loaded modules:" ) 1039 | #pprint( self.loaded_modules ) 1040 | 1041 | return use_mm 1042 | 1043 | 1044 | #------------------------------------------------------------------------ 1045 | 1046 | def _read_module( self, lines, index ): 1047 | 1048 | # Read module name: 1049 | m = re.match( r'\$MODULE[\s]+([^\s]+)[\s]*', lines[ index ] ) 1050 | module_name = m.group( 1 ) 1051 | 1052 | print( " Reading module {}".format( module_name ) ) 1053 | 1054 | index += 1 1055 | module_lines = [] 1056 | while index < len( lines ): 1057 | 1058 | line = lines[ index ] 1059 | index += 1 1060 | 1061 | m = re.match( 1062 | r'\$EndMODULE[\s]+' + module_name + r'[\s]*', line 1063 | ) 1064 | if m is not None: 1065 | return module_name, module_lines, index 1066 | 1067 | module_lines.append( line ) 1068 | 1069 | raise Exception( 1070 | "Could not find end of module '{}'".format( module_name ) 1071 | ) 1072 | 1073 | 1074 | #------------------------------------------------------------------------ 1075 | 1076 | def _write_library_intro( self ): 1077 | 1078 | # Write pre-index: 1079 | self.output_file.writelines( self.pre_index ) 1080 | 1081 | self.loaded_modules[ self._get_module_name( front = True ) ] = None 1082 | if self.include_reverse: 1083 | self.loaded_modules[ 1084 | self._get_module_name( front = False ) 1085 | ] = None 1086 | 1087 | # Write index: 1088 | for module_name in sorted( 1089 | self.loaded_modules.iterkeys(), 1090 | key = str.lower 1091 | ): 1092 | self.output_file.write( module_name + "\n" ) 1093 | 1094 | # Write post-index: 1095 | self.output_file.writelines( self.post_index ) 1096 | 1097 | 1098 | #------------------------------------------------------------------------ 1099 | 1100 | def _write_preserved_modules( self, up_to = None ): 1101 | 1102 | if up_to is not None: 1103 | up_to = up_to.lower() 1104 | 1105 | for module_name in sorted( 1106 | self.loaded_modules.iterkeys(), 1107 | key = str.lower 1108 | ): 1109 | if up_to is not None and module_name.lower() >= up_to: 1110 | continue 1111 | 1112 | module_lines = self.loaded_modules[ module_name ] 1113 | 1114 | if module_lines is not None: 1115 | 1116 | self.output_file.write( 1117 | "$MODULE {}\n".format( module_name ) 1118 | ) 1119 | self.output_file.writelines( module_lines ) 1120 | self.output_file.write( 1121 | "$EndMODULE {}\n".format( module_name ) 1122 | ) 1123 | 1124 | self.loaded_modules[ module_name ] = None 1125 | 1126 | 1127 | #------------------------------------------------------------------------ 1128 | 1129 | def _write_module_footer( self, front ): 1130 | 1131 | super( Svg2ModExportLegacyUpdater, self )._write_module_footer( 1132 | front, 1133 | ) 1134 | 1135 | # Write remaining modules: 1136 | if not front: 1137 | self._write_preserved_modules() 1138 | 1139 | 1140 | #------------------------------------------------------------------------ 1141 | 1142 | def _write_module_header( 1143 | self, 1144 | label_size, 1145 | label_pen, 1146 | reference_y, 1147 | value_y, 1148 | front, 1149 | ): 1150 | self._write_preserved_modules( 1151 | up_to = self._get_module_name( front ) 1152 | ) 1153 | 1154 | super( Svg2ModExportLegacyUpdater, self )._write_module_header( 1155 | label_size, 1156 | label_pen, 1157 | reference_y, 1158 | value_y, 1159 | front, 1160 | ) 1161 | 1162 | 1163 | #------------------------------------------------------------------------ 1164 | 1165 | #---------------------------------------------------------------------------- 1166 | 1167 | class Svg2ModExportPretty( Svg2ModExport ): 1168 | 1169 | layer_map = { 1170 | #'inkscape-name' : kicad-name, 1171 | 'Cu' : "{}.Cu", 1172 | 'Adhes' : "{}.Adhes", 1173 | 'Paste' : "{}.Paste", 1174 | 'SilkS' : "{}.SilkS", 1175 | 'Mask' : "{}.Mask", 1176 | 'CrtYd' : "{}.CrtYd", 1177 | 'Fab' : "{}.Fab", 1178 | 'Edge.Cuts' : "Edge.Cuts" 1179 | } 1180 | 1181 | 1182 | #------------------------------------------------------------------------ 1183 | 1184 | def _get_layer_name( self, name, front ): 1185 | 1186 | if front: 1187 | return self.layer_map[ name ].format("F") 1188 | else: 1189 | return self.layer_map[ name ].format("B") 1190 | 1191 | 1192 | #------------------------------------------------------------------------ 1193 | 1194 | def _get_module_name( self, front = None ): 1195 | 1196 | return self.imported.module_name 1197 | 1198 | 1199 | #------------------------------------------------------------------------ 1200 | 1201 | def _write_library_intro( self ): 1202 | 1203 | self.output_file.write( """(module {0} (layer F.Cu) (tedit {1:8X}) 1204 | (attr smd) 1205 | (descr "{2}") 1206 | (tags {3}) 1207 | """.format( 1208 | self.imported.module_name, #0 1209 | int( round( os.path.getctime( #1 1210 | self.imported.file_name 1211 | ) ) ), 1212 | "Imported from {}".format( self.imported.file_name ), #2 1213 | "svg2mod", #3 1214 | ) 1215 | ) 1216 | 1217 | 1218 | #------------------------------------------------------------------------ 1219 | 1220 | def _write_module_footer( self, front ): 1221 | 1222 | self.output_file.write( "\n)" ) 1223 | 1224 | 1225 | #------------------------------------------------------------------------ 1226 | 1227 | def _write_module_header( 1228 | self, 1229 | label_size, 1230 | label_pen, 1231 | reference_y, 1232 | value_y, 1233 | front, 1234 | ): 1235 | if front: 1236 | side = "F" 1237 | else: 1238 | side = "B" 1239 | 1240 | self.output_file.write( 1241 | """ (fp_text reference {0} (at 0 {1}) (layer {2}.SilkS) hide 1242 | (effects (font (size {3} {3}) (thickness {4}))) 1243 | ) 1244 | (fp_text value {5} (at 0 {6}) (layer {2}.SilkS) hide 1245 | (effects (font (size {3} {3}) (thickness {4}))) 1246 | )""".format( 1247 | 1248 | self._get_module_name(), #0 1249 | reference_y, #1 1250 | side, #2 1251 | label_size, #3 1252 | label_pen, #4 1253 | self.imported.module_value, #5 1254 | value_y, #6 1255 | ) 1256 | ) 1257 | 1258 | 1259 | #------------------------------------------------------------------------ 1260 | 1261 | def _write_modules( self ): 1262 | 1263 | self._write_module( front = True ) 1264 | 1265 | 1266 | #------------------------------------------------------------------------ 1267 | 1268 | def _write_polygon( self, points, layer, fill, stroke, stroke_width ): 1269 | 1270 | if fill: 1271 | self._write_polygon_filled( 1272 | points, layer, stroke_width 1273 | ) 1274 | 1275 | # Polygons with a fill and stroke are drawn with the filled polygon 1276 | # above: 1277 | if stroke and not fill: 1278 | 1279 | self._write_polygon_outline( 1280 | points, layer, stroke_width 1281 | ) 1282 | 1283 | 1284 | #------------------------------------------------------------------------ 1285 | 1286 | def _write_polygon_footer( self, layer, stroke_width ): 1287 | 1288 | self.output_file.write( 1289 | " )\n (layer {})\n (width {})\n )".format( 1290 | layer, stroke_width 1291 | ) 1292 | ) 1293 | 1294 | 1295 | #------------------------------------------------------------------------ 1296 | 1297 | def _write_polygon_header( self, points, layer ): 1298 | 1299 | self.output_file.write( "\n (fp_poly\n (pts \n" ) 1300 | 1301 | 1302 | #------------------------------------------------------------------------ 1303 | 1304 | def _write_polygon_point( self, point ): 1305 | 1306 | self.output_file.write( 1307 | " (xy {} {})\n".format( point.x, point.y ) 1308 | ) 1309 | 1310 | 1311 | #------------------------------------------------------------------------ 1312 | 1313 | def _write_polygon_segment( self, p, q, layer, stroke_width ): 1314 | 1315 | self.output_file.write( 1316 | """\n (fp_line 1317 | (start {} {}) 1318 | (end {} {}) 1319 | (layer {}) 1320 | (width {}) 1321 | )""".format( 1322 | p.x, p.y, 1323 | q.x, q.y, 1324 | layer, 1325 | stroke_width, 1326 | ) 1327 | ) 1328 | 1329 | 1330 | #------------------------------------------------------------------------ 1331 | 1332 | #---------------------------------------------------------------------------- 1333 | 1334 | def get_arguments(): 1335 | 1336 | parser = argparse.ArgumentParser( 1337 | description = ( 1338 | 'Convert Inkscape SVG drawings to KiCad footprint modules.' 1339 | ) 1340 | ) 1341 | 1342 | #------------------------------------------------------------------------ 1343 | 1344 | parser.add_argument( 1345 | '-i', '--input-file', 1346 | type = str, 1347 | dest = 'input_file_name', 1348 | metavar = 'FILENAME', 1349 | help = "name of the SVG file", 1350 | required = True, 1351 | ) 1352 | 1353 | parser.add_argument( 1354 | '-o', '--output-file', 1355 | type = str, 1356 | dest = 'output_file_name', 1357 | metavar = 'FILENAME', 1358 | help = "name of the module file", 1359 | ) 1360 | 1361 | parser.add_argument( 1362 | '--name', '--module-name', 1363 | type = str, 1364 | dest = 'module_name', 1365 | metavar = 'NAME', 1366 | help = "base name of the module", 1367 | default = "svg2mod", 1368 | ) 1369 | 1370 | parser.add_argument( 1371 | '--value', '--module-value', 1372 | type = str, 1373 | dest = 'module_value', 1374 | metavar = 'VALUE', 1375 | help = "value of the module", 1376 | default = "G***", 1377 | ) 1378 | 1379 | parser.add_argument( 1380 | '-f', '--factor', 1381 | type = float, 1382 | dest = 'scale_factor', 1383 | metavar = 'FACTOR', 1384 | help = "scale paths by this factor", 1385 | default = 1.0, 1386 | ) 1387 | 1388 | parser.add_argument( 1389 | '-p', '--precision', 1390 | type = float, 1391 | dest = 'precision', 1392 | metavar = 'PRECISION', 1393 | help = "smoothness for approximating curves with line segments (float)", 1394 | default = 10.0, 1395 | ) 1396 | 1397 | parser.add_argument( 1398 | '--front-only', 1399 | dest = 'front_only', 1400 | action = 'store_const', 1401 | const = True, 1402 | help = "omit output of back module (legacy output format)", 1403 | default = False, 1404 | ) 1405 | 1406 | parser.add_argument( 1407 | '--format', 1408 | type = str, 1409 | dest = 'format', 1410 | metavar = 'FORMAT', 1411 | choices = [ 'legacy', 'pretty' ], 1412 | help = "output module file format (legacy|pretty)", 1413 | default = 'pretty', 1414 | ) 1415 | 1416 | parser.add_argument( 1417 | '--units', 1418 | type = str, 1419 | dest = 'units', 1420 | metavar = 'UNITS', 1421 | choices = [ 'decimil', 'mm' ], 1422 | help = "output units, if output format is legacy (decimil|mm)", 1423 | default = 'mm', 1424 | ) 1425 | 1426 | return parser.parse_args(), parser 1427 | 1428 | 1429 | #------------------------------------------------------------------------ 1430 | 1431 | #---------------------------------------------------------------------------- 1432 | 1433 | main() 1434 | 1435 | 1436 | #---------------------------------------------------------------------------- 1437 | # vi: set et sts=4 sw=4 ts=4: 1438 | --------------------------------------------------------------------------------