diff --git a/.gitignore b/.gitignore index 24bc31d..6509060 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ *~ .*.sw? *.pyc +build +dist +svg2mod.egg-info +*.kicad_mod diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ed3b24d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,2 @@ +language: python +script: pip3 install ./ && svg2mod -i examples/dt-logo.svg -o output.kicad_mod --name TEST --value VALUE -f 2 -p 1 --format pretty --units mm -d 300 --center diff --git a/README.md b/README.md index d048ea5..28e1749 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,27 @@ # svg2mod -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. +[![Build Status](https://travis-ci.org/Sodium-Hydrogen/svg2mod.svg?branch=master)](https://travis-ci.org/Sodium-Hydrogen/svg2mod) + +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. + +## Requirements + +Python 3 + +## Installation + +```pip3 install git+https://github.com/Sodium-Hydrogen/svg2mod``` + +If building fails make sure setuptools is up to date. `pip3 install setuptools --upgrade` + +## Example + +```svg2mod -i input.svg -p 1.0``` ## Usage ``` -usage: svg2mod.py [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] - [-f FACTOR] [-p PRECISION] [-d DPI] [--front-only] [--format FORMAT] - [--units UNITS] +usage: svg2mod [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] + [-f FACTOR] [-p PRECISION] [--format FORMAT] [--units UNITS] + [-d DPI] [--center] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -24,10 +40,10 @@ optional arguments: -p PRECISION, --precision PRECISION smoothness for approximating curves with line segments (float) - -d DPI, --dpi DPI DPI of the SVG file (int) - --front-only omit output of back module (legacy output format) --format FORMAT output module file format (legacy|pretty) --units UNITS output units, if output format is legacy (decimil|mm) + -d DPI, --dpi DPI DPI of the SVG file (int) + --center Center the module to the center of the bounding box ``` ## SVG Files @@ -39,9 +55,7 @@ svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain * 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. * Paths with filled areas within holes may not work at all. * 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. - * Layers must be used to indicate the mapping of drawing elements to KiCad layers. - * Layers must be named according to the rules below. - * 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). + * Layers must be named according to the rules below. * Other types of elements such as rect, arc, and circle are not supported. * Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these elements into paths that will work. @@ -50,17 +64,24 @@ Layers must be named (case-insensitive) according to the following rules: | Inkscape layer name | KiCad layer(s) | KiCad legacy | KiCad pretty | |:-------------------:|:----------------:|:------------:|:------------:| -| Cu | F.Cu, B.Cu | Yes | Yes | -| Adhes | F.Adhes, B.Adhes | Yes | Yes | -| Paste | F.Paste, B.Paste | Yes | Yes | -| SilkS | F.SilkS, B.SilkS | Yes | Yes | -| Mask | F.Mask, B.Mask | Yes | Yes | -| Dwgs.User | Dwgs.User | Yes | -- | -| Cmts.User | Cmts.User | Yes | -- | -| Eco1.User | Eco1.User | Yes | -- | -| Eco2.User | Eco2.User | Yes | -- | +| F.Cu | F.Cu | Yes | Yes | +| B.Cu | B.Cu | Yes | Yes | +| F.Adhes | F.Adhes | Yes | Yes | +| B.Adhes | B.Adhes | Yes | Yes | +| F.Paste | F.Paste | Yes | Yes | +| B.Paste | B.Paste | Yes | Yes | +| F.SilkS | F.SilkS | Yes | Yes | +| B.SilkS | B.SilkS | Yes | Yes | +| F.Mask | F.Mask | Yes | Yes | +| B.Mask | B.Mask | Yes | Yes | +| Dwgs.User | Dwgs.User | Yes | Yes | +| Cmts.User | Cmts.User | Yes | Yes | +| Eco1.User | Eco1.User | Yes | Yes | +| Eco2.User | Eco2.User | Yes | Yes | | Edge.Cuts | Edge.Cuts | Yes | Yes | -| Fab | F.Fab, B.Fab | -- | Yes | -| CrtYd | F.CrtYd, B.CrtYd | -- | Yes | +| F.Fab | F.Fab | -- | Yes | +| B.Fab | B.Fab | -- | Yes | +| F.CrtYd | F.CrtYd | -- | Yes | +| B.CrtYd | B.CrtYd | -- | Yes | -Note: If you have a layer "Cu", all of its sub-layers will be treated as "Cu" regardless of their names. +Note: If you have a layer "F.Cu", all of its sub-layers will be treated as "F.Cu" regardless of their names. diff --git a/setup.py b/setup.py index d51ddd7..a05f4e4 100644 --- a/setup.py +++ b/setup.py @@ -25,9 +25,9 @@ version='0.1.0', description="Convert an SVG file to a KiCad footprint.", long_description=readme, - author='https://github.com/mtl', + author='https://github.com/Sodium-Hydrogen', author_email='', - url='https://github.com/mtl/svg2mod', + url='https://github.com/Sodium-Hydrogen/svg2mod', packages=setuptools.find_packages(), entry_points={'console_scripts':['svg2mod = svg2mod.svg2mod:main']}, package_dir={'svg2mod':'svg2mod'}, diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 244fdaa..55c3da2 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -258,11 +258,10 @@ class Group(Transformable): def __init__(self, elt=None): Transformable.__init__(self, elt) - + self.name = "" if elt is not None: - - for id, value in elt.attrib.iteritems(): + for id, value in elt.attrib.items(): id = self.parse_name( id ) if id[ "name" ] == "label": @@ -401,7 +400,7 @@ def parse(self, pathstr): # Close Path l = Segment(current_pt, start_pt) self.items.append(l) - + current_pt = start_pt elif command in 'LHV': # LineTo, Horizontal & Vertical line @@ -708,4 +707,3 @@ def default(self, obj): tag = getattr(cls, 'tag', None) if tag: svgClass[svg_ns + tag] = cls - diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index cc3f060..2e11764 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -60,6 +60,7 @@ def main(): exported = Svg2ModExportPretty( imported, args.output_file_name, + args.center, args.scale_factor, args.precision, args.dpi, @@ -75,10 +76,10 @@ def main(): exported = Svg2ModExportLegacyUpdater( imported, args.output_file_name, + args.center, args.scale_factor, args.precision, args.dpi, - include_reverse = not args.front_only, ) except Exception as e: @@ -91,11 +92,11 @@ def main(): exported = Svg2ModExportLegacy( imported, args.output_file_name, + args.center, args.scale_factor, args.precision, use_mm = use_mm, dpi = args.dpi, - include_reverse = not args.front_only, ) # Export the footprint: @@ -393,7 +394,7 @@ def intersects( self, line_segment, check_connects ): # Apply all transformations and rounding, then remove duplicate # consecutive points along the path. - def process( self, transformer, flip ): + def process( self, transformer, flip, fill ): points = [] for point in self.points: @@ -416,10 +417,11 @@ def process( self, transformer, flip ): #points[ -1 ].x, points[ -1 ].y, #) ) - points.append( svg.Point( - points[ 0 ].x, - points[ 0 ].y, - ) ) + if fill: + points.append( svg.Point( + points[ 0 ].x, + points[ 0 ].y, + ) ) #else: #print( "Polygon closed: start=({}, {}) end=({}, {})".format( @@ -478,7 +480,7 @@ def _get_fill_stroke( self, item ): if item.style is not None and item.style != "": - for property in item.style.split( ";" ): + for property in filter(None, item.style.split( ";" )): nv = property.split( ":" ); name = nv[ 0 ].strip() @@ -491,8 +493,11 @@ def _get_fill_stroke( self, item ): stroke = False elif name == "stroke-width": - value = value.replace( "px", "" ) - stroke_width = float( value ) * 25.4 / float(self.dpi) + if value.endswith("px"): + value = value.replace( "px", "" ) + stroke_width = float( value ) * 25.4 / float(self.dpi) + else: + stroke_width = float( value ) if not stroke: stroke_width = 0.0 @@ -509,6 +514,7 @@ def __init__( self, svg2mod_import, file_name, + center, scale_factor = 1.0, precision = 20.0, use_mm = True, @@ -524,6 +530,7 @@ def __init__( self.imported = svg2mod_import self.file_name = file_name + self.center = center self.scale_factor = scale_factor self.precision = precision self.use_mm = use_mm @@ -535,15 +542,21 @@ def _calculate_translation( self ): min_point, max_point = self.imported.svg.bbox() - # Center the drawing: - adjust_x = min_point.x + ( max_point.x - min_point.x ) / 2.0 - adjust_y = min_point.y + ( max_point.y - min_point.y ) / 2.0 + if(self.center): + # Center the drawing: + adjust_x = min_point.x + ( max_point.x - min_point.x ) / 2.0 + adjust_y = min_point.y + ( max_point.y - min_point.y ) / 2.0 - self.translation = svg.Point( - 0.0 - adjust_x, - 0.0 - adjust_y, - ) + self.translation = svg.Point( + 0.0 - adjust_x, + 0.0 - adjust_y, + ) + else: + self.translation = svg.Point( + 0.0, + 0.0, + ) #------------------------------------------------------------------------ @@ -553,7 +566,7 @@ def _prune( self, items = None ): if items is None: self.layers = {} - for name in self.layer_map.iterkeys(): + for name in self.layer_map.keys(): self.layers[ name ] = None items = self.imported.svg.items @@ -564,7 +577,7 @@ def _prune( self, items = None ): if not isinstance( item, svg.Group ): continue - for name in self.layers.iterkeys(): + for name in self.layers.keys(): #if re.search( name, item.name, re.I ): if name == item.name: print( "Found SVG layer: {}".format( item.name ) ) @@ -594,8 +607,10 @@ def _write_items( self, items, layer, flip = False ): ) ] + fill, stroke, stroke_width = self._get_fill_stroke( item ) + for segment in segments: - segment.process( self, flip ) + segment.process( self, flip, fill ) if len( segments ) > 1: points = segments[ 0 ].inline( segments[ 1 : ] ) @@ -603,8 +618,6 @@ def _write_items( self, items, layer, flip = False ): elif len( segments ) > 0: points = segments[ 0 ].points - fill, stroke, stroke_width = self._get_fill_stroke( item ) - if not self.use_mm: stroke_width = self._convert_mm_to_decimil( stroke_width @@ -653,7 +666,7 @@ def _write_module( self, front ): front, ) - for name, group in self.layers.iteritems(): + for name, group in self.layers.items(): if group is None: continue @@ -743,11 +756,16 @@ class Svg2ModExportLegacy( Svg2ModExport ): layer_map = { #'inkscape-name' : [ kicad-front, kicad-back ], - 'Cu' : [ 15, 0 ], - 'Adhes' : [ 17, 16 ], - 'Paste' : [ 19, 18 ], - 'SilkS' : [ 21, 20 ], - 'Mask' : [ 23, 22 ], + 'F.Cu' : [ 15, 15 ], + 'B.Cu' : [ 0, 0 ], + 'F.Adhes' : [ 17, 17 ], + 'B.Adhes' : [ 16, 16 ], + 'F.Paste' : [ 19, 19 ], + 'B.Paste' : [ 18, 18 ], + 'F.SilkS' : [ 21, 21 ], + 'B.SilkS' : [ 20, 20 ], + 'F.Mask' : [ 23, 23 ], + 'B.Mask' : [ 22, 22 ], 'Dwgs.User' : [ 24, 24 ], 'Cmts.User' : [ 25, 25 ], 'Eco1.User' : [ 26, 26 ], @@ -762,22 +780,23 @@ def __init__( self, svg2mod_import, file_name, + center, scale_factor = 1.0, precision = 20.0, use_mm = True, dpi = DEFAULT_DPI, - include_reverse = True, ): super( Svg2ModExportLegacy, self ).__init__( svg2mod_import, file_name, + center, scale_factor, precision, use_mm, dpi, ) - self.include_reverse = include_reverse + self.include_reverse = True #------------------------------------------------------------------------ @@ -953,6 +972,7 @@ def __init__( self, svg2mod_import, file_name, + center, scale_factor = 1.0, precision = 20.0, dpi = DEFAULT_DPI, @@ -964,11 +984,11 @@ def __init__( super( Svg2ModExportLegacyUpdater, self ).__init__( svg2mod_import, file_name, + center, scale_factor, precision, use_mm, dpi, - include_reverse, ) @@ -1094,7 +1114,7 @@ def _write_library_intro( self ): # Write index: for module_name in sorted( - self.loaded_modules.iterkeys(), + self.loaded_modules.keys(), key = str.lower ): self.output_file.write( module_name + "\n" ) @@ -1111,7 +1131,7 @@ def _write_preserved_modules( self, up_to = None ): up_to = up_to.lower() for module_name in sorted( - self.loaded_modules.iterkeys(), + self.loaded_modules.keys(), key = str.lower ): if up_to is not None and module_name.lower() >= up_to: @@ -1176,14 +1196,25 @@ class Svg2ModExportPretty( Svg2ModExport ): layer_map = { #'inkscape-name' : kicad-name, - 'Cu' : "{}.Cu", - 'Adhes' : "{}.Adhes", - 'Paste' : "{}.Paste", - 'SilkS' : "{}.SilkS", - 'Mask' : "{}.Mask", - 'CrtYd' : "{}.CrtYd", - 'Fab' : "{}.Fab", - 'Edge.Cuts' : "Edge.Cuts" + 'F.Cu' : "F.Cu", + 'B.Cu' : "B.Cu", + 'F.Adhes' : "F.Adhes", + 'B.Adhes' : "B.Adhes", + 'F.Paste' : "F.Paste", + 'B.Paste' : "B.Paste", + 'F.SilkS' : "F.SilkS", + 'B.SilkS' : "B.SilkS", + 'F.Mask' : "F.Mask", + 'B.Mask' : "B.Mask", + 'Dwgs.User' : "Dwgs.User", + 'Cmts.User' : "Cmts.User", + 'Eco1.User' : "Eco1.User", + 'Eco2.User' : "Eco2.User", + 'Edge.Cuts' : "Edge.Cuts", + 'F.CrtYd' : "F.CrtYd", + 'B.CrtYd' : "B.CrtYd", + 'F.Fab' : "F.Fab", + 'B.Fab' : "B.Fab" } @@ -1191,10 +1222,7 @@ class Svg2ModExportPretty( Svg2ModExport ): def _get_layer_name( self, name, front ): - if front: - return self.layer_map[ name ].format("F") - else: - return self.layer_map[ name ].format("B") + return self.layer_map[ name ] #------------------------------------------------------------------------ @@ -1209,7 +1237,7 @@ def _get_module_name( self, front = None ): def _write_library_intro( self ): self.output_file.write( """(module {0} (layer F.Cu) (tedit {1:8X}) - (attr smd) + (attr virtual) (descr "{2}") (tags {3}) """.format( @@ -1402,15 +1430,6 @@ def get_arguments(): default = 10.0, ) - parser.add_argument( - '--front-only', - dest = 'front_only', - action = 'store_const', - const = True, - help = "omit output of back module (legacy output format)", - default = False, - ) - parser.add_argument( '--format', type = str, @@ -1440,6 +1459,15 @@ def get_arguments(): default = DEFAULT_DPI, ) + parser.add_argument( + '--center', + dest = 'center', + action = 'store_const', + const = True, + help = "Center the module to the center of the bounding box", + default = False, + ) + return parser.parse_args(), parser @@ -1451,4 +1479,3 @@ def get_arguments(): #---------------------------------------------------------------------------- -# vi: set et sts=4 sw=4 ts=4: